diff --git a/contribs/gnodev/pkg/dev/node_state.go b/contribs/gnodev/pkg/dev/node_state.go index 557565ea0b1..5194653e88a 100644 --- a/contribs/gnodev/pkg/dev/node_state.go +++ b/contribs/gnodev/pkg/dev/node_state.go @@ -127,7 +127,6 @@ func (n *Node) ExportStateAsGenesis(ctx context.Context) (*bft.GenesisDoc, error // Get current blockstore state doc := *n.Node.GenesisDoc() // copy doc - genState := doc.AppState.(gnoland.GnoGenesisState) genState.Balances = n.config.BalancesList genState.Txs = state diff --git a/examples/gno.land/r/sys/params/params.gno b/examples/gno.land/r/sys/params/params.gno index fa04c90de3f..74334631c6a 100644 --- a/examples/gno.land/r/sys/params/params.gno +++ b/examples/gno.land/r/sys/params/params.gno @@ -19,9 +19,13 @@ package params import ( "std" + prms "sys/params" "gno.land/p/demo/dao" "gno.land/r/gov/dao/bridge" + + // loads the latest DAO implementation in the bridge. + _ "gno.land/r/gov/dao/init" ) func NewStringPropExecutor(key string, value string) dao.Executor { @@ -44,6 +48,26 @@ func NewBytesPropExecutor(key string, value []byte) dao.Executor { return newPropExecutor(key, func() { std.SetParamBytes(key, value) }) } +func NewPrefixedStringPropExecutor(keeperPrefix, key string, value string) dao.Executor { + return newPropExecutor(key, func() { prms.SetPrefixedString(keeperPrefix, key, value) }) +} + +func NewPrefixedInt64PropExecutor(keeperPrefix, key string, value int64) dao.Executor { + return newPropExecutor(key, func() { prms.SetPrefixedInt64(keeperPrefix, key, value) }) +} + +func NewPrefixedUint64PropExecutor(keeperPrefix, key string, value uint64) dao.Executor { + return newPropExecutor(key, func() { prms.SetPrefixedUint64(keeperPrefix, key, value) }) +} + +func NewPrefixedBoolPropExecutor(keeperPrefix, key string, value bool) dao.Executor { + return newPropExecutor(key, func() { prms.SetPrefixedBool(keeperPrefix, key, value) }) +} + +func NewPrefixedBytesPropExecutor(keeperPrefix, key string, value []byte) dao.Executor { + return newPropExecutor(keeperPrefix+key, func() { prms.SetPrefixedBytes(keeperPrefix, key, value) }) +} + func newPropExecutor(key string, fn func()) dao.Executor { callback := func() error { fn() @@ -52,3 +76,13 @@ func newPropExecutor(key string, fn func()) dao.Executor { } return bridge.GovDAO().NewGovDAOExecutor(callback) } +func propose(exec dao.Executor, title, desc string) uint64 { + // The executor's callback function is executed only after the proposal has been voted on + // and approved by the GovDAO. + prop := dao.ProposalRequest{ + Title: title, + Description: desc, + Executor: exec, + } + return bridge.GovDAO().Propose(prop) +} diff --git a/examples/gno.land/r/sys/params/unlock.gno b/examples/gno.land/r/sys/params/unlock.gno new file mode 100644 index 00000000000..1857afda1e8 --- /dev/null +++ b/examples/gno.land/r/sys/params/unlock.gno @@ -0,0 +1,18 @@ +package params + +const ( + bankKeeperPrefix = "bank" + lockTransferKey = "lockTransfer.string" + unlockTransferTitle = "Proposal to unlock the transfer of ugnot." + lockTransferTitle = "Proposal to lock the transfer of ugnot." +) + +func ProposeUnlockTransfer() uint64 { + exe := NewPrefixedStringPropExecutor(bankKeeperPrefix, lockTransferKey, "") + return propose(exe, unlockTransferTitle, "") +} + +func ProposeLockTransfer() uint64 { + exe := NewPrefixedStringPropExecutor(bankKeeperPrefix, lockTransferKey, "ugnot") + return propose(exe, lockTransferTitle, "") +} diff --git a/examples/gno.land/r/sys/params/unlock_test.gno b/examples/gno.land/r/sys/params/unlock_test.gno new file mode 100644 index 00000000000..25c4568af8d --- /dev/null +++ b/examples/gno.land/r/sys/params/unlock_test.gno @@ -0,0 +1,51 @@ +package params + +import ( + "testing" + + "gno.land/p/demo/dao" + "gno.land/p/demo/simpledao" + "gno.land/p/demo/urequire" + "gno.land/r/gov/dao/bridge" +) + +func TestProUnlockTransfer(t *testing.T) { + govdao := bridge.GovDAO() + id := ProposeUnlockTransfer() + p, err := govdao.GetPropStore().ProposalByID(id) + urequire.NoError(t, err) + urequire.Equal(t, unlockTransferTitle, p.Title()) +} + +func TestFailUnlockTransfer(t *testing.T) { + govdao := bridge.GovDAO() + id := ProposeUnlockTransfer() + urequire.PanicsWithMessage( + t, + simpledao.ErrProposalNotAccepted.Error(), + func() { + govdao.ExecuteProposal(id) + }, + ) +} + +func TestExeUnlockTransfer(t *testing.T) { + govdao := bridge.GovDAO() + id := ProposeUnlockTransfer() + p, err := govdao.GetPropStore().ProposalByID(id) + urequire.NoError(t, err) + urequire.True(t, dao.Active == p.Status()) + + govdao.VoteOnProposal(id, dao.YesVote) + + urequire.True(t, dao.Accepted == p.Status()) + + urequire.NotPanics( + t, + func() { + govdao.ExecuteProposal(id) + }, + ) + + urequire.True(t, dao.ExecutionSuccessful == p.Status()) +} diff --git a/gno.land/genesis/genesis_params.toml b/gno.land/genesis/genesis_params.toml index fb080024624..f5dd739955d 100644 --- a/gno.land/genesis/genesis_params.toml +++ b/gno.land/genesis/genesis_params.toml @@ -1,27 +1,31 @@ ## gno.land -["gno.land/r/sys/params.sys"] - users_pkgpath.string = "gno.land/r/sys/users" # if empty, no namespace support. + +# NOTE: These are module parameters actually +#["gno.land/r/sys/params.sys"] +# users_pkgpath.string = "gno.land/r/sys/users" # if empty, no namespace support. # TODO: validators_pkgpath.string = "gno.land/r/sys/validators" # TODO: rewards_pkgpath.string = "gno.land/r/sys/rewards" # TODO: token_lock.bool = true -## gnovm -["gno.land/r/sys/params.vm"] +## gnovm module parameters +["vm"] chain_domain.string = "gno.land" + sysusers_pkgpath.string= "gno.land/r/sys/users" + # TODO: Leverage toml unmarshaler to extract these into VM Params struct before writing to genesis # TODO: max_gas.int64 = 100_000_000 # TODO: chain_tz.string = "UTC" # TODO: default_storage_allowance.string = "" ## tm2 -["gno.land/r/sys/params.tm2"] +# ["gno.land/r/sys/params.tm2"] ## misc -["gno.land/r/sys/params.misc"] +# ["gno.land/r/sys/params.misc"] -## testing +## testing arbirary parameters # do not remove these lines. they are needed for a txtar integration test. -["gno.land/r/sys/params.test"] +["vm:gno.land/r/sys/params.test"] foo.string = "bar" foo.int64 = -1337 foo.uint64 = 42 diff --git a/gno.land/pkg/gnoland/app.go b/gno.land/pkg/gnoland/app.go index 0826071b9f5..0c3270b2a4c 100644 --- a/gno.land/pkg/gnoland/app.go +++ b/gno.land/pkg/gnoland/app.go @@ -96,14 +96,18 @@ func NewAppWithOptions(cfg *AppOptions) (abci.Application, error) { baseApp.MountStoreWithDB(baseKey, dbadapter.StoreConstructor, cfg.DB) // Construct keepers. - paramsKpr := params.NewParamsKeeper(mainKey, "vm") + + paramsKpr := params.NewParamsKeeper(mainKey) acctKpr := auth.NewAccountKeeper(mainKey, paramsKpr, ProtoGnoAccount) + bankKpr := bank.NewBankKeeper(acctKpr, paramsKpr) gpKpr := auth.NewGasPriceKeeper(mainKey) - bankKpr := bank.NewBankKeeper(acctKpr) - vmk := vm.NewVMKeeper(baseKey, mainKey, acctKpr, bankKpr, paramsKpr) vmk.Output = cfg.VMOutput + paramsKpr.Register(acctKpr.GetParamfulKey(), acctKpr) + paramsKpr.Register(bankKpr.GetParamfulKey(), bankKpr) + paramsKpr.Register(vmk.GetParamfulKey(), vmk) + // Set InitChainer icc := cfg.InitChainerConfig icc.baseApp = baseApp @@ -123,7 +127,6 @@ func NewAppWithOptions(cfg *AppOptions) (abci.Application, error) { ) { // Add last gas price in the context ctx = ctx.WithValue(auth.GasPriceContextKey{}, gpKpr.LastGasPrice(ctx)) - // Override auth params. ctx = ctx.WithValue(auth.AuthParamsContextKey{}, acctKpr.GetParams(ctx)) // Continue on with default auth ante handler. @@ -325,11 +328,8 @@ func (cfg InitChainerConfig) loadAppState(ctx sdk.Context, appState any) ([]abci if !ok { return nil, fmt.Errorf("invalid AppState of type %T", appState) } - cfg.acctKpr.InitGenesis(ctx, state.Auth) - params := cfg.acctKpr.GetParams(ctx) - ctx = ctx.WithValue(auth.AuthParamsContextKey{}, params) - auth.InitChainer(ctx, cfg.gpKpr.(auth.GasPriceKeeper), params.InitialGasPrice) + cfg.bankKpr.InitGenesis(ctx, state.Bank) // Apply genesis balances. for _, bal := range state.Balances { acc := cfg.acctKpr.NewAccountWithAddress(ctx, bal.Address) @@ -344,6 +344,25 @@ func (cfg InitChainerConfig) loadAppState(ctx sdk.Context, appState any) ([]abci for _, param := range state.Params { param.register(ctx, cfg.paramsKpr) } + // The account keeper's initial genesis state must be set after genesis + // accounts are created in account keeeper with genesis balances + cfg.acctKpr.InitGenesis(ctx, state.Auth) + + // The unrestricted address must have been created as one of the genesis accounts. + // Otherwise, we cannot verify the unrestricted address in the genesis state. + + for _, addr := range state.Auth.Params.UnrestrictedAddrs { + acc := cfg.acctKpr.GetAccount(ctx, addr) + accr := acc.(*GnoAccount) + accr.SetUnrestricted() + cfg.acctKpr.SetAccount(ctx, acc) + } + + cfg.vmKpr.InitGenesis(ctx, state.VM) + + params := cfg.acctKpr.GetParams(ctx) + ctx = ctx.WithValue(auth.AuthParamsContextKey{}, params) + auth.InitChainer(ctx, cfg.gpKpr, params.InitialGasPrice) // Replay genesis txs. txResponses := make([]abci.ResponseDeliverTx, 0, len(state.Txs)) diff --git a/gno.land/pkg/gnoland/app_test.go b/gno.land/pkg/gnoland/app_test.go index 5d0de49d457..8bd99d7f952 100644 --- a/gno.land/pkg/gnoland/app_test.go +++ b/gno.land/pkg/gnoland/app_test.go @@ -108,12 +108,13 @@ func TestNewAppWithOptions(t *testing.T) { path string expectedVal string }{ - {"params/vm/foo.string", `"hello"`}, - {"params/vm/foo.int64", `"-42"`}, - {"params/vm/foo.uint64", `"1337"`}, - {"params/vm/foo.bool", `true`}, - {"params/vm/foo.bytes", `"SGkh"`}, // XXX: make this test more readable + {"params/foo.string", `"hello"`}, + {"params/foo.int64", `"-42"`}, + {"params/foo.uint64", `"1337"`}, + {"params/foo.bool", `true`}, + {"params/foo.bytes", `"SGkh"`}, // XXX: make this test more readable } + for _, tc := range tcs { qres := bapp.Query(abci.RequestQuery{ Path: tc.path, @@ -216,12 +217,13 @@ func testInitChainerLoadStdlib(t *testing.T, cached bool) { //nolint:thelper cfg := InitChainerConfig{ StdlibDir: stdlibDir, vmKpr: mock, + acctKpr: &mockAuthKeeper{}, + bankKpr: &mockBankKeeper{}, + paramsKpr: &mockParamsKeeper{}, + gpKpr: &mockGasPriceKeeper{}, CacheStdlibLoad: cached, } - // Construct keepers. - paramsKpr := params.NewParamsKeeper(iavlCapKey, "") - cfg.acctKpr = auth.NewAccountKeeper(iavlCapKey, paramsKpr, ProtoGnoAccount) - cfg.gpKpr = auth.NewGasPriceKeeper(iavlCapKey) + cfg.InitChainer(testCtx, abci.RequestInitChain{ AppState: DefaultGenState(), }) @@ -311,6 +313,9 @@ func TestInitChainer_MetadataTxs(t *testing.T) { }, // Make sure the deployer account has a balance Balances: balances, + Auth: auth.DefaultGenesisState(), + Bank: bank.DefaultGenesisState(), + VM: vm.DefaultGenesisState(), } } @@ -322,6 +327,9 @@ func TestInitChainer_MetadataTxs(t *testing.T) { }, }, Balances: balances, + Auth: auth.DefaultGenesisState(), + Bank: bank.DefaultGenesisState(), + VM: vm.DefaultGenesisState(), } } ) @@ -822,12 +830,14 @@ func newGasPriceTestApp(t *testing.T) abci.Application { baseApp.MountStoreWithDB(baseKey, dbadapter.StoreConstructor, cfg.DB) // Construct keepers. - paramsKpr := params.NewParamsKeeper(mainKey, "") + paramsKpr := params.NewParamsKeeper(mainKey) acctKpr := auth.NewAccountKeeper(mainKey, paramsKpr, ProtoGnoAccount) gpKpr := auth.NewGasPriceKeeper(mainKey) - bankKpr := bank.NewBankKeeper(acctKpr) + bankKpr := bank.NewBankKeeper(acctKpr, paramsKpr) vmk := vm.NewVMKeeper(baseKey, mainKey, acctKpr, bankKpr, paramsKpr) - + paramsKpr.Register(acctKpr.GetParamfulKey(), acctKpr) + paramsKpr.Register(bankKpr.GetParamfulKey(), bankKpr) + paramsKpr.Register(vmk.GetParamfulKey(), vmk) // Set InitChainer icc := cfg.InitChainerConfig icc.baseApp = baseApp @@ -950,6 +960,10 @@ func gnoGenesisState(t *testing.T) GnoGenesisState { } }`) err := amino.UnmarshalJSON(genBytes, &gen) + + gen.Bank = bank.DefaultGenesisState() + gen.VM = vm.DefaultGenesisState() + if err != nil { t.Fatalf("failed to create genesis state: %v", err) } diff --git a/gno.land/pkg/gnoland/genesis.go b/gno.land/pkg/gnoland/genesis.go index a754e7a4644..d5f2ff19097 100644 --- a/gno.land/pkg/gnoland/genesis.go +++ b/gno.land/pkg/gnoland/genesis.go @@ -13,6 +13,7 @@ import ( "github.com/gnolang/gno/tm2/pkg/crypto" osm "github.com/gnolang/gno/tm2/pkg/os" "github.com/gnolang/gno/tm2/pkg/sdk/auth" + "github.com/gnolang/gno/tm2/pkg/sdk/bank" "github.com/gnolang/gno/tm2/pkg/std" "github.com/pelletier/go-toml" ) @@ -77,11 +78,18 @@ func LoadGenesisParamsFile(path string) ([]Param, error) { } params := make([]Param, 0) + // By default parameters are grouped by modules. the module separator is ":" + for category, keys := range m { + separator := ":" + // the category prefixed with "vm:" contains arbirary parameters + if strings.HasPrefix(category, "vm:") { + separator = "." + } for key, kinds := range keys { for kind, val := range kinds { param := Param{ - key: category + "." + key, + key: category + separator + key, kind: kind, } switch kind { @@ -200,7 +208,8 @@ func DefaultGenState() GnoGenesisState { Balances: []Balance{}, Txs: []TxWithMetadata{}, Auth: authGen, + Bank: bank.DefaultGenesisState(), + VM: vmm.DefaultGenesisState(), } - return gs } diff --git a/gno.land/pkg/gnoland/mock_test.go b/gno.land/pkg/gnoland/mock_test.go index 62aecaf5278..322df79ddca 100644 --- a/gno.land/pkg/gnoland/mock_test.go +++ b/gno.land/pkg/gnoland/mock_test.go @@ -4,10 +4,15 @@ import ( "log/slog" "github.com/gnolang/gno/gno.land/pkg/sdk/vm" + "github.com/gnolang/gno/tm2/pkg/crypto" "github.com/gnolang/gno/tm2/pkg/events" "github.com/gnolang/gno/tm2/pkg/log" "github.com/gnolang/gno/tm2/pkg/sdk" + "github.com/gnolang/gno/tm2/pkg/sdk/auth" + "github.com/gnolang/gno/tm2/pkg/sdk/bank" + "github.com/gnolang/gno/tm2/pkg/service" + "github.com/gnolang/gno/tm2/pkg/std" ) type ( @@ -113,6 +118,84 @@ func (m *mockVMKeeper) CommitGnoTransactionStore(ctx sdk.Context) { } } +func (m *mockVMKeeper) InitGenesis(ctx sdk.Context, gs vm.GenesisState) {} + +type mockBankKeeper struct{} + +func (m *mockBankKeeper) InputOutputCoins(ctx sdk.Context, inputs []bank.Input, outputs []bank.Output) error { + return nil +} + +func (m *mockBankKeeper) SendCoins(ctx sdk.Context, fromAddr crypto.Address, toAddr crypto.Address, amt std.Coins) error { + return nil +} + +func (m *mockBankKeeper) SendCoinsUnrestricted(ctx sdk.Context, fromAddr crypto.Address, toAddr crypto.Address, amt std.Coins) error { + return nil +} + +func (m *mockBankKeeper) SubtractCoins(ctx sdk.Context, addr crypto.Address, amt std.Coins) (std.Coins, error) { + return nil, nil +} + +func (m *mockBankKeeper) AddCoins(ctx sdk.Context, addr crypto.Address, amt std.Coins) (std.Coins, error) { + return nil, nil +} + +func (m *mockBankKeeper) InitGenesis(ctx sdk.Context, data bank.GenesisState) {} +func (m *mockBankKeeper) GetParams(ctx sdk.Context) bank.Params { return bank.Params{} } +func (m *mockBankKeeper) GetCoins(ctx sdk.Context, addr crypto.Address) std.Coins { return nil } +func (m *mockBankKeeper) SetCoins(ctx sdk.Context, addr crypto.Address, amt std.Coins) error { + return nil +} + +func (m *mockBankKeeper) HasCoins(ctx sdk.Context, addr crypto.Address, amt std.Coins) bool { + return true +} + +type mockAuthKeeper struct{} + +func (m *mockAuthKeeper) NewAccountWithAddress(ctx sdk.Context, addr crypto.Address) std.Account { + return nil +} +func (m *mockAuthKeeper) GetAccount(ctx sdk.Context, addr crypto.Address) std.Account { return nil } +func (m *mockAuthKeeper) GetAllAccounts(ctx sdk.Context) []std.Account { return nil } +func (m *mockAuthKeeper) SetAccount(ctx sdk.Context, acc std.Account) {} +func (m *mockAuthKeeper) IterateAccounts(ctx sdk.Context, process func(std.Account) bool) {} +func (m *mockAuthKeeper) InitGenesis(ctx sdk.Context, data auth.GenesisState) {} +func (m *mockAuthKeeper) GetParams(ctx sdk.Context) auth.Params { return auth.Params{} } + +type mockParamsKeeper struct{} + +func (m *mockParamsKeeper) GetString(ctx sdk.Context, key string, ptr *string) {} +func (m *mockParamsKeeper) GetInt64(ctx sdk.Context, key string, ptr *int64) {} +func (m *mockParamsKeeper) GetUint64(ctx sdk.Context, key string, ptr *uint64) {} +func (m *mockParamsKeeper) GetBool(ctx sdk.Context, key string, ptr *bool) {} +func (m *mockParamsKeeper) GetBytes(ctx sdk.Context, key string, ptr *[]byte) {} + +func (m *mockParamsKeeper) SetString(ctx sdk.Context, key string, value string) {} +func (m *mockParamsKeeper) SetInt64(ctx sdk.Context, key string, value int64) {} +func (m *mockParamsKeeper) SetUint64(ctx sdk.Context, key string, value uint64) {} +func (m *mockParamsKeeper) SetBool(ctx sdk.Context, key string, value bool) {} +func (m *mockParamsKeeper) SetBytes(ctx sdk.Context, key string, value []byte) {} + +func (m *mockParamsKeeper) Has(ctx sdk.Context, key string) bool { return false } +func (m *mockParamsKeeper) GetRaw(ctx sdk.Context, key string) []byte { return nil } + +func (m *mockParamsKeeper) GetParams(ctx sdk.Context, prefixKey string, key string, target interface{}) (bool, error) { + return true, nil +} + +func (m *mockParamsKeeper) SetParams(ctx sdk.Context, prefixKey string, key string, params interface{}) error { + return nil +} + +type mockGasPriceKeeper struct{} + +func (m *mockGasPriceKeeper) LastGasPrice(ctx sdk.Context) std.GasPrice { return std.GasPrice{} } +func (m *mockGasPriceKeeper) SetGasPrice(ctx sdk.Context, gp std.GasPrice) {} +func (m *mockGasPriceKeeper) UpdateGasPrice(ctx sdk.Context) {} + type ( lastBlockHeightDelegate func() int64 loggerDelegate func() *slog.Logger diff --git a/gno.land/pkg/gnoland/node_inmemory.go b/gno.land/pkg/gnoland/node_inmemory.go index 3fd3063f8b9..0eccad56922 100644 --- a/gno.land/pkg/gnoland/node_inmemory.go +++ b/gno.land/pkg/gnoland/node_inmemory.go @@ -40,7 +40,7 @@ func NewMockedPrivValidator() bft.PrivValidator { func NewDefaultGenesisConfig(chainid, chaindomain string) *bft.GenesisDoc { // custom chain domain var domainParam Param - _ = domainParam.Parse("gno.land/r/sys/params.vm.chain_domain.string=" + chaindomain) + _ = domainParam.Parse("vm:chain_domain.string=" + chaindomain) return &bft.GenesisDoc{ GenesisTime: time.Now(), diff --git a/gno.land/pkg/gnoland/test_common.go b/gno.land/pkg/gnoland/test_common.go new file mode 100644 index 00000000000..b4611706031 --- /dev/null +++ b/gno.land/pkg/gnoland/test_common.go @@ -0,0 +1,53 @@ +package gnoland + +import ( + abci "github.com/gnolang/gno/tm2/pkg/bft/abci/types" + bft "github.com/gnolang/gno/tm2/pkg/bft/types" + "github.com/gnolang/gno/tm2/pkg/db/memdb" + "github.com/gnolang/gno/tm2/pkg/log" + "github.com/gnolang/gno/tm2/pkg/sdk" + "github.com/gnolang/gno/tm2/pkg/sdk/auth" + "github.com/gnolang/gno/tm2/pkg/sdk/bank" + "github.com/gnolang/gno/tm2/pkg/sdk/params" + + "github.com/gnolang/gno/tm2/pkg/store" + "github.com/gnolang/gno/tm2/pkg/store/iavl" +) + +type testEnv struct { + ctx sdk.Context + acck auth.AccountKeeper + bank bank.BankKeeper +} + +func setupTestEnv() testEnv { + db := memdb.NewMemDB() + + authCapKey := store.NewStoreKey("authCapKey") + + ms := store.NewCommitMultiStore(db) + ms.MountStoreWithDB(authCapKey, iavl.StoreConstructor, db) + ms.LoadLatestVersion() + paramk := params.NewParamsKeeper(authCapKey) + acck := auth.NewAccountKeeper(authCapKey, paramk, ProtoGnoAccount) + bank := bank.NewBankKeeper(acck, paramk) + paramk.Register(acck.GetParamfulKey(), acck) + paramk.Register(bank.GetParamfulKey(), bank) + + ctx := sdk.NewContext(sdk.RunTxModeDeliver, ms, &bft.Header{Height: 1, ChainID: "test-chain-id"}, log.NewNoopLogger()) + + ctx = ctx.WithConsensusParams(&abci.ConsensusParams{ + Block: &abci.BlockParams{ + MaxTxBytes: 1024, + MaxDataBytes: 1024 * 100, + MaxBlockBytes: 1024 * 100, + MaxGas: 10 * 1000 * 1000, + TimeIotaMS: 10, + }, + Validator: &abci.ValidatorParams{ + PubKeyTypeURLs: []string{}, // XXX + }, + }) + + return testEnv{ctx: ctx, acck: acck, bank: bank} +} diff --git a/gno.land/pkg/gnoland/types.go b/gno.land/pkg/gnoland/types.go index 66fb2f54e8a..e45164406de 100644 --- a/gno.land/pkg/gnoland/types.go +++ b/gno.land/pkg/gnoland/types.go @@ -7,9 +7,11 @@ import ( "fmt" "os" + "github.com/gnolang/gno/gno.land/pkg/sdk/vm" "github.com/gnolang/gno/tm2/pkg/amino" "github.com/gnolang/gno/tm2/pkg/crypto" "github.com/gnolang/gno/tm2/pkg/sdk/auth" + "github.com/gnolang/gno/tm2/pkg/sdk/bank" "github.com/gnolang/gno/tm2/pkg/std" ) @@ -18,8 +20,80 @@ var ( ErrBalanceEmptyAmount = errors.New("balance amount is empty") ) +const ( + // unrestricted allows unrestricted transfers. + unrestricted BitSet = 1 << iota + + // TODO: validatorAccount marks an account as validator. + validatorAccount + + // TODO: realmAccount marks an account as realm. + realmAccount +) + +// bitSet represents a set of flags stored in a 64-bit unsigned integer. +// Each bit in the BitSet corresponds to a specific flag. +type BitSet uint64 + +func (bs BitSet) String() string { + return fmt.Sprintf("0x%016X", uint64(bs)) // Show all 64 bits +} + +var _ std.AccountRestricter = &GnoAccount{} + type GnoAccount struct { std.BaseAccount + Attributes BitSet `json:"attributes" yaml:"attributes"` +} + +// validFlags defines the set of all valid flags that can be used with BitSet. +var validFlags = unrestricted | validatorAccount | realmAccount + +func (ga *GnoAccount) setFlag(flag BitSet) { + if !isValidFlag(flag) { + panic(fmt.Sprintf("setFlag: invalid flag %d (binary: %b). Valid flags: %b", flag, flag, validFlags)) + } + ga.Attributes |= flag +} + +func (ga *GnoAccount) clearFlag(flag BitSet) { + if !isValidFlag(flag) { + panic(fmt.Sprintf("clearFlag: invalid flag %d (binary: %b). Valid flags: %b", flag, flag, validFlags)) + } + ga.Attributes &= ^flag +} + +func (ga *GnoAccount) hasFlag(flag BitSet) bool { + if !isValidFlag(flag) { + panic(fmt.Sprintf("hasFlag: invalid flag %d (binary: %b). Valid flags: %b", flag, flag, validFlags)) + } + return ga.Attributes&flag != 0 +} + +// isValidFlag ensures that a given BitSet uses only the allowed subset of bits +// as defined in validFlags. This prevents accidentally setting invalid flags, +// especially since BitSet can represent all 64 bits of a uint64. +func isValidFlag(flag BitSet) bool { + return flag&^validFlags == 0 && flag != 0 +} + +// SetUnrestricted allows the account to bypass global transfer locking restrictions. +// By default, accounts are restricted when global transfer locking is enabled. +func (ga *GnoAccount) SetUnrestricted() { + ga.setFlag(unrestricted) +} + +// IsUnrestricted checks whether the account is unrestricted. +func (ga *GnoAccount) IsUnrestricted() bool { + return ga.hasFlag(unrestricted) +} + +// String implements fmt.Stringer +func (ga *GnoAccount) String() string { + return fmt.Sprintf("%s\n Attributes: %s", + ga.BaseAccount.String(), + ga.Attributes.String(), + ) } func ProtoGnoAccount() std.Account { @@ -29,8 +103,10 @@ func ProtoGnoAccount() std.Account { type GnoGenesisState struct { Balances []Balance `json:"balances"` Txs []TxWithMetadata `json:"txs"` - Params []Param `json:"params"` Auth auth.GenesisState `json:"auth"` + Bank bank.GenesisState `json:"bank"` + VM vm.GenesisState `json:"vm"` + Params []Param `json:"params"` } type TxWithMetadata struct { diff --git a/gno.land/pkg/gnoland/types_test.go b/gno.land/pkg/gnoland/types_test.go index c501325bc3e..2516fd65ec6 100644 --- a/gno.land/pkg/gnoland/types_test.go +++ b/gno.land/pkg/gnoland/types_test.go @@ -131,6 +131,98 @@ func TestReadGenesisTxs(t *testing.T) { }) } +func TestGnoAccountRestriction(t *testing.T) { + testEnv := setupTestEnv() + ctx, acckpr, bankpr := testEnv.ctx, testEnv.acck, testEnv.bank + + fromAddress := crypto.AddressFromPreimage([]byte("from")) + toAddress := crypto.AddressFromPreimage([]byte("to")) + fromAccount := acckpr.NewAccountWithAddress(ctx, fromAddress) + toAccount := acckpr.NewAccountWithAddress(ctx, toAddress) + + // Default account is not unrestricted + assert.False(t, fromAccount.(*GnoAccount).IsUnrestricted()) + + // Send Unrestricted + fromAccount.SetCoins(std.NewCoins(std.NewCoin("foocoin", 10))) + acckpr.SetAccount(ctx, fromAccount) + acckpr.SetAccount(ctx, toAccount) + + err := bankpr.SendCoins(ctx, fromAddress, toAddress, std.NewCoins(std.NewCoin("foocoin", 3))) + require.NoError(t, err) + balance := acckpr.GetAccount(ctx, toAddress).GetCoins() + assert.Equal(t, balance.String(), "3foocoin") + + // Send Restricted + bankpr.AddRestrictedDenoms(ctx, "foocoin") + err = bankpr.SendCoins(ctx, fromAddress, toAddress, std.NewCoins(std.NewCoin("foocoin", 3))) + require.Error(t, err) + assert.Equal(t, "restricted token transfer error", err.Error()) + + // Set unrestrict Account + fromAccount.(*GnoAccount).SetUnrestricted() + assert.True(t, fromAccount.(*GnoAccount).IsUnrestricted()) + + // Persisted unrestricted state + acckpr.SetAccount(ctx, fromAccount) + fromAccount = acckpr.GetAccount(ctx, fromAddress) + assert.True(t, fromAccount.(*GnoAccount).IsUnrestricted()) + + // Send Restricted + bankpr.AddRestrictedDenoms(ctx, "foocoin") + err = bankpr.SendCoins(ctx, fromAddress, toAddress, std.NewCoins(std.NewCoin("foocoin", 3))) + require.NoError(t, err) + assert.Equal(t, balance.String(), "3foocoin") +} + +func TestGnoAccountSendRestrictions(t *testing.T) { + testEnv := setupTestEnv() + ctx, acckpr, bankpr := testEnv.ctx, testEnv.acck, testEnv.bank + + bankpr.AddRestrictedDenoms(ctx, "foocoin") + addr := crypto.AddressFromPreimage([]byte("addr1")) + addr2 := crypto.AddressFromPreimage([]byte("addr2")) + acc := acckpr.NewAccountWithAddress(ctx, addr) + + // All accounts are restricted by default when the transfer restriction is applied. + + // Test GetCoins/SetCoins + acckpr.SetAccount(ctx, acc) + require.True(t, bankpr.GetCoins(ctx, addr).IsEqual(std.NewCoins())) + + bankpr.SetCoins(ctx, addr, std.NewCoins(std.NewCoin("foocoin", 10))) + require.True(t, bankpr.GetCoins(ctx, addr).IsEqual(std.NewCoins(std.NewCoin("foocoin", 10)))) + + // Test HasCoins + require.True(t, bankpr.HasCoins(ctx, addr, std.NewCoins(std.NewCoin("foocoin", 10)))) + require.True(t, bankpr.HasCoins(ctx, addr, std.NewCoins(std.NewCoin("foocoin", 5)))) + require.False(t, bankpr.HasCoins(ctx, addr, std.NewCoins(std.NewCoin("foocoin", 15)))) + require.False(t, bankpr.HasCoins(ctx, addr, std.NewCoins(std.NewCoin("barcoin", 5)))) + + bankpr.SetCoins(ctx, addr, std.NewCoins(std.NewCoin("foocoin", 15))) + + // Test sending coins restricted to locked accounts. + err := bankpr.SendCoins(ctx, addr, addr2, std.NewCoins(std.NewCoin("foocoin", 5))) + require.ErrorIs(t, err, std.RestrictedTransferError{}, "expected restricted transfer error, got %v", err) + require.True(t, bankpr.GetCoins(ctx, addr).IsEqual(std.NewCoins(std.NewCoin("foocoin", 15)))) + require.True(t, bankpr.GetCoins(ctx, addr2).IsEqual(std.NewCoins(std.NewCoin("foocoin", 0)))) + + // Test sending coins unrestricted to locked accounts. + bankpr.AddCoins(ctx, addr, std.NewCoins(std.NewCoin("barcoin", 30))) + err = bankpr.SendCoins(ctx, addr, addr2, std.NewCoins(std.NewCoin("barcoin", 10))) + require.NoError(t, err) + require.True(t, bankpr.GetCoins(ctx, addr).IsEqual(std.NewCoins(std.NewCoin("barcoin", 20), std.NewCoin("foocoin", 15)))) + require.True(t, bankpr.GetCoins(ctx, addr2).IsEqual(std.NewCoins(std.NewCoin("barcoin", 10)))) + + // Remove the restrictions + bankpr.DelAllRestrictedDenoms(ctx) + // Test sending coins restricted to locked accounts. + err = bankpr.SendCoins(ctx, addr, addr2, std.NewCoins(std.NewCoin("foocoin", 5))) + require.NoError(t, err) + require.True(t, bankpr.GetCoins(ctx, addr).IsEqual(std.NewCoins(std.NewCoin("barcoin", 20), std.NewCoin("foocoin", 10)))) + require.True(t, bankpr.GetCoins(ctx, addr2).IsEqual(std.NewCoins(std.NewCoin("barcoin", 10), std.NewCoin("foocoin", 5)))) +} + func TestSignGenesisTx(t *testing.T) { t.Parallel() @@ -156,3 +248,27 @@ func TestSignGenesisTx(t *testing.T) { assert.True(t, pubKey.VerifyBytes(payload, sigs[0].Signature)) } } + +func TestSetFlag(t *testing.T) { + account := &GnoAccount{} + + // Test setting a valid flag + account.setFlag(unrestricted) + assert.True(t, account.hasFlag(unrestricted), "Expected unrestricted flag to be set") + + // Test setting an invalid flag + assert.Panics(t, func() { + account.setFlag(BitSet(0x1000)) // Invalid flag + }, "Expected panic for invalid flag") +} + +func TestClearFlag(t *testing.T) { + account := &GnoAccount{} + + // Set and then clear the flag + account.setFlag(unrestricted) + assert.True(t, account.hasFlag(unrestricted), "Expected unrestricted flag to be set before clearing") + + account.clearFlag(unrestricted) + assert.False(t, account.hasFlag(unrestricted), "Expected unrestricted flag to be cleared") +} diff --git a/gno.land/pkg/integration/node_testing.go b/gno.land/pkg/integration/node_testing.go index 1af699f014d..ed966c51312 100644 --- a/gno.land/pkg/integration/node_testing.go +++ b/gno.land/pkg/integration/node_testing.go @@ -16,6 +16,8 @@ import ( bft "github.com/gnolang/gno/tm2/pkg/bft/types" "github.com/gnolang/gno/tm2/pkg/crypto" "github.com/gnolang/gno/tm2/pkg/db/memdb" + "github.com/gnolang/gno/tm2/pkg/sdk/auth" + "github.com/gnolang/gno/tm2/pkg/sdk/bank" "github.com/gnolang/gno/tm2/pkg/std" "github.com/stretchr/testify/require" ) @@ -60,19 +62,17 @@ func TestingNodeConfig(t TestingTS, gnoroot string, additionalTxs ...gnoland.TxW cfg := TestingMinimalNodeConfig(gnoroot) cfg.SkipGenesisVerification = true - creator := crypto.MustAddressFromString(DefaultAccount_Address) // test1 - params := LoadDefaultGenesisParamFile(t, gnoroot) + creator := crypto.MustAddressFromString(DefaultAccount_Address) // test1 balances := LoadDefaultGenesisBalanceFile(t, gnoroot) txs := make([]gnoland.TxWithMetadata, 0) txs = append(txs, LoadDefaultPackages(t, creator, gnoroot)...) txs = append(txs, additionalTxs...) - - cfg.Genesis.AppState = gnoland.GnoGenesisState{ - Balances: balances, - Txs: txs, - Params: params, - } + ggs := cfg.Genesis.AppState.(gnoland.GnoGenesisState) + ggs.Balances = balances + ggs.Txs = txs + ggs.Params = params + cfg.Genesis.AppState = ggs return cfg, creator } @@ -100,6 +100,20 @@ func TestingMinimalNodeConfig(gnoroot string) *gnoland.InMemoryNodeConfig { } func DefaultTestingGenesisConfig(gnoroot string, self crypto.PubKey, tmconfig *tmcfg.Config) *bft.GenesisDoc { + authGen := auth.DefaultGenesisState() + authGen.Params.UnrestrictedAddrs = []crypto.Address{crypto.MustAddressFromString(DefaultAccount_Address)} + authGen.Params.InitialGasPrice = std.GasPrice{Gas: 0, Price: std.Coin{Amount: 0, Denom: "ugnot"}} + genState := gnoland.DefaultGenState() + genState.Balances = []gnoland.Balance{ + { + Address: crypto.MustAddressFromString(DefaultAccount_Address), + Amount: std.MustParseCoins(ugnot.ValueString(10000000000000)), + }, + } + genState.Txs = []gnoland.TxWithMetadata{} + genState.Params = []gnoland.Param{} + genState.Auth = authGen + genState.Bank = bank.DefaultGenesisState() return &bft.GenesisDoc{ GenesisTime: time.Now(), ChainID: tmconfig.ChainID(), @@ -119,16 +133,7 @@ func DefaultTestingGenesisConfig(gnoroot string, self crypto.PubKey, tmconfig *t Name: "self", }, }, - AppState: gnoland.GnoGenesisState{ - Balances: []gnoland.Balance{ - { - Address: crypto.MustAddressFromString(DefaultAccount_Address), - Amount: std.MustParseCoins(ugnot.ValueString(10_000_000_000_000)), - }, - }, - Txs: []gnoland.TxWithMetadata{}, - Params: []gnoland.Param{}, - }, + AppState: genState, } } @@ -213,5 +218,8 @@ func GenerateTestingGenesisState(creator crypto.PrivKey, pkgs ...gnovm.MemPackag Address: creator.PubKey().Address(), Amount: std.MustParseCoins(ugnot.ValueString(10_000_000_000_000)), }}, + Auth: auth.DefaultGenesisState(), + Bank: bank.DefaultGenesisState(), + VM: vmm.DefaultGenesisState(), } } diff --git a/gno.land/pkg/integration/testdata/adduserfrom.txtar b/gno.land/pkg/integration/testdata/adduserfrom.txtar index 8bbfaa738fd..a4b71479cae 100644 --- a/gno.land/pkg/integration/testdata/adduserfrom.txtar +++ b/gno.land/pkg/integration/testdata/adduserfrom.txtar @@ -27,7 +27,7 @@ stdout ' "BaseAccount": {' stdout ' "address": "g1mtmrdmqfu0aryqfl4aw65n35haw2wdjkh5p4cp",' stdout ' "coins": "10000000ugnot",' stdout ' "public_key": null,' -stdout ' "account_number": "59",' +stdout ' "account_number": "60",' stdout ' "sequence": "0"' stdout ' }' stdout '}' diff --git a/gno.land/pkg/integration/testdata/genesis_params.txtar b/gno.land/pkg/integration/testdata/genesis_params.txtar index d09ededf78a..87fd25d53d4 100644 --- a/gno.land/pkg/integration/testdata/genesis_params.txtar +++ b/gno.land/pkg/integration/testdata/genesis_params.txtar @@ -7,22 +7,21 @@ gnoland start # default initialization of "gnoland" provides the expected default values. # Verify the default chain domain parameter for Gno.land -gnokey query params/vm/gno.land/r/sys/params.vm.chain_domain.string +gnokey query params/vm:chain_domain.string stdout 'data: "gno.land"$' # Test custom parameters to confirm they return the expected values and types. -gnokey query params/vm/gno.land/r/sys/params.test.foo.string +gnokey query params/vm:gno.land/r/sys/params.test.foo.string stdout 'data: "bar"$' -gnokey query params/vm/gno.land/r/sys/params.test.foo.int64 +gnokey query params/vm:gno.land/r/sys/params.test.foo.int64 stdout 'data: "-1337"' -gnokey query params/vm/gno.land/r/sys/params.test.foo.uint64 +gnokey query params/vm:gno.land/r/sys/params.test.foo.uint64 stdout 'data: "42"' -gnokey query params/vm/gno.land/r/sys/params.test.foo.bool +gnokey query params/vm:gno.land/r/sys/params.test.foo.bool stdout 'data: true' # TODO: Consider adding a test case for a byte array parameter - diff --git a/gno.land/pkg/integration/testdata/gnoweb_airgapped.txtar b/gno.land/pkg/integration/testdata/gnoweb_airgapped.txtar index 838db121442..c4d0e6bcdf8 100644 --- a/gno.land/pkg/integration/testdata/gnoweb_airgapped.txtar +++ b/gno.land/pkg/integration/testdata/gnoweb_airgapped.txtar @@ -19,7 +19,7 @@ stdout ' "BaseAccount": {' stdout ' "address": "g1meuazsmy8ztaz2xpuyraqq4axy6s00ycl07zva",' stdout ' "coins": "[0-9]*ugnot",' # dynamic stdout ' "public_key": null,' -stdout ' "account_number": "57",' +stdout ' "account_number": "58",' stdout ' "sequence": "0"' stdout ' }' stdout '}' @@ -30,7 +30,7 @@ gnokey maketx call -pkgpath "gno.land/r/demo/echo" -func "Render" -gas-fee 10000 cp stdout call.tx # Sign -gnokey sign -tx-path $WORK/call.tx -chainid "tendermint_test" -account-number 57 -account-sequence 0 user1 +gnokey sign -tx-path $WORK/call.tx -chainid "tendermint_test" -account-number 58 -account-sequence 0 user1 cmpenv stdout sign.stdout.golden gnokey broadcast $WORK/call.tx diff --git a/gno.land/pkg/integration/testdata/params.txtar b/gno.land/pkg/integration/testdata/params.txtar index 30363aa6369..2b1f86ca6f8 100644 --- a/gno.land/pkg/integration/testdata/params.txtar +++ b/gno.land/pkg/integration/testdata/params.txtar @@ -3,58 +3,126 @@ gnoland start # query before adding the package -gnokey query params/vm/gno.land/r/sys/setter.foo.string +gnokey query params/gno.land/r/sys/params.foo.string stdout 'data: $' -gnokey query params/vm/gno.land/r/sys/setter.bar.bool +gnokey query params/gno.land/r/sys/params.bar.bool stdout 'data: $' -gnokey query params/vm/gno.land/r/sys/setter.baz.int64 +gnokey query params/gno.land/r/sys/params.baz.int64 stdout 'data: $' -gnokey maketx addpkg -pkgdir $WORK/setter -pkgpath gno.land/r/sys/setter -gas-fee 1000000ugnot -gas-wanted 100000000 -broadcast -chainid=tendermint_test test1 +# ---- 1 Test std.SetParamXXX when called from gno.land/r/sys/params + +gnokey maketx addpkg -pkgdir $WORK/params -pkgpath gno.land/r/sys/params -gas-fee 1000000ugnot -gas-wanted 100000000 -broadcast -chainid=tendermint_test test1 # query after adding the package, but before setting values -gnokey query params/vm/gno.land/r/sys/setter.foo.string +gnokey query params/vm:gno.land/r/sys/params.foo.string stdout 'data: $' -gnokey query params/vm/gno.land/r/sys/setter.bar.bool +gnokey query params/vm:gno.land/r/sys/params.bar.bool stdout 'data: $' -gnokey query params/vm/gno.land/r/sys/setter.baz.int64 +gnokey query params/vm:gno.land/r/sys/params.baz.int64 stdout 'data: $' -# set foo (string) -gnokey maketx call -pkgpath gno.land/r/sys/setter -func SetFoo -args foo1 -gas-fee 1000000ugnot -gas-wanted 10000000 -broadcast -chainid=tendermint_test test1 -gnokey query params/vm/gno.land/r/sys/setter.foo.string +## set foo (string) +gnokey maketx call -pkgpath gno.land/r/sys/params -func SetFoo -args foo1 -gas-fee 1000000ugnot -gas-wanted 10000000 -broadcast -chainid=tendermint_test test1 +gnokey query params/vm:gno.land/r/sys/params.foo.string stdout 'data: "foo1"' # override foo -gnokey maketx call -pkgpath gno.land/r/sys/setter -func SetFoo -args foo2 -gas-fee 1000000ugnot -gas-wanted 10000000 -broadcast -chainid=tendermint_test test1 -gnokey query params/vm/gno.land/r/sys/setter.foo.string +gnokey maketx call -pkgpath gno.land/r/sys/params -func SetFoo -args foo2 -gas-fee 1000000ugnot -gas-wanted 10000000 -broadcast -chainid=tendermint_test test1 +gnokey query params/vm:gno.land/r/sys/params.foo.string stdout 'data: "foo2"' # set bar (bool) -gnokey maketx call -pkgpath gno.land/r/sys/setter -func SetBar -args true -gas-fee 1000000ugnot -gas-wanted 10000000 -broadcast -chainid=tendermint_test test1 -gnokey query params/vm/gno.land/r/sys/setter.bar.bool +gnokey maketx call -pkgpath gno.land/r/sys/params -func SetBar -args true -gas-fee 1000000ugnot -gas-wanted 10000000 -broadcast -chainid=tendermint_test test1 +gnokey query params/vm:gno.land/r/sys/params.bar.bool stdout 'data: true' -# override bar -gnokey maketx call -pkgpath gno.land/r/sys/setter -func SetBar -args false -gas-fee 1000000ugnot -gas-wanted 10000000 -broadcast -chainid=tendermint_test test1 -gnokey query params/vm/gno.land/r/sys/setter.bar.bool +# override bar +gnokey maketx call -pkgpath gno.land/r/sys/params -func SetBar -args false -gas-fee 1000000ugnot -gas-wanted 10000000 -broadcast -chainid=tendermint_test test1 +gnokey query params/vm:gno.land/r/sys/params.bar.bool stdout 'data: false' -# set baz (bool) -gnokey maketx call -pkgpath gno.land/r/sys/setter -func SetBaz -args 1337 -gas-fee 1000000ugnot -gas-wanted 10000000 -broadcast -chainid=tendermint_test test1 -gnokey query params/vm/gno.land/r/sys/setter.baz.int64 +# set baz (int64) +gnokey maketx call -pkgpath gno.land/r/sys/params -func SetBaz -args 1337 -gas-fee 1000000ugnot -gas-wanted 10000000 -broadcast -chainid=tendermint_test test1 +gnokey query params/vm:gno.land/r/sys/params.baz.int64 stdout 'data: "1337"' # override baz -gnokey maketx call -pkgpath gno.land/r/sys/setter -func SetBaz -args 31337 -gas-fee 1000000ugnot -gas-wanted 10000000 -broadcast -chainid=tendermint_test test1 -gnokey query params/vm/gno.land/r/sys/setter.baz.int64 +gnokey maketx call -pkgpath gno.land/r/sys/params -func SetBaz -args -31337 -gas-fee 1000000ugnot -gas-wanted 10000000 -broadcast -chainid=tendermint_test test1 +gnokey query params/vm:gno.land/r/sys/params.baz.int64 +stdout 'data: "-31337"' + +# set uint64 +gnokey maketx call -pkgpath gno.land/r/sys/params -func SetBaz -args 31337 -gas-fee 1000000ugnot -gas-wanted 10000000 -broadcast -chainid=tendermint_test test1 +gnokey query params/vm:gno.land/r/sys/params.baz.int64 stdout 'data: "31337"' --- setter/setter.gno -- -package setter +# set uint64 +gnokey maketx call -pkgpath gno.land/r/sys/params -func SetBaz -args -31337 -gas-fee 1000000ugnot -gas-wanted 10000000 -broadcast -chainid=tendermint_test test1 +gnokey query params/vm:gno.land/r/sys/params.baz.int64 +stdout 'data: "-31337"' + + +# It is impossible to call std.SetParamXXX with a pararmeter key in the : format (e.g. "bank:Transfer.string") because it is an invalid key. +! gnokey maketx call -pkgpath gno.land/r/sys/params -func SetLockTransfer -args ugnot -gas-fee 1000000ugnot -gas-wanted 10000000 -broadcast -chainid=tendermint_test test1 +stderr 'invalid parameter key: bank:lockTransfer.string' + + +# ---- 2 Test std.SetParamXXX when called outside of gno.land/r/sys/params + +gnokey maketx addpkg -pkgdir $WORK/params -pkgpath gno.land/r/myrealm -gas-fee 1000000ugnot -gas-wanted 100000000 -broadcast -chainid=tendermint_test test1 + +## set foo (string) +gnokey maketx call -pkgpath gno.land/r/myrealm -func SetFoo -args foo1 -gas-fee 1000000ugnot -gas-wanted 10000000 -broadcast -chainid=tendermint_test test1 +gnokey query params/vm:gno.land/r/myrealm.foo.string +stdout 'data: "foo1"' + +# override foo +gnokey maketx call -pkgpath gno.land/r/myrealm -func SetFoo -args foo2 -gas-fee 1000000ugnot -gas-wanted 10000000 -broadcast -chainid=tendermint_test test1 +gnokey query params/vm:gno.land/r/myrealm.foo.string +stdout 'data: "foo2"' + + +# set bar (bool) +gnokey maketx call -pkgpath gno.land/r/myrealm -func SetBar -args true -gas-fee 1000000ugnot -gas-wanted 10000000 -broadcast -chainid=tendermint_test test1 +gnokey query params/vm:gno.land/r/myrealm.bar.bool +stdout 'data: true' + +# override bar +gnokey maketx call -pkgpath gno.land/r/myrealm -func SetBar -args false -gas-fee 1000000ugnot -gas-wanted 10000000 -broadcast -chainid=tendermint_test test1 +gnokey query params/vm:gno.land/r/myrealm.bar.bool +stdout 'data: false' + + +# set baz (int64) +gnokey maketx call -pkgpath gno.land/r/myrealm -func SetBaz -args 1337 -gas-fee 1000000ugnot -gas-wanted 10000000 -broadcast -chainid=tendermint_test test1 +gnokey query params/vm:gno.land/r/myrealm.baz.int64 +stdout 'data: "1337"' + +# override baz +gnokey maketx call -pkgpath gno.land/r/myrealm -func SetBaz -args -31337 -gas-fee 1000000ugnot -gas-wanted 10000000 -broadcast -chainid=tendermint_test test1 +gnokey query params/vm:gno.land/r/myrealm.baz.int64 +stdout 'data: "-31337"' + +# It is impossible to call std.SetParamXXX with a parameter key in the : format (e.g. "bank:Transfer.string") because it is an invalid key. +! gnokey maketx call -pkgpath gno.land/r/myrealm -func SetLockTransfer -args ugnot -gas-fee 1000000ugnot -gas-wanted 10000000 -broadcast -chainid=tendermint_test test1 +stderr 'Data: invalid parameter key: bank:lockTransfer.string' + +# . is invalid (e.g., "bank.Transfer.string") +! gnokey maketx call -pkgpath gno.land/r/myrealm -func SetInvalidString -args ugnot -gas-fee 1000000ugnot -gas-wanted 10000000 -broadcast -chainid=tendermint_test test1 +stderr 'Data: invalid parameter key: bank.lockTransfer.string' + +# . (e.g., "bank_Transfer.string") is a valid name. +gnokey maketx call -pkgpath gno.land/r/myrealm -func SetValidString -args ugnot -gas-fee 1000000ugnot -gas-wanted 10000000 -broadcast -chainid=tendermint_test test1 +gnokey query params/vm:gno.land/r/myrealm.bank_lockTransfer.string +stdout 'data: "ugnot"' + + +-- params/setter.gno -- +package params import ( "std" @@ -63,3 +131,9 @@ import ( func SetFoo(newFoo string) { std.SetParamString("foo.string", newFoo) } func SetBar(newBar bool) { std.SetParamBool("bar.bool", newBar) } func SetBaz(newBaz int64) { std.SetParamInt64("baz.int64", newBaz) } +func SetUint64(newBaz uint64) { std.SetParamUint64("baz.uint64", newBaz) } +func SetBytes() { std.SetParamBytes("baz.bytes", []byte{255,255}) } + +func SetLockTransfer(denom string) { std.SetParamString("bank:lockTransfer.string", denom) } +func SetInvalidString(denom string) { std.SetParamString("bank.lockTransfer.string", denom) } +func SetValidString(denom string) { std.SetParamString("bank_lockTransfer.string", denom) } diff --git a/gno.land/pkg/integration/testdata/params_module.txtar b/gno.land/pkg/integration/testdata/params_module.txtar new file mode 100644 index 00000000000..6dfe1daf937 --- /dev/null +++ b/gno.land/pkg/integration/testdata/params_module.txtar @@ -0,0 +1,52 @@ +# tests for + +gnoland start + +# ---- 1 Test sys/params.SetPrefixedXXX when called from gno.land/r/sys/params + +gnokey maketx addpkg -pkgdir $WORK/prefixed -pkgpath gno.land/r/sys/params -gas-fee 1000000ugnot -gas-wanted 100000000 -broadcast -chainid=tendermint_test test1 + +## before set lock transfer +gnokey query params/bank:p +stdout 'data: ' + +## lock transfer +gnokey maketx call -pkgpath gno.land/r/sys/params -func SetLockTransfer -args "ugnot" -gas-fee 1000000ugnot -gas-wanted 10000000 -broadcast -chainid=tendermint_test test1 + +## query bank module +gnokey query params/bank:p +stdout 'data: {"restricted_denoms":\["ugnot"\]}' + +## query vm module +gnokey query params/vm:p +stdout 'data: {"sysusers_pkgpath":"gno.land/r/sys/users","chain_domain":"gno.land"}' + +## query auth module +gnokey query params/auth:p +stdout 'data: {"max_memo_bytes":"65536","tx_sig_limit":"7","tx_size_cost_per_byte":"10","sig_verify_cost_ed25519":"590","sig_verify_cost_secp256k1":"1000","gas_price_change_compressor":"10","target_gas_ratio":"70","initial_gasprice":{"gas":"0","price":""},"unrestricted_addrs":\["g1jg8mtutu9khhfwc4nxmuhcpftf0pajdhfvsqf5"\]}' + + +-- prefixed/setter.gno -- +package params + +import ( + "sys/params" +) + +// This should succeed if it is called from gno.land/r/sys/params +func SetLockTransfer(denom string) { params.SetPrefixedString("bank","lockTransfer.string", denom) } + +// SetPrefixedXXX must be called from gno.land/r/sys/params; otherwise it panics +// This should fail because the parameter does not exist +func SetPrefixBank(newFoo string) { params.SetPrefixedString("bank","foo.string", newFoo) } + + +// This should fail because the key "bank:lockTransfer.string" is not valid +func SetInvalidKey(denom string) { params.SetPrefixedString("bank", "bank:lockTransfer.string", "ugnot") } + + +func SetPrefixedString(s string) { params.SetPrefixedString("bank","foo.string", s) } +func SetPrefixedBool(b bool) { params.SetPrefixedBool("bank","bar.bool", b) } +func SetPrefixedInt64(i int64) { params.SetPrefixedInt64("bank","baz.int64", i) } +func SetPrefixedUint64(u uint64) { params.SetPrefixedUint64("bank","baz.uint64", u) } +func SetPrefixedBytes() { params.SetPrefixedBytes("bank","baz.bytes", []byte{255,255}) } diff --git a/gno.land/pkg/integration/testdata/params_prefixed.txtar b/gno.land/pkg/integration/testdata/params_prefixed.txtar new file mode 100644 index 00000000000..39135726fcd --- /dev/null +++ b/gno.land/pkg/integration/testdata/params_prefixed.txtar @@ -0,0 +1,96 @@ +# test set prefixed parameter keys + +gnoland start + +# ---- 1 Test sys/params.SetPrefixedXXX when called from gno.land/r/sys/params + +gnokey maketx addpkg -pkgdir $WORK/prefixed -pkgpath gno.land/r/sys/params -gas-fee 1000000ugnot -gas-wanted 100000000 -broadcast -chainid=tendermint_test test1 + +## lock transfer +gnokey maketx call -pkgpath gno.land/r/sys/params -func SetLockTransfer -args "ugnot" -gas-fee 1000000ugnot -gas-wanted 10000000 -broadcast -chainid=tendermint_test test1 +gnokey query params/bank:p +stdout 'data: {"restricted_denoms":\["ugnot"\]}' + +# unlock transfer +gnokey maketx call -pkgpath gno.land/r/sys/params -func SetLockTransfer -args "" -gas-fee 1000000ugnot -gas-wanted 10000000 -broadcast -chainid=tendermint_test test1 +gnokey query params/bank:p +stdout 'data: {"restricted_denoms":\[\]}' + +# set non-exists module params key +! gnokey maketx call -pkgpath gno.land/r/sys/params -func SetPrefixBank -args "foo" -gas-fee 1000000ugnot -gas-wanted 10000000 -broadcast -chainid=tendermint_test test1 +stderr 'invalid bank parameter key: foo.string' + +gnokey query params/bank:foo.string +stdout 'data: ' + +# set invalid key +! gnokey maketx call -pkgpath gno.land/r/sys/params -func SetInvalidKey -args "ugnot" -gas-fee 1000000ugnot -gas-wanted 10000000 -broadcast -chainid=tendermint_test test1 +stderr 'invalid parameter key: bank/lockTransfer.string' + +gnokey query params/bank:lockTransfer.string +stdout 'data: ' + +gnokey query params/bank:lockTransfer.string +stdout 'data: ' + + +# ---- 2 Test sys/params.SetPrefixedXXX when called outside of gno.land/r/sys/params +gnokey maketx addpkg -pkgdir $WORK/prefixed -pkgpath gno.land/r/myrealm -gas-fee 1000000ugnot -gas-wanted 100000000 -broadcast -chainid=tendermint_test test1 + +## can not call SetPrefixXXX out side of gno.land/r/params +! gnokey maketx call -pkgpath gno.land/r/myrealm -func SetPrefixedString -args "foo" -gas-fee 1000000ugnot -gas-wanted 10000000 -broadcast -chainid=tendermint_test test1 +stderr 'Data: prefixed parameter "foo.string" with keeper prefix "bank" must be set in "gno.land/r/sys/params"' + +gnokey query params/bank:foo.string +stdout 'data: ' + + +! gnokey maketx call -pkgpath gno.land/r/myrealm -func SetPrefixedBool -args true -gas-fee 1000000ugnot -gas-wanted 10000000 -broadcast -chainid=tendermint_test test1 +stderr 'Data: prefixed parameter "bar.bool" with keeper prefix "bank" must be set in "gno.land/r/sys/params"' + +gnokey query params/bank:bar.bool +stdout 'data: ' + +! gnokey maketx call -pkgpath gno.land/r/myrealm -func SetPrefixedInt64 -args -100 -gas-fee 1000000ugnot -gas-wanted 10000000 -broadcast -chainid=tendermint_test test1 +stderr 'Data: prefixed parameter "baz.int64" with keeper prefix "bank" must be set in "gno.land/r/sys/params"' + +gnokey query params/bank:baz.int64 +stdout 'data: ' + +! gnokey maketx call -pkgpath gno.land/r/myrealm -func SetPrefixedUint64 -args 100 -gas-fee 1000000ugnot -gas-wanted 10000000 -broadcast -chainid=tendermint_test test1 +stderr 'Data: prefixed parameter "baz.uint64" with keeper prefix "bank" must be set in "gno.land/r/sys/params"' + +gnokey query params/bank:baz.uint64 +stdout 'data: ' + +! gnokey maketx call -pkgpath gno.land/r/myrealm -func SetPrefixedBytes -gas-fee 1000000ugnot -gas-wanted 10000000 -broadcast -chainid=tendermint_test test1 +stderr 'Data: prefixed parameter "baz.bytes" with keeper prefix "bank" must be set in "gno.land/r/sys/params"' + +gnokey query params/bank:baz.bytes +stdout 'data: ' + + +-- prefixed/setter.gno -- +package params + +import ( + "sys/params" +) + +// This should succeed if it is called from gno.land/r/sys/params +func SetLockTransfer(denom string) { params.SetPrefixedString("bank","lockTransfer.string", denom) } + +// SetPrefixedXXX must be called from gno.land/r/sys/params; otherwise it panics +// This should fail because the parameter does not exist +func SetPrefixBank(newFoo string) { params.SetPrefixedString("bank","foo.string", newFoo) } + + +// This should fail because the key "bank:lockTransfer.string" is not valid +func SetInvalidKey(denom string) { params.SetPrefixedString("bank", "bank/lockTransfer.string", "ugnot") } + + +func SetPrefixedString(s string) { params.SetPrefixedString("bank","foo.string", s) } +func SetPrefixedBool(b bool) { params.SetPrefixedBool("bank","bar.bool", b) } +func SetPrefixedInt64(i int64) { params.SetPrefixedInt64("bank","baz.int64", i) } +func SetPrefixedUint64(u uint64) { params.SetPrefixedUint64("bank","baz.uint64", u) } +func SetPrefixedBytes() { params.SetPrefixedBytes("bank","baz.bytes", []byte{255,255}) } diff --git a/gno.land/pkg/integration/testdata/restart_missing_type.txtar b/gno.land/pkg/integration/testdata/restart_missing_type.txtar index cc8ed702734..ffb47a4ac2d 100644 --- a/gno.land/pkg/integration/testdata/restart_missing_type.txtar +++ b/gno.land/pkg/integration/testdata/restart_missing_type.txtar @@ -9,15 +9,15 @@ stdout 'g1lmgyf29g6zqgpln5pq05zzt7qkz2wga7xgagv4' loadpkg gno.land/p/demo/avl gnoland start -gnokey sign -tx-path $WORK/tx1.tx -chainid tendermint_test -account-sequence 0 -account-number 57 user1 +gnokey sign -tx-path $WORK/tx1.tx -chainid tendermint_test -account-sequence 0 -account-number 58 user1 ! gnokey broadcast $WORK/tx1.tx stderr 'out of gas' -gnokey sign -tx-path $WORK/tx2.tx -chainid tendermint_test -account-sequence 1 -account-number 57 user1 +gnokey sign -tx-path $WORK/tx2.tx -chainid tendermint_test -account-sequence 1 -account-number 58 user1 gnokey broadcast $WORK/tx2.tx stdout 'OK!' -gnokey sign -tx-path $WORK/tx3.tx -chainid tendermint_test -account-sequence 2 -account-number 57 user1 +gnokey sign -tx-path $WORK/tx3.tx -chainid tendermint_test -account-sequence 2 -account-number 58 user1 gnokey broadcast $WORK/tx3.tx stdout 'OK!' diff --git a/gno.land/pkg/integration/testdata/simulate_gas.txtar b/gno.land/pkg/integration/testdata/simulate_gas.txtar index 0dcb9ba424b..4fe8a690adf 100644 --- a/gno.land/pkg/integration/testdata/simulate_gas.txtar +++ b/gno.land/pkg/integration/testdata/simulate_gas.txtar @@ -6,11 +6,11 @@ gnoland start # simulate only gnokey maketx call -pkgpath gno.land/r/simulate -func Hello -gas-fee 1000000ugnot -gas-wanted 2000000 -broadcast -chainid=tendermint_test -simulate only test1 -stdout 'GAS USED: 99371' +stdout 'GAS USED: 99587' # simulate skip gnokey maketx call -pkgpath gno.land/r/simulate -func Hello -gas-fee 1000000ugnot -gas-wanted 2000000 -broadcast -chainid=tendermint_test -simulate skip test1 -stdout 'GAS USED: 99371' # same as simulate only +stdout 'GAS USED: 99587' # same as simulate only -- package/package.gno -- diff --git a/gno.land/pkg/integration/testdata/transfer_lock.txtar b/gno.land/pkg/integration/testdata/transfer_lock.txtar new file mode 100644 index 00000000000..146965290ec --- /dev/null +++ b/gno.land/pkg/integration/testdata/transfer_lock.txtar @@ -0,0 +1,55 @@ +## It tests locking token transfers while allowing the payment of gas fees. + +## locking transfer applies to regular accounts +adduser regular1 + + +loadpkg gno.land/r/demo/wugnot +loadpkg gno.land/r/demo/echo + +## start a new node. +## The -lock-transfer flag is intended for integration testing purposes +## and is not a valid application flag for gnoland. + +gnoland start -lock-transfer + +## test1 is the DefaultAccount in the integration test. To ensure that the unrestricted account can send tokens even when token transfers are locked, +## we included it in the unrestricted account list in the genesis state. By default, the unrestricted account list is empty. +gnokey maketx send -send "9999999ugnot" -to $regular1_user_addr -gas-fee 1ugnot -gas-wanted 10000000 -broadcast -chainid=tendermint_test test1 + +stdout 'OK!' + +## Restricted simple token transfer +! gnokey maketx send -send "9999999ugnot" -to g1jg8mtutu9khhfwc4nxmuhcpftf0pajdhfvsqf5 -gas-fee 1ugnot -gas-wanted 10000000 -broadcast -chainid=tendermint_test regular1 +stderr 'restricted token transfer error' + +## Restricted token transfer by calling a realm deposit function. +! gnokey maketx call -pkgpath gno.land/r/demo/wugnot -func Deposit -gas-fee 1000000ugnot -send "10000ugnot" -gas-wanted 2000000 -broadcast -chainid=tendermint_test regular1 +stderr 'restricted token transfer error' + + +## Restricted token transfer with depositing to a realm package while adding a package. +! gnokey maketx addpkg -pkgdir $WORK -pkgpath gno.land/r/bank -deposit "1000ugnot" -gas-fee 1000000ugnot -gas-wanted 10000000 -broadcast -chainid=tendermint_test regular1 +stderr 'restricted token transfer error' + +## paying gas fees to add a package is acceptable. +gnokey maketx addpkg -pkgdir $WORK -pkgpath gno.land/r/bank -gas-fee 1000000ugnot -gas-wanted 12500000 -broadcast -chainid=tendermint_test regular1 +stdout 'OK!' + +## paying gas fees to call a realm function is acceptable. +gnokey maketx call -pkgpath gno.land/r/demo/echo -func Render -args "Hello!" -gas-fee 1000000ugnot -gas-wanted 12500000 -broadcast -chainid=tendermint_test regular1 +stdout 'Hello!' + +-- bank.gno -- +package bank +import ( +"std" +) +func Withdraw(denom string, amt int64) string{ + caller := std.OriginCaller() + coin := std.Coins{{denom, amt}} + banker := std.NewBanker(std.BankerTypeOriginSend) + pkgaddr := std.OriginPkgAddress() + banker.SendCoins(pkgaddr, caller, coin) + return "Withdrawn!" +} diff --git a/gno.land/pkg/integration/testdata/transfer_unlock.txtar b/gno.land/pkg/integration/testdata/transfer_unlock.txtar new file mode 100644 index 00000000000..6a5ac729722 --- /dev/null +++ b/gno.land/pkg/integration/testdata/transfer_unlock.txtar @@ -0,0 +1,65 @@ +## It tests unlocking token transfers through GovDAO voting +loadpkg gno.land/r/sys/params +loadpkg gno.land/r/gov/dao/v2 + +patchpkg "g1wymu47drhr0kuq2098m792lytgtj2nyx77yrsm" "g1jg8mtutu9khhfwc4nxmuhcpftf0pajdhfvsqf5" + +adduser regular1 + +## The -lock-transfer flag is not a Gnoland service flag; it is a flag for the txtar setting. +gnoland start -lock-transfer + +## test1 is the DefaultAccount in the integration test. To ensure that the unrestricted account can send tokens even when token transfers are locked, +## we included it in the unrestricted account list in the genesis state. By default, the unrestricted account list is empty. +gnokey maketx send -send "9999999ugnot" -to $regular1_user_addr -gas-fee 1ugnot -gas-wanted 10000000 -broadcast -chainid=tendermint_test test1 + +stdout 'OK!' + +## Restricted simple token transfer for a regular account +! gnokey maketx send -send "100ugnot" -to g1jg8mtutu9khhfwc4nxmuhcpftf0pajdhfvsqf5 -gas-fee 1ugnot -gas-wanted 10000000 -broadcast -chainid=tendermint_test regular1 + +stderr 'restricted token transfer error' + +## Submit a proposal to unlock the transfer. When token transfer is locked, only the predefined unrestricted account test1 in the genesis state can +## pay the fee and submit a proposal to unlock the transfer. +gnokey maketx call -pkgpath gno.land/r/sys/params -func ProposeUnlockTransfer -send 250000000ugnot -gas-fee 1ugnot -gas-wanted 9500000 -broadcast -chainid=tendermint_test test1 + +stdout '(0 uint64)' + + +## Vote unlock proposal with unrestricted account test1 +gnokey maketx run -gas-fee 1ugnot -gas-wanted 95000000 -broadcast -chainid=tendermint_test test1 $WORK/run/vote_proposal.gno + +stdout 'OK!' + +## Execute unlock proposal with unrestricted account test1 +gnokey maketx run -gas-fee 1ugnot -gas-wanted 95000000 -broadcast -chainid=tendermint_test test1 $WORK/run/exec_proposal.gno + +stdout 'OK!' + +## Restricted transfer is unlocked, allowing simple token transfers for regular accounts. +gnokey maketx send -send "100ugnot" -to g1jg8mtutu9khhfwc4nxmuhcpftf0pajdhfvsqf5 -gas-fee 1ugnot -gas-wanted 10000000 -broadcast -chainid=tendermint_test regular1 + +stdout 'OK!' + +-- run/vote_proposal.gno -- +package main + +import ( + govdao "gno.land/r/gov/dao/v2" +) + +func main() { + govdao.GovDAO.VoteOnProposal(0,"YES") +} + +-- run/exec_proposal.gno -- +package main + +import ( + govdao "gno.land/r/gov/dao/v2" +) + +func main() { + govdao.GovDAO.ExecuteProposal(0) +} diff --git a/gno.land/pkg/integration/testscript_gnoland.go b/gno.land/pkg/integration/testscript_gnoland.go index 1531b83dfef..6c048c60318 100644 --- a/gno.land/pkg/integration/testscript_gnoland.go +++ b/gno.land/pkg/integration/testscript_gnoland.go @@ -183,19 +183,17 @@ func SetupGnolandTestscript(t *testing.T, p *testscript.Params) error { env.Setenv("SID", sid) } - balanceFile := LoadDefaultGenesisBalanceFile(t, gnoRootDir) - genesisParamFile := LoadDefaultGenesisParamFile(t, gnoRootDir) - // Track new user balances added via the `adduser` // command and packages added with the `loadpkg` command. // This genesis will be use when node is started. - genesis := &gnoland.GnoGenesisState{ - Balances: balanceFile, - Params: genesisParamFile, - Txs: []gnoland.TxWithMetadata{}, - } - env.Values[envKeyGenesis] = genesis + genesis := gnoland.DefaultGenState() + genesis.Balances = LoadDefaultGenesisBalanceFile(t, gnoRootDir) + genesis.Params = LoadDefaultGenesisParamFile(t, gnoRootDir) + genesis.Auth.Params.InitialGasPrice = std.GasPrice{Gas: 0, Price: std.Coin{Amount: 0, Denom: "ugnot"}} + genesis.Txs = []gnoland.TxWithMetadata{} + + env.Values[envKeyGenesis] = &genesis env.Values[envKeyPkgsLoader] = NewPkgsLoader() env.Setenv("GNOROOT", gnoRootDir) @@ -273,6 +271,7 @@ func gnolandCmd(t *testing.T, nodesManager *NodesManager, gnoRootDir string) fun // directly or use the config command for this. fs := flag.NewFlagSet("start", flag.ContinueOnError) nonVal := fs.Bool("non-validator", false, "set up node as a non-validator") + lockTransfer := fs.Bool("lock-transfer", false, "lock transfer ugnot") if err := fs.Parse(cmdargs); err != nil { ts.Fatalf("unable to parse `gnoland start` flags: %s", err) } @@ -285,10 +284,16 @@ func gnolandCmd(t *testing.T, nodesManager *NodesManager, gnoRootDir string) fun } cfg := TestingMinimalNodeConfig(gnoRootDir) - genesis := ts.Value(envKeyGenesis).(*gnoland.GnoGenesisState) - genesis.Txs = append(pkgsTxs, genesis.Txs...) + tsGenesis := ts.Value(envKeyGenesis).(*gnoland.GnoGenesisState) + genesis := cfg.Genesis.AppState.(gnoland.GnoGenesisState) + genesis.Txs = append(genesis.Txs, append(pkgsTxs, tsGenesis.Txs...)...) + genesis.Balances = append(genesis.Balances, tsGenesis.Balances...) + genesis.Params = append(genesis.Params, tsGenesis.Params...) + if *lockTransfer { + genesis.Bank.Params.RestrictedDenoms = []string{"ugnot"} + } - cfg.Genesis.AppState = *genesis + cfg.Genesis.AppState = genesis if *nonVal { pv := gnoland.NewMockedPrivValidator() cfg.Genesis.Validators = []bft.GenesisValidator{ diff --git a/gno.land/pkg/sdk/vm/builtins.go b/gno.land/pkg/sdk/vm/builtins.go index 161e459873d..ee663d0f7e1 100644 --- a/gno.land/pkg/sdk/vm/builtins.go +++ b/gno.land/pkg/sdk/vm/builtins.go @@ -1,8 +1,13 @@ package vm import ( + "fmt" + + gno "github.com/gnolang/gno/gnovm/pkg/gnolang" + gstd "github.com/gnolang/gno/gnovm/stdlibs/std" "github.com/gnolang/gno/tm2/pkg/crypto" "github.com/gnolang/gno/tm2/pkg/sdk" + "github.com/gnolang/gno/tm2/pkg/sdk/params" "github.com/gnolang/gno/tm2/pkg/std" ) @@ -60,21 +65,80 @@ func (bnk *SDKBanker) RemoveCoin(b32addr crypto.Bech32Address, denom string, amo // SDKParams type SDKParams struct { - vmk *VMKeeper + pmk *params.ParamsKeeper ctx sdk.Context } -func NewSDKParams(vmk *VMKeeper, ctx sdk.Context) *SDKParams { +// These are the native function implementations bound to standard libraries in Gno. +// All methods of this struct are not intended to be called from outside vm/stdlibs/std. +// +// The key has the format ..: +// realm: A realm path indicating where Set methods are called from. +// paramname: The parameter key. If it contains a prefix that matches the module's paramkey +// prefix (which by default is the module name), it indicates an attempt to set the module's +// parameters for the chain. Otherwise, it is treated as an arbitrary parameter. +// type: The primitive type of the parameter value. + +func NewSDKParams(pmk *params.ParamsKeeper, ctx sdk.Context) *SDKParams { return &SDKParams{ - vmk: vmk, + pmk: pmk, ctx: ctx, } } -func (prm *SDKParams) SetString(key, value string) { prm.vmk.prmk.SetString(prm.ctx, key, value) } -func (prm *SDKParams) SetBool(key string, value bool) { prm.vmk.prmk.SetBool(prm.ctx, key, value) } -func (prm *SDKParams) SetInt64(key string, value int64) { prm.vmk.prmk.SetInt64(prm.ctx, key, value) } -func (prm *SDKParams) SetUint64(key string, value uint64) { - prm.vmk.prmk.SetUint64(prm.ctx, key, value) +func (prm *SDKParams) SetString(key gstd.ParamKey, value string) { + prm.assertRealmAccess(key) + prm.willSetKeeperParams(prm.ctx, key, value) + prm.pmk.SetString(prm.ctx, key.String(), value) +} + +// Set a boolean parameter in the format of realmPath.parameter.bool +func (prm *SDKParams) SetBool(key gstd.ParamKey, value bool) { + prm.assertRealmAccess(key) + prm.willSetKeeperParams(prm.ctx, key, value) + prm.pmk.SetBool(prm.ctx, key.String(), value) +} + +func (prm *SDKParams) SetInt64(key gstd.ParamKey, value int64) { + prm.assertRealmAccess(key) + prm.willSetKeeperParams(prm.ctx, key, value) + prm.pmk.SetInt64(prm.ctx, key.String(), value) +} + +func (prm *SDKParams) SetUint64(key gstd.ParamKey, value uint64) { + prm.assertRealmAccess(key) + prm.willSetKeeperParams(prm.ctx, key, value) + prm.pmk.SetUint64(prm.ctx, key.String(), value) +} + +func (prm *SDKParams) SetBytes(key gstd.ParamKey, value []byte) { + prm.assertRealmAccess(key) + prm.willSetKeeperParams(prm.ctx, key, value) + prm.pmk.SetBytes(prm.ctx, key.String(), value) +} + +// ParamKey's prefix must match the keeper's paramKeyPrefix; otherwise it will panic and revert the transaction. +func (prm *SDKParams) willSetKeeperParams(ctx sdk.Context, key gstd.ParamKey, value interface{}) { + kp := key.Prefix + // ParamfulKeeper can be accessed from SysParamsRealmPath only + if key.Realm != SysParamsRealmPath || kp == "" { + return + } + + if !prm.pmk.IsRegistered(kp) { + panic(fmt.Sprintf("keeper key <%s> does not exist", kp)) + } + kpr := prm.pmk.GetRegisteredKeeper(kp) + if kpr != nil { + kpr.WillSetParam(prm.ctx, key.Key, value) + } +} + +func (prm *SDKParams) assertRealmAccess(key gstd.ParamKey) { + if gno.IsRealmPath(key.Realm) == false { + panic(fmt.Sprintf("parameters must be set in a valid realm")) + } + if key.Realm != SysParamsRealmPath && key.Prefix != "" { + panic(fmt.Sprintf("prefixed parameter %q with keeper prefix %q must be set in %q", key.Key, key.Prefix, SysParamsRealmPath)) + } } -func (prm *SDKParams) SetBytes(key string, value []byte) { prm.vmk.prmk.SetBytes(prm.ctx, key, value) } diff --git a/gno.land/pkg/sdk/vm/builtins_test.go b/gno.land/pkg/sdk/vm/builtins_test.go new file mode 100644 index 00000000000..5153a936d80 --- /dev/null +++ b/gno.land/pkg/sdk/vm/builtins_test.go @@ -0,0 +1,175 @@ +package vm + +import ( + "testing" + + gstd "github.com/gnolang/gno/gnovm/stdlibs/std" + "github.com/stretchr/testify/require" +) + +func TestParamsRestrictedRealm(t *testing.T) { + env := setupTestEnv() + params := NewSDKParams(&env.vmk.prmk, env.ctx) + + testCases := []struct { + name string + setFunc func() + expectedMsg string + }{ + { + name: "SetString should panic", + setFunc: func() { + pk := gstd.ParamKey{ + Realm: "gno.land/p/foo", + Prefix: "bank", + Key: "name", + Type: "string", + } + params.SetString(pk, "foo") + }, + expectedMsg: "parameters must be set in a valid realm", + }, + { + name: "SetBool should panic", + setFunc: func() { + pk := gstd.ParamKey{ + Realm: "gno.land/p/foo", + Prefix: "bank", + Key: "isFoo", + Type: "bool", + } + params.SetBool(pk, true) + }, + expectedMsg: "parameters must be set in a valid realm", + }, + { + name: "SetInt64 should panic", + setFunc: func() { + pk := gstd.ParamKey{ + Realm: "gno.land/p/foo", + Prefix: "bank", + Key: "number", + Type: "int64", + } + params.SetInt64(pk, -100) + }, + expectedMsg: "parameters must be set in a valid realm", + }, + { + name: "SetUint64 should panic", + setFunc: func() { + pk := gstd.ParamKey{ + Realm: "gno.land/p/foo", + Prefix: "bank", + Key: "number", + Type: "uint64", + } + params.SetUint64(pk, 100) + }, + expectedMsg: "parameters must be set in a valid realm", + }, + { + name: "SetBytes should panic", + setFunc: func() { + pk := gstd.ParamKey{ + Realm: "gno.land/p/foo", + Prefix: "bank", + Key: "name", + Type: "bytes", + } + + params.SetBytes(pk, []byte("foo")) + }, + expectedMsg: "parameters must be set in a valid realm", + }, + } + + for _, tc := range testCases { + t.Run(tc.name, func(t *testing.T) { + require.PanicsWithValue(t, tc.expectedMsg, tc.setFunc, "The panic message did not match the expected value") + }) + } +} + +func TestParamsKeeper(t *testing.T) { + env := setupTestEnv() + params := NewSDKParams(&env.vmk.prmk, env.ctx) + + testCases := []struct { + name string + setFunc func() + expectedMsg string + }{ + { + name: "SetString should panic", + setFunc: func() { + pk := gstd.ParamKey{ + Realm: "gno.land/r/sys/params", + Prefix: "foo", + Key: "name", + Type: "string", + } + params.SetString(pk, "foo") + }, + expectedMsg: `keeper key does not exist`, + }, + { + name: "SetBool should panic", + setFunc: func() { + pk := gstd.ParamKey{ + Realm: "gno.land/r/sys/params", + Prefix: "foo", + Key: "isFoo", + Type: "bool", + } + params.SetBool(pk, true) + }, + expectedMsg: `keeper key does not exist`, + }, + { + name: "SetInt64 should panic", + setFunc: func() { + pk := gstd.ParamKey{ + Realm: "gno.land/r/sys/params", + Prefix: "foo", + Key: "number", + Type: "int64", + } + params.SetInt64(pk, -100) + }, + expectedMsg: `keeper key does not exist`, + }, + { + name: "SetUint64 should panic", + setFunc: func() { + pk := gstd.ParamKey{ + Realm: "gno.land/r/sys/params", + Prefix: "foo", + Key: "number", + Type: "uint64", + } + params.SetUint64(pk, 100) + }, + expectedMsg: `keeper key does not exist`, + }, + { + name: "SetBytes should panic", + setFunc: func() { + pk := gstd.ParamKey{ + Realm: "gno.land/r/sys/params", + Prefix: "foo", + Key: "name", + Type: "bytes", + } + params.SetBytes(pk, []byte("foo")) + }, + expectedMsg: `keeper key does not exist`, + }, + } + + for _, tc := range testCases { + t.Run(tc.name, func(t *testing.T) { + require.PanicsWithValue(t, tc.expectedMsg, tc.setFunc, "The panic message did not match the expected value") + }) + } +} diff --git a/gno.land/pkg/sdk/vm/common_test.go b/gno.land/pkg/sdk/vm/common_test.go index 10402f31f64..ffa53be55e9 100644 --- a/gno.land/pkg/sdk/vm/common_test.go +++ b/gno.land/pkg/sdk/vm/common_test.go @@ -47,12 +47,16 @@ func _setupTestEnv(cacheStdlibs bool) testEnv { ms.LoadLatestVersion() ctx := sdk.NewContext(sdk.RunTxModeDeliver, ms, &bft.Header{ChainID: "test-chain-id"}, log.NewNoopLogger()) - prmk := paramsm.NewParamsKeeper(iavlCapKey, "params") - acck := authm.NewAccountKeeper(iavlCapKey, prmk, std.ProtoBaseAccount) - bank := bankm.NewBankKeeper(acck) + prmk := paramsm.NewParamsKeeper(iavlCapKey) + acck := authm.NewAccountKeeper(iavlCapKey, prmk, std.ProtoBaseAccount) + bank := bankm.NewBankKeeper(acck, prmk) vmk := NewVMKeeper(baseCapKey, iavlCapKey, acck, bank, prmk) + prmk.Register(acck.GetParamfulKey(), acck) + prmk.Register(bank.GetParamfulKey(), bank) + prmk.Register(vmk.GetParamfulKey(), vmk) + mcw := ms.MultiCacheWrap() vmk.Initialize(log.NewNoopLogger(), mcw) stdlibCtx := vmk.MakeGnoTransactionStore(ctx.WithMultiStore(mcw)) diff --git a/gno.land/pkg/sdk/vm/consts.go b/gno.land/pkg/sdk/vm/consts.go index 292f34a9d20..66fb188a22d 100644 --- a/gno.land/pkg/sdk/vm/consts.go +++ b/gno.land/pkg/sdk/vm/consts.go @@ -1,6 +1,7 @@ package vm const ( - ModuleName = "vm" - RouterKey = ModuleName + ModuleName = "vm" + RouterKey = ModuleName + SysParamsRealmPath = "gno.land/r/sys/params" ) diff --git a/gno.land/pkg/sdk/vm/genesis.go b/gno.land/pkg/sdk/vm/genesis.go new file mode 100644 index 00000000000..90c91d6fee9 --- /dev/null +++ b/gno.land/pkg/sdk/vm/genesis.go @@ -0,0 +1,48 @@ +package vm + +import ( + "fmt" + + "github.com/gnolang/gno/tm2/pkg/amino" + "github.com/gnolang/gno/tm2/pkg/sdk" +) + +// GenesisState - all state that must be provided at genesis +type GenesisState struct { + Params Params `json:"params" yaml:"params"` +} + +// NewGenesisState - Create a new genesis state +func NewGenesisState(params Params) GenesisState { + return GenesisState{params} +} + +// DefaultGenesisState - Return a default genesis state +func DefaultGenesisState() GenesisState { + return NewGenesisState(DefaultParams()) +} + +// ValidateGenesis performs basic validation of genesis data returning an +// error for any failed validation criteria. +func ValidateGenesis(data GenesisState) error { + if amino.DeepEqual(data, GenesisState{}) { + return fmt.Errorf("vm genesis state cannot be empty") + } + return data.Params.Validate() +} + +// InitGenesis - Init store state from genesis data +func (vm *VMKeeper) InitGenesis(ctx sdk.Context, data GenesisState) { + if err := ValidateGenesis(data); err != nil { + panic(err) + } + if err := vm.SetParams(ctx, data.Params); err != nil { + panic(err) + } +} + +// ExportGenesis returns a GenesisState for a given context and keeper +func (vm *VMKeeper) ExportGenesis(ctx sdk.Context) GenesisState { + params := vm.GetParams(ctx) + return NewGenesisState(params) +} diff --git a/gno.land/pkg/sdk/vm/keeper.go b/gno.land/pkg/sdk/vm/keeper.go index db2574502f1..7e620542bb7 100644 --- a/gno.land/pkg/sdk/vm/keeper.go +++ b/gno.land/pkg/sdk/vm/keeper.go @@ -53,6 +53,7 @@ type VMKeeperI interface { LoadStdlibCached(ctx sdk.Context, stdlibDir string) MakeGnoTransactionStore(ctx sdk.Context) sdk.Context CommitGnoTransactionStore(ctx sdk.Context) + InitGenesis(ctx sdk.Context, data GenesisState) } var _ VMKeeperI = &VMKeeper{} @@ -275,7 +276,7 @@ func (vm *VMKeeper) checkNamespacePermission(ctx sdk.Context, creator crypto.Add OriginPkgAddr: pkgAddr.Bech32(), // XXX: should we remove the banker ? Banker: NewSDKBanker(vm, ctx), - Params: NewSDKParams(vm, ctx), + Params: NewSDKParams(&vm.prmk, ctx), EventLogger: ctx.EventLogger(), } @@ -380,7 +381,7 @@ func (vm *VMKeeper) AddPackage(ctx sdk.Context, msg MsgAddPackage) (err error) { OriginSendSpent: new(std.Coins), OriginPkgAddr: pkgAddr.Bech32(), Banker: NewSDKBanker(vm, ctx), - Params: NewSDKParams(vm, ctx), + Params: NewSDKParams(&vm.prmk, ctx), EventLogger: ctx.EventLogger(), } // Parse and run the files, construct *PV. @@ -471,7 +472,7 @@ func (vm *VMKeeper) Call(ctx sdk.Context, msg MsgCall) (res string, err error) { OriginSendSpent: new(std.Coins), OriginPkgAddr: pkgAddr.Bech32(), Banker: NewSDKBanker(vm, ctx), - Params: NewSDKParams(vm, ctx), + Params: NewSDKParams(&vm.prmk, ctx), EventLogger: ctx.EventLogger(), } // Construct machine and evaluate. @@ -603,7 +604,7 @@ func (vm *VMKeeper) Run(ctx sdk.Context, msg MsgRun) (res string, err error) { OriginSendSpent: new(std.Coins), OriginPkgAddr: pkgAddr.Bech32(), Banker: NewSDKBanker(vm, ctx), - Params: NewSDKParams(vm, ctx), + Params: NewSDKParams(&vm.prmk, ctx), EventLogger: ctx.EventLogger(), } @@ -781,12 +782,12 @@ func (vm *VMKeeper) queryEvalInternal(ctx sdk.Context, pkgPath string, expr stri ChainDomain: chainDomain, Height: ctx.BlockHeight(), Timestamp: ctx.BlockTime().Unix(), - // OriginCaller: caller, - // OriginSend: send, - // OriginSendSpent: nil, + // OrigCaller: caller, + // OrigSend: send, + // OrigSendSpent: nil, OriginPkgAddr: pkgAddr.Bech32(), Banker: NewSDKBanker(vm, ctx), // safe as long as ctx is a fork to be discarded. - Params: NewSDKParams(vm, ctx), + Params: NewSDKParams(&vm.prmk, ctx), EventLogger: ctx.EventLogger(), } m := gno.NewMachineWithOptions( diff --git a/gno.land/pkg/sdk/vm/keeper_test.go b/gno.land/pkg/sdk/vm/keeper_test.go index 243b2712632..6a62c55ab68 100644 --- a/gno.land/pkg/sdk/vm/keeper_test.go +++ b/gno.land/pkg/sdk/vm/keeper_test.go @@ -200,7 +200,6 @@ func GetAdmin() string { res, err := env.vmk.Call(ctx, msg2) assert.Error(t, err) assert.Equal(t, "", res) - fmt.Println(err.Error()) assert.True(t, strings.Contains(err.Error(), "insufficient coins error")) } @@ -352,7 +351,7 @@ func TestVMKeeperParams(t *testing.T) { // Create test package. files := []*gnovm.MemFile{ {Name: "init.gno", Body: ` -package test +package params import "std" @@ -367,7 +366,7 @@ func Do() string { return "XXX" // return std.GetConfig("gno.land/r/test.foo"), if we want to expose std.GetConfig, maybe as a std.TestGetConfig }`}, } - pkgPath := "gno.land/r/test" + pkgPath := SysParamsRealmPath msg1 := NewMsgAddPackage(addr, pkgPath, files) err := env.vmk.AddPackage(ctx, msg1) assert.NoError(t, err) @@ -384,8 +383,8 @@ func Do() string { var foo string var bar int64 - env.vmk.prmk.GetString(ctx, "gno.land/r/test.foo.string", &foo) - env.vmk.prmk.GetInt64(ctx, "gno.land/r/test.bar.int64", &bar) + env.vmk.prmk.GetString(ctx, "vm:gno.land/r/sys/params.foo.string", &foo) + env.vmk.prmk.GetInt64(ctx, "vm:gno.land/r/sys/params.bar.int64", &bar) assert.Equal(t, "foo2", foo) assert.Equal(t, int64(1337), bar) } diff --git a/gno.land/pkg/sdk/vm/params.go b/gno.land/pkg/sdk/vm/params.go index 248fb8a81fb..81da1e9c1f6 100644 --- a/gno.land/pkg/sdk/vm/params.go +++ b/gno.land/pkg/sdk/vm/params.go @@ -1,20 +1,135 @@ package vm -import "github.com/gnolang/gno/tm2/pkg/sdk" +import ( + "fmt" + "regexp" + "strings" + + gno "github.com/gnolang/gno/gnovm/pkg/gnolang" + "github.com/gnolang/gno/tm2/pkg/amino" + "github.com/gnolang/gno/tm2/pkg/sdk" +) const ( - sysUsersPkgParamPath = "gno.land/r/sys/params.sys.users_pkgpath.string" - chainDomainParamPath = "gno.land/r/sys/params.chain_domain.string" + sysUsersPkgDefault = "gno.land/r/sys/users" + chainDomainDefault = "gno.land" + paramsKey = "p" +) + +var ASCIIDomain = regexp.MustCompile(`^(?:[A-Za-z0-9](?:[A-Za-z0-9-]{0,61}[A-Za-z0-9])?\.)+[A-Za-z]{2,}$`) + +// Params defines the parameters for the bank module. +type Params struct { + SysUsersPkgPath string `json:"sysusers_pkgpath" yaml:"sysusers_pkgpath"` + ChainDomain string `json:"chain_domain" yaml:"chain_domain"` +} + +// NewParams creates a new Params object +func NewParams(userPkgPath, chainDomain string) Params { + return Params{ + SysUsersPkgPath: userPkgPath, + ChainDomain: chainDomain, + } +} + +// DefaultParams returns a default set of parameters. +func DefaultParams() Params { + return NewParams(sysUsersPkgDefault, chainDomainDefault) +} + +// String implements the stringer interface. +func (p Params) String() string { + var sb strings.Builder + sb.WriteString("Params: \n") + sb.WriteString(fmt.Sprintf("SysUsersPkgPath: %q\n", p.SysUsersPkgPath)) + sb.WriteString(fmt.Sprintf("ChainDomain: %q\n", p.ChainDomain)) + return sb.String() +} + +func (p Params) Validate() error { + if p.SysUsersPkgPath != "" && !gno.ReRealmPath.MatchString(p.SysUsersPkgPath) { + return fmt.Errorf("invalid package/realm path %q, failed to match %q", p.SysUsersPkgPath, gno.ReRealmPath) + } + if p.ChainDomain != "" && !ASCIIDomain.MatchString(p.ChainDomain) { + return fmt.Errorf("invalid chain domain %q, failed to match %q", p.ChainDomain, ASCIIDomain) + } + return nil +} + +// Equals returns a boolean determining if two Params types are identical. +func (p Params) Equals(p2 Params) bool { + return amino.DeepEqual(p, p2) +} + +func (vm *VMKeeper) SetParams(ctx sdk.Context, params Params) error { + if err := params.Validate(); err != nil { + return err + } + err := vm.prmk.SetParams(ctx, ModuleName, paramsKey, params) + return err +} + +func (vm *VMKeeper) GetParams(ctx sdk.Context) Params { + params := &Params{} + // NOTE: important to not use local cached fields unless they are synchronously stored to the underlying store. + // this optimization generally only belongs in paramk.GetParams(), not here. users of paramk.GetParams() generally + // should not cache anything and instead rely on the efficiency of paramk.GetParams(). + _, err := vm.prmk.GetParams(ctx, ModuleName, paramsKey, params) + if err != nil { + panic(err) + } + + return *params +} + +const ( + sysUsersPkgParamPath = "vm:users_pkgpath.string" + chainDomainParamPath = "vm:chain_domain.string" ) func (vm *VMKeeper) getChainDomainParam(ctx sdk.Context) string { - chainDomain := "gno.land" // default + chainDomain := chainDomainDefault // default vm.prmk.GetString(ctx, chainDomainParamPath, &chainDomain) return chainDomain } func (vm *VMKeeper) getSysUsersPkgParam(ctx sdk.Context) string { - var sysUsersPkg string + sysUsersPkg := sysUsersPkgDefault vm.prmk.GetString(ctx, sysUsersPkgParamPath, &sysUsersPkg) return sysUsersPkg } + +func (vm *VMKeeper) GetParamfulKey() string { + return ModuleName +} + +// WillSetParam checks if the key contains the module's parameter key prefix and updates the +// module parameter accordingly.The key is in the format (.)?. If is present, +// the key is an arbitrary key; otherwise, the key is a module key and needs to be checked against +// the module's parameter keys. +func (vm *VMKeeper) WillSetParam(ctx sdk.Context, key string, value interface{}) { + params := vm.GetParams(ctx) + realm := gno.ReRealmPath.FindString(key) + var err error + if realm == "" { // module parameters + switch key { + case "sysusers_pkgpath.string": + params.SysUsersPkgPath = value.(string) + err = vm.SetParams(ctx, params) + case "chain_domain.string": + params.ChainDomain = value.(string) + err = vm.SetParams(ctx, params) + default: + panic(fmt.Sprintf("unknown parameter key: %s\n", key)) + } + } + if err != nil { + panic(err) + } + // TODO: Check for duplicate parameter key names between individual fields and the fields + // of the Params struct. + err = vm.prmk.SetParams(ctx, ModuleName, key, value) + if err != nil { + panic(err) + } +} diff --git a/gno.land/pkg/sdk/vm/params_test.go b/gno.land/pkg/sdk/vm/params_test.go new file mode 100644 index 00000000000..a809da1db05 --- /dev/null +++ b/gno.land/pkg/sdk/vm/params_test.go @@ -0,0 +1,125 @@ +package vm + +import ( + "fmt" + "testing" + + "github.com/stretchr/testify/assert" +) + +// TestParamsString verifies the output of the String method. +func TestParamsString(t *testing.T) { + p := Params{ + SysUsersPkgPath: "gno.land/r/sys/users", + ChainDomain: "example.com", + } + result := p.String() + + // Construct the expected string. + expected := "Params: \n" + + fmt.Sprintf("SysUsersPkgPath: %q\n", p.SysUsersPkgPath) + + fmt.Sprintf("ChainDomain: %q\n", p.ChainDomain) + + // Assert: check if the result matches the expected string. + if result != expected { + t.Errorf("Params.String() = %q; want %q", result, expected) + } +} + +func TestWillSetParam(t *testing.T) { + env := setupTestEnv() + ctx := env.vmk.MakeGnoTransactionStore(env.ctx) + vmk := env.vmk + dps := DefaultParams() + + tests := []struct { + name string + key string + value string + getExpectedValue func(prms Params) string + shouldPanic bool + isUpdated bool + isEqual bool + }{ + { + name: "update sysusers_pkgpath", + key: "sysusers_pkgpath.string", + value: "gno.land/r/foo", + getExpectedValue: func(prms Params) string { + return prms.SysUsersPkgPath + }, + shouldPanic: false, + isUpdated: true, + isEqual: true, + }, + { + name: "update chain_domain", + key: "chain_domain.string", + value: "example.com", + getExpectedValue: func(prms Params) string { + return prms.ChainDomain + }, + shouldPanic: false, + isUpdated: true, + isEqual: true, + }, + { + name: "unknown parameter key panics", + key: "unknown_key", + value: "some value", + getExpectedValue: nil, + shouldPanic: true, + isUpdated: false, + isEqual: false, // Not applicable, but included for consistency + }, + { + name: "non-empty realm does not update params", + key: "gno.land/r/sys/params.sysusers_pkgpath.string", + value: "gno.land/r/foo", + getExpectedValue: func(prms Params) string { + return prms.SysUsersPkgPath // Expect unchanged value + }, + shouldPanic: false, + isUpdated: false, + isEqual: false, + }, + { + name: "error from SetParams panics", + key: "sysusers_pkgpath.string", + value: "path/to/pkg", + shouldPanic: true, + isUpdated: false, + isEqual: false, // Not applicable + }, + { + name: "error from prmk.SetParams panics", + key: "chain_domain.string", + value: "example/com", + shouldPanic: true, + isUpdated: false, + isEqual: false, // Not applicable + }, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + if tt.shouldPanic { + assert.Panics(t, func() { + vmk.WillSetParam(ctx, tt.key, tt.value) + }, "expected panic for test: %s", tt.name) + } else { + vmk.WillSetParam(ctx, tt.key, tt.value) + if tt.getExpectedValue != nil { + prms := vmk.GetParams(ctx) + if tt.isUpdated { + assert.False(t, prms.Equals(dps), "expected values to be different") + } + if tt.isEqual { + actual := tt.getExpectedValue(prms) + assert.Equal(t, tt.value, actual, "expected values to match") + } + } + } + }) + } +} diff --git a/gnovm/pkg/test/test.go b/gnovm/pkg/test/test.go index 71ec3bb2568..5e352c43570 100644 --- a/gnovm/pkg/test/test.go +++ b/gnovm/pkg/test/test.go @@ -18,6 +18,7 @@ import ( "github.com/gnolang/gno/gnovm" gno "github.com/gnolang/gno/gnovm/pkg/gnolang" "github.com/gnolang/gno/gnovm/stdlibs" + gstd "github.com/gnolang/gno/gnovm/stdlibs/std" teststd "github.com/gnolang/gno/gnovm/tests/stdlibs/std" "github.com/gnolang/gno/tm2/pkg/crypto" "github.com/gnolang/gno/tm2/pkg/sdk" @@ -87,11 +88,11 @@ func newTestParams() *testParams { return &testParams{} } -func (tp *testParams) SetBool(key string, val bool) { /* noop */ } -func (tp *testParams) SetBytes(key string, val []byte) { /* noop */ } -func (tp *testParams) SetInt64(key string, val int64) { /* noop */ } -func (tp *testParams) SetUint64(key string, val uint64) { /* noop */ } -func (tp *testParams) SetString(key string, val string) { /* noop */ } +func (tp *testParams) SetBool(key gstd.ParamKey, val bool) { /* noop */ } +func (tp *testParams) SetBytes(key gstd.ParamKey, val []byte) { /* noop */ } +func (tp *testParams) SetInt64(key gstd.ParamKey, val int64) { /* noop */ } +func (tp *testParams) SetUint64(key gstd.ParamKey, val uint64) { /* noop */ } +func (tp *testParams) SetString(key gstd.ParamKey, val string) { /* noop */ } // ---------------------------------------- // main test function diff --git a/gnovm/stdlibs/generated.go b/gnovm/stdlibs/generated.go index 25d9b974e85..9d84d47b1f6 100644 --- a/gnovm/stdlibs/generated.go +++ b/gnovm/stdlibs/generated.go @@ -11,6 +11,7 @@ import ( libs_crypto_sha256 "github.com/gnolang/gno/gnovm/stdlibs/crypto/sha256" libs_math "github.com/gnolang/gno/gnovm/stdlibs/math" libs_std "github.com/gnolang/gno/gnovm/stdlibs/std" + libs_sys_params "github.com/gnolang/gno/gnovm/stdlibs/sys/params" libs_testing "github.com/gnolang/gno/gnovm/stdlibs/testing" libs_time "github.com/gnolang/gno/gnovm/stdlibs/time" ) @@ -762,6 +763,156 @@ var nativeFuncs = [...]NativeFunc{ p0, p1) }, }, + { + "sys/params", + "setPrefixedString", + []gno.FieldTypeExpr{ + {Name: gno.N("p0"), Type: gno.X("string")}, + {Name: gno.N("p1"), Type: gno.X("string")}, + {Name: gno.N("p2"), Type: gno.X("string")}, + }, + []gno.FieldTypeExpr{}, + true, + func(m *gno.Machine) { + b := m.LastBlock() + var ( + p0 string + rp0 = reflect.ValueOf(&p0).Elem() + p1 string + rp1 = reflect.ValueOf(&p1).Elem() + p2 string + rp2 = reflect.ValueOf(&p2).Elem() + ) + + gno.Gno2GoValue(b.GetPointerTo(nil, gno.NewValuePathBlock(1, 0, "")).TV, rp0) + gno.Gno2GoValue(b.GetPointerTo(nil, gno.NewValuePathBlock(1, 1, "")).TV, rp1) + gno.Gno2GoValue(b.GetPointerTo(nil, gno.NewValuePathBlock(1, 2, "")).TV, rp2) + + libs_sys_params.X_setPrefixedString( + m, + p0, p1, p2) + }, + }, + { + "sys/params", + "setPrefixedBool", + []gno.FieldTypeExpr{ + {Name: gno.N("p0"), Type: gno.X("string")}, + {Name: gno.N("p1"), Type: gno.X("string")}, + {Name: gno.N("p2"), Type: gno.X("bool")}, + }, + []gno.FieldTypeExpr{}, + true, + func(m *gno.Machine) { + b := m.LastBlock() + var ( + p0 string + rp0 = reflect.ValueOf(&p0).Elem() + p1 string + rp1 = reflect.ValueOf(&p1).Elem() + p2 bool + rp2 = reflect.ValueOf(&p2).Elem() + ) + + gno.Gno2GoValue(b.GetPointerTo(nil, gno.NewValuePathBlock(1, 0, "")).TV, rp0) + gno.Gno2GoValue(b.GetPointerTo(nil, gno.NewValuePathBlock(1, 1, "")).TV, rp1) + gno.Gno2GoValue(b.GetPointerTo(nil, gno.NewValuePathBlock(1, 2, "")).TV, rp2) + + libs_sys_params.X_setPrefixedBool( + m, + p0, p1, p2) + }, + }, + { + "sys/params", + "setPrefixedInt64", + []gno.FieldTypeExpr{ + {Name: gno.N("p0"), Type: gno.X("string")}, + {Name: gno.N("p1"), Type: gno.X("string")}, + {Name: gno.N("p2"), Type: gno.X("int64")}, + }, + []gno.FieldTypeExpr{}, + true, + func(m *gno.Machine) { + b := m.LastBlock() + var ( + p0 string + rp0 = reflect.ValueOf(&p0).Elem() + p1 string + rp1 = reflect.ValueOf(&p1).Elem() + p2 int64 + rp2 = reflect.ValueOf(&p2).Elem() + ) + + gno.Gno2GoValue(b.GetPointerTo(nil, gno.NewValuePathBlock(1, 0, "")).TV, rp0) + gno.Gno2GoValue(b.GetPointerTo(nil, gno.NewValuePathBlock(1, 1, "")).TV, rp1) + gno.Gno2GoValue(b.GetPointerTo(nil, gno.NewValuePathBlock(1, 2, "")).TV, rp2) + + libs_sys_params.X_setPrefixedInt64( + m, + p0, p1, p2) + }, + }, + { + "sys/params", + "setPrefixedUint64", + []gno.FieldTypeExpr{ + {Name: gno.N("p0"), Type: gno.X("string")}, + {Name: gno.N("p1"), Type: gno.X("string")}, + {Name: gno.N("p2"), Type: gno.X("uint64")}, + }, + []gno.FieldTypeExpr{}, + true, + func(m *gno.Machine) { + b := m.LastBlock() + var ( + p0 string + rp0 = reflect.ValueOf(&p0).Elem() + p1 string + rp1 = reflect.ValueOf(&p1).Elem() + p2 uint64 + rp2 = reflect.ValueOf(&p2).Elem() + ) + + gno.Gno2GoValue(b.GetPointerTo(nil, gno.NewValuePathBlock(1, 0, "")).TV, rp0) + gno.Gno2GoValue(b.GetPointerTo(nil, gno.NewValuePathBlock(1, 1, "")).TV, rp1) + gno.Gno2GoValue(b.GetPointerTo(nil, gno.NewValuePathBlock(1, 2, "")).TV, rp2) + + libs_sys_params.X_setPrefixedUint64( + m, + p0, p1, p2) + }, + }, + { + "sys/params", + "setPrefixedBytes", + []gno.FieldTypeExpr{ + {Name: gno.N("p0"), Type: gno.X("string")}, + {Name: gno.N("p1"), Type: gno.X("string")}, + {Name: gno.N("p2"), Type: gno.X("[]byte")}, + }, + []gno.FieldTypeExpr{}, + true, + func(m *gno.Machine) { + b := m.LastBlock() + var ( + p0 string + rp0 = reflect.ValueOf(&p0).Elem() + p1 string + rp1 = reflect.ValueOf(&p1).Elem() + p2 []byte + rp2 = reflect.ValueOf(&p2).Elem() + ) + + gno.Gno2GoValue(b.GetPointerTo(nil, gno.NewValuePathBlock(1, 0, "")).TV, rp0) + gno.Gno2GoValue(b.GetPointerTo(nil, gno.NewValuePathBlock(1, 1, "")).TV, rp1) + gno.Gno2GoValue(b.GetPointerTo(nil, gno.NewValuePathBlock(1, 2, "")).TV, rp2) + + libs_sys_params.X_setPrefixedBytes( + m, + p0, p1, p2) + }, + }, { "testing", "unixNano", @@ -922,6 +1073,7 @@ var initOrder = [...]string{ "regexp/syntax", "regexp", "std", + "sys/params", "testing", "time", "unicode/utf16", diff --git a/gnovm/stdlibs/std/params.gno b/gnovm/stdlibs/std/params.gno index ce400270cda..df2fc94cd36 100644 --- a/gnovm/stdlibs/std/params.gno +++ b/gnovm/stdlibs/std/params.gno @@ -6,6 +6,8 @@ func setParamInt64(key string, val int64) func setParamUint64(key string, val uint64) func setParamBytes(key string, val []byte) +// SetParamXXX(k, v) are for setting arbitrary parameters that can be called from any realm. +// It may or may not affect system behavior. func SetParamString(key string, val string) { setParamString(key, val) } func SetParamBool(key string, val bool) { setParamBool(key, val) } func SetParamInt64(key string, val int64) { setParamInt64(key, val) } diff --git a/gnovm/stdlibs/std/params.go b/gnovm/stdlibs/std/params.go index e21bd9912dd..7e46d82bf1d 100644 --- a/gnovm/stdlibs/std/params.go +++ b/gnovm/stdlibs/std/params.go @@ -8,65 +8,127 @@ import ( gno "github.com/gnolang/gno/gnovm/pkg/gnolang" ) -// ParamsInterface is the interface through which Gno is capable of accessing +// ParamsSetterInterface is the interface through which Gno is capable of accessing // the blockchain's params. // // The name is what it is to avoid a collision with Gno's Params, when // transpiling. + type ParamsInterface interface { - SetString(key, val string) - SetBool(key string, val bool) - SetInt64(key string, val int64) - SetUint64(key string, val uint64) - SetBytes(key string, val []byte) + SetString(key ParamKey, val string) + SetBool(key ParamKey, val bool) + SetInt64(key ParamKey, val int64) + SetUint64(key ParamKey, val uint64) + SetBytes(key ParamKey, val []byte) +} + +type ParamKey struct { + Realm string + Prefix string + Key string + Type string +} + +func NewParamKey(m *gno.Machine, prefix, key string, kind string) (ParamKey, error) { + // validate key. + if err := validate(prefix, key, kind); err != nil { + es := err.Error() + m.Panic(typedString(es)) + return ParamKey{}, err + } + _, realm := currentRealm(m) + + return ParamKey{ + Realm: realm, + Prefix: prefix, + Key: key, + Type: kind, + }, nil +} + +// String representation of ParamKey in the format: +// :(".")? +func (pk ParamKey) String() string { + pks := "" + if pk.Prefix == "" { + pks = fmt.Sprintf("vm:%s.%s", pk.Realm, pk.Key) + } else { + pks = fmt.Sprintf("%s:%s", pk.Prefix, pk.Key) + } + return pks } func X_setParamString(m *gno.Machine, key, val string) { - pk := pkey(m, key, "string") + pk, err := NewParamKey(m, "", key, "string") + if err != nil { + return + } GetContext(m).Params.SetString(pk, val) } func X_setParamBool(m *gno.Machine, key string, val bool) { - pk := pkey(m, key, "bool") + pk, err := NewParamKey(m, "", key, "bool") + if err != nil { + return + } GetContext(m).Params.SetBool(pk, val) } func X_setParamInt64(m *gno.Machine, key string, val int64) { - pk := pkey(m, key, "int64") + pk, err := NewParamKey(m, "", key, "int64") + if err != nil { + return + } GetContext(m).Params.SetInt64(pk, val) } func X_setParamUint64(m *gno.Machine, key string, val uint64) { - pk := pkey(m, key, "uint64") + pk, err := NewParamKey(m, "", key, "uint64") + if err != nil { + return + } GetContext(m).Params.SetUint64(pk, val) } func X_setParamBytes(m *gno.Machine, key string, val []byte) { - pk := pkey(m, key, "bytes") + pk, err := NewParamKey(m, "", key, "bytes") + if err != nil { + return + } GetContext(m).Params.SetBytes(pk, val) } -func pkey(m *gno.Machine, key string, kind string) string { +func validate(prefix, key, kind string) error { // validate key. untypedKey := strings.TrimSuffix(key, "."+kind) if key == untypedKey { - m.Panic(typedString("invalid param key: " + key)) + return fmt.Errorf("invalid parameter key: %s", key) } if len(key) == 0 { - m.Panic(typedString("empty param key")) + return fmt.Errorf("empty param key") } first := rune(key[0]) if !unicode.IsLetter(first) && first != '_' { - m.Panic(typedString("invalid param key: " + key)) + return fmt.Errorf("invalid parameter key: %s", key) } for _, char := range untypedKey[1:] { if !unicode.IsLetter(char) && !unicode.IsDigit(char) && char != '_' { - m.Panic(typedString("invalid param key: " + key)) + return fmt.Errorf("invalid parameter key: %s", key) } } - // decorate key with realm and type. - _, rlmPath := currentRealm(m) - return fmt.Sprintf("%s.%s", rlmPath, key) + if prefix != "" { + // validate keeperPrefix + first = rune(prefix[0]) + if !unicode.IsLetter(first) { + return fmt.Errorf("invalid prefix: %s", prefix) + } + for _, char := range prefix[1:] { + if !unicode.IsLetter(char) && !unicode.IsDigit(char) && char != '_' { + return fmt.Errorf("invalid prefix: %s", prefix) + } + } + } + return nil } diff --git a/gnovm/stdlibs/std/params_test.go b/gnovm/stdlibs/std/params_test.go new file mode 100644 index 00000000000..b726a1d3b9a --- /dev/null +++ b/gnovm/stdlibs/std/params_test.go @@ -0,0 +1,40 @@ +package std + +import ( + "testing" +) + +func TestValidate(t *testing.T) { + tests := []struct { + prefix string + key string + kind string + wantErr bool + }{ + // Valid cases + {"module", "valid_key.string", "string", false}, + {"prefix1", "validKey.int64", "int64", false}, + {"p_", "_valid123.bool", "bool", false}, + + // Invalid key cases + {"module", "invalidKey", "string", true}, // Missing ".kind" suffix + {"module", "", "string", true}, // Empty key + {"module", "1invalid.string", "string", true}, // Starts with a number + {"module", "-invalid.string", "string", true}, // Starts with an invalid character + {"module", "invalid-123.string", "string", true}, // Contains invalid character (-) + {"module", "valid/path.key.bool", "bool", true}, // Contains invalid character (/) + + // Invalid prefix cases + {"1prefix", "valid_key.string", "string", true}, // Prefix starts with a number + {"-prefix", "valid_key.string", "string", true}, // Prefix starts with an invalid character + {"prefix!", "valid_key.string", "string", true}, // Prefix contains invalid character (!) + {"module/submodule", "valid/path.key.bool", "bool", true}, // Prefix contains invalid character (/) + } + + for _, tt := range tests { + err := validate(tt.prefix, tt.key, tt.kind) + if (err != nil) != tt.wantErr { + t.Errorf("validate(%q, %q, %q) = %v, wantErr %v", tt.prefix, tt.key, tt.kind, err, tt.wantErr) + } + } +} diff --git a/gnovm/stdlibs/sys/params/params.gno b/gnovm/stdlibs/sys/params/params.gno new file mode 100644 index 00000000000..102098e6677 --- /dev/null +++ b/gnovm/stdlibs/sys/params/params.gno @@ -0,0 +1,20 @@ +package params + +func setPrefixedString(keeperPrefix, key string, val string) +func setPrefixedBool(keeperPrefix, key string, val bool) +func setPrefixedInt64(keeperPrefix, key string, val int64) +func setPrefixedUint64(keeperPrefix, key string, val uint64) +func setPrefixedBytes(keeperPrefix, key string, val []byte) + +// SetPrefixedXXX(module, k, v) is used for module parameters and can be called only from the gno.land/r/sys/params realm. +// It modifies an existing parameter predefined in the module and affects system behavior. + +func SetPrefixedString(keeperPrefix, key string, val string) { + setPrefixedString(keeperPrefix, key, val) +} +func SetPrefixedBool(keeperPrefix, key string, val bool) { setPrefixedBool(keeperPrefix, key, val) } +func SetPrefixedInt64(keeperPrefix, key string, val int64) { setPrefixedInt64(keeperPrefix, key, val) } +func SetPrefixedUint64(keeperPrefix, key string, val uint64) { + setPrefixedUint64(keeperPrefix, key, val) +} +func SetPrefixedBytes(keeperPrefix, key string, val []byte) { setPrefixedBytes(keeperPrefix, key, val) } diff --git a/gnovm/stdlibs/sys/params/params.go b/gnovm/stdlibs/sys/params/params.go new file mode 100644 index 00000000000..32e79d5aa40 --- /dev/null +++ b/gnovm/stdlibs/sys/params/params.go @@ -0,0 +1,46 @@ +package params + +import ( + gno "github.com/gnolang/gno/gnovm/pkg/gnolang" + "github.com/gnolang/gno/gnovm/stdlibs/std" +) + +func X_setPrefixedString(m *gno.Machine, keeperPrefix, key, val string) { + pk, err := std.NewParamKey(m, keeperPrefix, key, "string") + if err != nil { + return + } + std.GetContext(m).Params.SetString(pk, val) +} + +func X_setPrefixedBool(m *gno.Machine, keeperPrefix, key string, val bool) { + pk, err := std.NewParamKey(m, keeperPrefix, key, "bool") + if err != nil { + return + } + std.GetContext(m).Params.SetBool(pk, val) +} + +func X_setPrefixedInt64(m *gno.Machine, keeperPrefix, key string, val int64) { + pk, err := std.NewParamKey(m, keeperPrefix, key, "int64") + if err != nil { + return + } + std.GetContext(m).Params.SetInt64(pk, val) +} + +func X_setPrefixedUint64(m *gno.Machine, keeperPrefix, key string, val uint64) { + pk, err := std.NewParamKey(m, keeperPrefix, key, "uint64") + if err != nil { + return + } + std.GetContext(m).Params.SetUint64(pk, val) +} + +func X_setPrefixedBytes(m *gno.Machine, keeperPrefix, key string, val []byte) { + pk, err := std.NewParamKey(m, keeperPrefix, key, "bytes") + if err != nil { + return + } + std.GetContext(m).Params.SetBytes(pk, val) +} diff --git a/tm2/pkg/sdk/auth/abci.go b/tm2/pkg/sdk/auth/abci.go index 86cbf962fad..9cbebe676e9 100644 --- a/tm2/pkg/sdk/auth/abci.go +++ b/tm2/pkg/sdk/auth/abci.go @@ -14,6 +14,6 @@ func EndBlocker(ctx sdk.Context, gk GasPriceKeeperI) { // InitChainer is called in the InitChain(), it set the initial gas price in the // GasPriceKeeper store // for the next gas price -func InitChainer(ctx sdk.Context, gk GasPriceKeeper, gp std.GasPrice) { +func InitChainer(ctx sdk.Context, gk GasPriceKeeperI, gp std.GasPrice) { gk.SetGasPrice(ctx, gp) } diff --git a/tm2/pkg/sdk/auth/ante.go b/tm2/pkg/sdk/auth/ante.go index 12e47494664..f2634bcb9ce 100644 --- a/tm2/pkg/sdk/auth/ante.go +++ b/tm2/pkg/sdk/auth/ante.go @@ -322,7 +322,8 @@ func DeductFees(bank BankKeeperI, ctx sdk.Context, acc std.Account, fees std.Coi )) } - err := bank.SendCoins(ctx, acc.GetAddress(), FeeCollectorAddress(), fees) + // Sending coins is unrestricted to pay for gas fees + err := bank.SendCoinsUnrestricted(ctx, acc.GetAddress(), FeeCollectorAddress(), fees) if err != nil { return abciResult(err) } diff --git a/tm2/pkg/sdk/auth/genesis.go b/tm2/pkg/sdk/auth/genesis.go index c863c237a41..49799ad8b15 100644 --- a/tm2/pkg/sdk/auth/genesis.go +++ b/tm2/pkg/sdk/auth/genesis.go @@ -1,19 +1,38 @@ package auth import ( + "fmt" + "github.com/gnolang/gno/tm2/pkg/amino" "github.com/gnolang/gno/tm2/pkg/sdk" ) -// InitGenesis - Init store state from genesis data -func (ak AccountKeeper) InitGenesis(ctx sdk.Context, data GenesisState) { +// GenesisState - all state that must be provided at genesis +type GenesisState struct { + Params Params `json:"params" yaml:"params"` +} + +// NewGenesisState - Create a new genesis state +func NewGenesisState(params Params) GenesisState { + return GenesisState{params} +} + +// DefaultGenesisState - Return a default genesis state +func DefaultGenesisState() GenesisState { + return NewGenesisState(DefaultParams()) +} + +// ValidateGenesis performs basic validation of genesis data returning an +// error for any failed validation criteria. +func ValidateGenesis(data GenesisState) error { if amino.DeepEqual(data, GenesisState{}) { - if err := ak.SetParams(ctx, DefaultParams()); err != nil { - panic(err) - } - return + return fmt.Errorf("auth genesis state cannot be empty") } + return data.Params.Validate() +} +// InitGenesis - Init store state from genesis data +func (ak AccountKeeper) InitGenesis(ctx sdk.Context, data GenesisState) { if err := ValidateGenesis(data); err != nil { panic(err) } diff --git a/tm2/pkg/sdk/auth/keeper.go b/tm2/pkg/sdk/auth/keeper.go index fc83997fdc4..741c8e8d287 100644 --- a/tm2/pkg/sdk/auth/keeper.go +++ b/tm2/pkg/sdk/auth/keeper.go @@ -17,8 +17,10 @@ import ( type AccountKeeper struct { // The (unexposed) key used to access the store from the Context. key store.StoreKey - // The keeper used to store auth parameters + + // store module parameters paramk params.ParamsKeeper + // The prototypical Account constructor. proto func() std.Account } diff --git a/tm2/pkg/sdk/auth/params.go b/tm2/pkg/sdk/auth/params.go index fda85c7a3d6..eec50534c62 100644 --- a/tm2/pkg/sdk/auth/params.go +++ b/tm2/pkg/sdk/auth/params.go @@ -5,6 +5,7 @@ import ( "strings" "github.com/gnolang/gno/tm2/pkg/amino" + "github.com/gnolang/gno/tm2/pkg/crypto" "github.com/gnolang/gno/tm2/pkg/sdk" "github.com/gnolang/gno/tm2/pkg/std" ) @@ -13,25 +14,29 @@ type AuthParamsContextKey struct{} // Default parameter values const ( - DefaultMaxMemoBytes int64 = 65536 - DefaultTxSigLimit int64 = 7 - DefaultTxSizeCostPerByte int64 = 10 - DefaultSigVerifyCostED25519 int64 = 590 - DefaultSigVerifyCostSecp256k1 int64 = 1000 + DefaultMaxMemoBytes int64 = 65536 + DefaultTxSigLimit int64 = 7 + DefaultTxSizeCostPerByte int64 = 10 + DefaultSigVerifyCostED25519 int64 = 590 + DefaultSigVerifyCostSecp256k1 int64 = 1000 + DefaultGasPricesChangeCompressor int64 = 10 DefaultTargetGasRatio int64 = 70 // 70% of the MaxGas in a block + + paramsKey = "p" ) // Params defines the parameters for the auth module. type Params struct { - MaxMemoBytes int64 `json:"max_memo_bytes" yaml:"max_memo_bytes"` - TxSigLimit int64 `json:"tx_sig_limit" yaml:"tx_sig_limit"` - TxSizeCostPerByte int64 `json:"tx_size_cost_per_byte" yaml:"tx_size_cost_per_byte"` - SigVerifyCostED25519 int64 `json:"sig_verify_cost_ed25519" yaml:"sig_verify_cost_ed25519"` - SigVerifyCostSecp256k1 int64 `json:"sig_verify_cost_secp256k1" yaml:"sig_verify_cost_secp256k1"` - GasPricesChangeCompressor int64 `json:"gas_price_change_compressor" yaml:"gas_price_change_compressor"` - TargetGasRatio int64 `json:"target_gas_ratio" yaml:"target_gas_ratio"` - InitialGasPrice std.GasPrice `json:"initial_gasprice"` + MaxMemoBytes int64 `json:"max_memo_bytes" yaml:"max_memo_bytes"` + TxSigLimit int64 `json:"tx_sig_limit" yaml:"tx_sig_limit"` + TxSizeCostPerByte int64 `json:"tx_size_cost_per_byte" yaml:"tx_size_cost_per_byte"` + SigVerifyCostED25519 int64 `json:"sig_verify_cost_ed25519" yaml:"sig_verify_cost_ed25519"` + SigVerifyCostSecp256k1 int64 `json:"sig_verify_cost_secp256k1" yaml:"sig_verify_cost_secp256k1"` + GasPricesChangeCompressor int64 `json:"gas_price_change_compressor" yaml:"gas_price_change_compressor"` + TargetGasRatio int64 `json:"target_gas_ratio" yaml:"target_gas_ratio"` + InitialGasPrice std.GasPrice `json:"initial_gasprice"` + UnrestrictedAddrs []crypto.Address `json:"unrestricted_addrs" yaml:"unrestricted_addrs"` } // NewParams creates a new Params object @@ -56,15 +61,15 @@ func (p Params) Equals(p2 Params) bool { // DefaultParams returns a default set of parameters. func DefaultParams() Params { - return Params{ - MaxMemoBytes: DefaultMaxMemoBytes, - TxSigLimit: DefaultTxSigLimit, - TxSizeCostPerByte: DefaultTxSizeCostPerByte, - SigVerifyCostED25519: DefaultSigVerifyCostED25519, - SigVerifyCostSecp256k1: DefaultSigVerifyCostSecp256k1, - GasPricesChangeCompressor: DefaultGasPricesChangeCompressor, - TargetGasRatio: DefaultTargetGasRatio, - } + return NewParams( + DefaultMaxMemoBytes, + DefaultTxSigLimit, + DefaultTxSizeCostPerByte, + DefaultSigVerifyCostED25519, + DefaultSigVerifyCostSecp256k1, + DefaultGasPricesChangeCompressor, + DefaultTargetGasRatio, + ) } // String implements the stringer interface. @@ -83,16 +88,19 @@ func (p Params) String() string { } func (p Params) Validate() error { - if p.TxSigLimit == 0 { + if p.MaxMemoBytes <= 0 { + return fmt.Errorf("invalid max memo bytes: %d", p.MaxMemoBytes) + } + if p.TxSigLimit <= 0 { return fmt.Errorf("invalid tx signature limit: %d", p.TxSigLimit) } - if p.SigVerifyCostED25519 == 0 { + if p.SigVerifyCostED25519 <= 0 { return fmt.Errorf("invalid ED25519 signature verification cost: %d", p.SigVerifyCostED25519) } - if p.SigVerifyCostSecp256k1 == 0 { + if p.SigVerifyCostSecp256k1 <= 0 { return fmt.Errorf("invalid SECK256k1 signature verification cost: %d", p.SigVerifyCostSecp256k1) } - if p.TxSizeCostPerByte == 0 { + if p.TxSizeCostPerByte <= 0 { return fmt.Errorf("invalid tx size cost per byte: %d", p.TxSizeCostPerByte) } if p.GasPricesChangeCompressor <= 0 { @@ -108,20 +116,30 @@ func (ak AccountKeeper) SetParams(ctx sdk.Context, params Params) error { if err := params.Validate(); err != nil { return err } - err := ak.paramk.SetParams(ctx, ModuleName, params) + err := ak.paramk.SetParams(ctx, ModuleName, paramsKey, params) return err } func (ak AccountKeeper) GetParams(ctx sdk.Context) Params { params := &Params{} + // NOTE: important to not use local cached fields unless they are synchronously stored to the underlying store. + // this optimization generally only belongs in paramk.GetParams(), not here. users of paramk.GetParams() + // generally should not cache anything and instead rely on the efficiency of paramk.GetParams(). - ok, err := ak.paramk.GetParams(ctx, ModuleName, params) - - if !ok { - panic("params key " + ModuleName + " does not exist") - } + _, err := ak.paramk.GetParams(ctx, ModuleName, paramsKey, params) if err != nil { - panic(err.Error()) + panic(err) } + return *params } + +func (ak AccountKeeper) GetParamfulKey() string { + return ModuleName +} + +// WillSetParam checks if the key contains the module's parameter key prefix and updates the module parameter accordingly. +func (ak AccountKeeper) WillSetParam(ctx sdk.Context, key string, value interface{}) { + // TODO: add parameter settings here. + panic("setting params for auth is not supported yet") +} diff --git a/tm2/pkg/sdk/auth/test_common.go b/tm2/pkg/sdk/auth/test_common.go index e0a6316bead..7d0d031c914 100644 --- a/tm2/pkg/sdk/auth/test_common.go +++ b/tm2/pkg/sdk/auth/test_common.go @@ -28,12 +28,15 @@ func setupTestEnv() testEnv { ms := store.NewCommitMultiStore(db) ms.MountStoreWithDB(authCapKey, iavl.StoreConstructor, db) ms.LoadLatestVersion() + paramk := params.NewParamsKeeper(authCapKey) - paramk := params.NewParamsKeeper(authCapKey, "") acck := NewAccountKeeper(authCapKey, paramk, std.ProtoBaseAccount) bank := NewDummyBankKeeper(acck) gk := NewGasPriceKeeper(authCapKey) + paramk.Register(acck.GetParamfulKey(), acck) + paramk.Register(bank.GetParamfulKey(), bank) + ctx := sdk.NewContext(sdk.RunTxModeDeliver, ms, &bft.Header{Height: 1, ChainID: "test-chain-id"}, log.NewNoopLogger()) ctx = ctx.WithValue(AuthParamsContextKey{}, DefaultParams()) ctx = ctx.WithConsensusParams(&abci.ConsensusParams{ @@ -63,6 +66,10 @@ func NewDummyBankKeeper(acck AccountKeeper) DummyBankKeeper { return DummyBankKeeper{acck} } +func (bank DummyBankKeeper) SendCoinsUnrestricted(ctx sdk.Context, fromAddr crypto.Address, toAddr crypto.Address, amt std.Coins) error { + return bank.SendCoins(ctx, fromAddr, toAddr, amt) +} + // SendCoins for the dummy supply keeper func (bank DummyBankKeeper) SendCoins(ctx sdk.Context, fromAddr crypto.Address, toAddr crypto.Address, amt std.Coins) error { fromAcc := bank.acck.GetAccount(ctx, fromAddr) @@ -87,3 +94,10 @@ func (bank DummyBankKeeper) SendCoins(ctx sdk.Context, fromAddr crypto.Address, return nil } + +func (bank DummyBankKeeper) GetParamfulKey() string { + return "dummy_bank" +} + +// WillSetParam checks if the key contains the module's parameter key prefix and updates the module parameter accordingly. +func (bank DummyBankKeeper) WillSetParam(ctx sdk.Context, key string, value interface{}) { return } diff --git a/tm2/pkg/sdk/auth/types.go b/tm2/pkg/sdk/auth/types.go index 3fb2d10fbb5..4965122bf67 100644 --- a/tm2/pkg/sdk/auth/types.go +++ b/tm2/pkg/sdk/auth/types.go @@ -22,6 +22,7 @@ var _ AccountKeeperI = AccountKeeper{} // Limited interface only needed for auth. type BankKeeperI interface { SendCoins(ctx sdk.Context, fromAddr crypto.Address, toAddr crypto.Address, amt std.Coins) error + SendCoinsUnrestricted(ctx sdk.Context, fromAddr crypto.Address, toAddr crypto.Address, amt std.Coins) error } type GasPriceKeeperI interface { @@ -31,24 +32,3 @@ type GasPriceKeeperI interface { } var _ GasPriceKeeperI = GasPriceKeeper{} - -// GenesisState - all auth state that must be provided at genesis -type GenesisState struct { - Params Params `json:"params"` -} - -// NewGenesisState - Create a new genesis state -func NewGenesisState(params Params) GenesisState { - return GenesisState{params} -} - -// DefaultGenesisState - Return a default genesis state -func DefaultGenesisState() GenesisState { - return NewGenesisState(DefaultParams()) -} - -// ValidateGenesis performs basic validation of auth genesis data returning an -// error for any failed validation criteria. -func ValidateGenesis(data GenesisState) error { - return data.Params.Validate() -} diff --git a/tm2/pkg/sdk/bank/common_test.go b/tm2/pkg/sdk/bank/common_test.go index c8210be7175..28d0b72c2c4 100644 --- a/tm2/pkg/sdk/bank/common_test.go +++ b/tm2/pkg/sdk/bank/common_test.go @@ -16,9 +16,10 @@ import ( ) type testEnv struct { - ctx sdk.Context - bank BankKeeper - acck auth.AccountKeeper + ctx sdk.Context + bank BankKeeper + acck auth.AccountKeeper + paramk params.ParamsKeeper } func setupTestEnv() testEnv { @@ -29,13 +30,16 @@ func setupTestEnv() testEnv { ms := store.NewCommitMultiStore(db) ms.MountStoreWithDB(authCapKey, iavl.StoreConstructor, db) ms.LoadLatestVersion() - paramk := params.NewParamsKeeper(authCapKey, "") ctx := sdk.NewContext(sdk.RunTxModeDeliver, ms, &bft.Header{ChainID: "test-chain-id"}, log.NewNoopLogger()) + + paramk := params.NewParamsKeeper(authCapKey) acck := auth.NewAccountKeeper( authCapKey, paramk, std.ProtoBaseAccount, ) + bank := NewBankKeeper(acck, paramk) - bank := NewBankKeeper(acck) + paramk.Register(acck.GetParamfulKey(), acck) + paramk.Register(bank.GetParamfulKey(), bank) - return testEnv{ctx: ctx, bank: bank, acck: acck} + return testEnv{ctx: ctx, bank: bank, acck: acck, paramk: paramk} } diff --git a/tm2/pkg/sdk/bank/consts.go b/tm2/pkg/sdk/bank/consts.go index 4284a44c940..33a6e559ee6 100644 --- a/tm2/pkg/sdk/bank/consts.go +++ b/tm2/pkg/sdk/bank/consts.go @@ -1,5 +1,6 @@ package bank const ( - ModuleName = "bank" + ModuleName = "bank" + lockTransferKey = "lockTransfer.string" ) diff --git a/tm2/pkg/sdk/bank/genesis.go b/tm2/pkg/sdk/bank/genesis.go new file mode 100644 index 00000000000..ee6a474d693 --- /dev/null +++ b/tm2/pkg/sdk/bank/genesis.go @@ -0,0 +1,44 @@ +package bank + +import ( + "github.com/gnolang/gno/tm2/pkg/sdk" +) + +// GenesisState - all state that must be provided at genesis +type GenesisState struct { + Params Params `json:"params" yaml:"params"` +} + +// NewGenesisState - Create a new genesis state +func NewGenesisState(params Params) GenesisState { + return GenesisState{params} +} + +// DefaultGenesisState - Return a default genesis state +func DefaultGenesisState() GenesisState { + return NewGenesisState(DefaultParams()) +} + +// ValidateGenesis performs basic validation of genesis data returning an +// error for any failed validation criteria. +func ValidateGenesis(data GenesisState) error { + return data.Params.Validate() +} + +// InitGenesis - Init store state from genesis data +func (bank BankKeeper) InitGenesis(ctx sdk.Context, data GenesisState) { + if err := ValidateGenesis(data); err != nil { + panic(err) + } + + if err := bank.SetParams(ctx, data.Params); err != nil { + panic(err) + } +} + +// ExportGenesis returns a GenesisState for a given context and keeper +func (bank BankKeeper) ExportGenesis(ctx sdk.Context) GenesisState { + params := bank.GetParams(ctx) + + return NewGenesisState(params) +} diff --git a/tm2/pkg/sdk/bank/keeper.go b/tm2/pkg/sdk/bank/keeper.go index f98e6b3e225..e5f9f9005a7 100644 --- a/tm2/pkg/sdk/bank/keeper.go +++ b/tm2/pkg/sdk/bank/keeper.go @@ -7,6 +7,7 @@ import ( "github.com/gnolang/gno/tm2/pkg/crypto" "github.com/gnolang/gno/tm2/pkg/sdk" "github.com/gnolang/gno/tm2/pkg/sdk/auth" + "github.com/gnolang/gno/tm2/pkg/sdk/params" "github.com/gnolang/gno/tm2/pkg/std" ) @@ -21,9 +22,13 @@ type BankKeeperI interface { SubtractCoins(ctx sdk.Context, addr crypto.Address, amt std.Coins) (std.Coins, error) AddCoins(ctx sdk.Context, addr crypto.Address, amt std.Coins) (std.Coins, error) SetCoins(ctx sdk.Context, addr crypto.Address, amt std.Coins) error + SendCoinsUnrestricted(ctx sdk.Context, fromAddr crypto.Address, toAddr crypto.Address, amt std.Coins) error + + InitGenesis(ctx sdk.Context, data GenesisState) + GetParams(ctx sdk.Context) Params } -var _ BankKeeperI = BankKeeper{} +var _ BankKeeperI = &BankKeeper{} // BankKeeper only allows transfers between accounts without the possibility of // creating coins. It implements the BankKeeperI interface. @@ -31,16 +36,82 @@ type BankKeeper struct { ViewKeeper acck auth.AccountKeeper + // The keeper used to store parameters + paramk params.ParamsKeeper } // NewBankKeeper returns a new BankKeeper. -func NewBankKeeper(acck auth.AccountKeeper) BankKeeper { +func NewBankKeeper(acck auth.AccountKeeper, pk params.ParamsKeeper) BankKeeper { return BankKeeper{ ViewKeeper: NewViewKeeper(acck), acck: acck, + paramk: pk, } } +func (bank BankKeeper) AddRestrictedDenoms(ctx sdk.Context, restrictedDenoms ...string) { + if len(restrictedDenoms) == 0 { + return + } + params := bank.GetParams(ctx) + rdSet := toSet(params.RestrictedDenoms) + for _, denom := range restrictedDenoms { + rdSet[denom] = struct{}{} + } + + ds := make([]string, 0, len(rdSet)) + for d := range rdSet { + ds = append(ds, d) + } + params.RestrictedDenoms = ds + if err := bank.SetParams(ctx, params); err != nil { + panic(err) + } +} + +func (bank BankKeeper) DelRestrictedDenoms(ctx sdk.Context, restrictedDenoms ...string) { + params := bank.GetParams(ctx) + + rdSet := toSet(params.RestrictedDenoms) + for _, denom := range restrictedDenoms { + delete(rdSet, denom) + } + ds := make([]string, 0, len(rdSet)) + for d := range rdSet { + ds = append(ds, d) + } + params.RestrictedDenoms = ds + if err := bank.SetParams(ctx, params); err != nil { + panic(err) + } +} + +func (bank BankKeeper) DelAllRestrictedDenoms(ctx sdk.Context) { + params := bank.GetParams(ctx) + params.RestrictedDenoms = []string{} + + err := bank.paramk.SetParams(ctx, ModuleName, paramsKey, params) + if err != nil { + panic(err) + } +} + +func (bank BankKeeper) RestrictedDenoms(ctx sdk.Context) []string { + params := bank.GetParams(ctx) + return params.RestrictedDenoms +} + +type stringSet map[string]struct{} + +func toSet(str []string) stringSet { + ss := stringSet{} + + for _, key := range str { + ss[key] = struct{}{} + } + return ss +} + // InputOutputCoins handles a list of inputs and outputs func (bank BankKeeper) InputOutputCoins(ctx sdk.Context, inputs []Input, outputs []Output) error { // Safety check ensuring that when sending coins the bank must maintain the @@ -50,6 +121,9 @@ func (bank BankKeeper) InputOutputCoins(ctx sdk.Context, inputs []Input, outputs } for _, in := range inputs { + if !bank.canSendCoins(ctx, in.Address, in.Coins) { + return std.RestrictedTransferError{} + } _, err := bank.SubtractCoins(ctx, in.Address, in.Coins) if err != nil { return err @@ -84,8 +158,45 @@ func (bank BankKeeper) InputOutputCoins(ctx sdk.Context, inputs []Input, outputs return nil } -// SendCoins moves coins from one account to another +// canSendCoins returns true if the coins can be sent without violating any restriction. +func (bank BankKeeper) canSendCoins(ctx sdk.Context, addr crypto.Address, amt std.Coins) bool { + rds := bank.RestrictedDenoms(ctx) + if len(rds) == 0 { + // No restrictions. + return true + } + if amt.ContainOneOfDenom(toSet(rds)) { + acc := bank.acck.GetAccount(ctx, addr) + accr := acc.(std.AccountRestricter) + if acc != nil && !accr.IsUnrestricted() { + return false + } + } + return true +} + +// SendCoins moves coins from one account to another, restrction could be applied func (bank BankKeeper) SendCoins(ctx sdk.Context, fromAddr crypto.Address, toAddr crypto.Address, amt std.Coins) error { + // read restricted boolean value from param.IsRestrictedTransfer() + // canSendCoins is true until they have agreed to the waiver + if !bank.canSendCoins(ctx, fromAddr, amt) { + return std.RestrictedTransferError{} + } + + return bank.sendCoins(ctx, fromAddr, toAddr, amt) +} + +// SendCoinsUnrestricted is used for paying gas. +func (bank BankKeeper) SendCoinsUnrestricted(ctx sdk.Context, fromAddr crypto.Address, toAddr crypto.Address, amt std.Coins) error { + return bank.sendCoins(ctx, fromAddr, toAddr, amt) +} + +func (bank BankKeeper) sendCoins( + ctx sdk.Context, + fromAddr crypto.Address, + toAddr crypto.Address, + amt std.Coins, +) error { _, err := bank.SubtractCoins(ctx, fromAddr, amt) if err != nil { return err diff --git a/tm2/pkg/sdk/bank/keeper_test.go b/tm2/pkg/sdk/bank/keeper_test.go index df2039a682c..8d2c01f320e 100644 --- a/tm2/pkg/sdk/bank/keeper_test.go +++ b/tm2/pkg/sdk/bank/keeper_test.go @@ -95,7 +95,7 @@ func TestBankKeeper(t *testing.T) { env := setupTestEnv() ctx := env.ctx - bank := NewBankKeeper(env.acck) + bank := env.bank addr := crypto.AddressFromPreimage([]byte("addr1")) addr2 := crypto.AddressFromPreimage([]byte("addr2")) @@ -160,3 +160,71 @@ func TestViewKeeper(t *testing.T) { require.False(t, view.HasCoins(ctx, addr, std.NewCoins(std.NewCoin("foocoin", 15)))) require.False(t, view.HasCoins(ctx, addr, std.NewCoins(std.NewCoin("barcoin", 5)))) } + +// Test AddRestrictedDenoms +func TestAddRestrictedDenoms(t *testing.T) { + env := setupTestEnv() + ctx := env.ctx + bank := env.bank + // Add a single denom + bank.AddRestrictedDenoms(ctx, "foo") + params := bank.GetParams(ctx) + require.Contains(t, params.RestrictedDenoms, "foo") + + // Add multiple denoms + bank.AddRestrictedDenoms(ctx, "goo", "bar") + params = bank.GetParams(ctx) + require.Contains(t, params.RestrictedDenoms, "goo") + require.Contains(t, params.RestrictedDenoms, "bar") + + // Add duplicate denom (should not duplicate) + initialLength := len(params.RestrictedDenoms) + bank.AddRestrictedDenoms(ctx, "bar") + params = bank.GetParams(ctx) + require.Equal(t, initialLength, len(params.RestrictedDenoms)) // No change in length + + // Add empty list (should not modify state) + paramsBefore := bank.GetParams(ctx) + bank.AddRestrictedDenoms(ctx) + paramsAfter := bank.GetParams(ctx) + require.Equal(t, paramsBefore.RestrictedDenoms, paramsAfter.RestrictedDenoms) // No change +} + +// Test DelRestrictedDenoms +func TestDelRestrictedDenoms(t *testing.T) { + env := setupTestEnv() + ctx := env.ctx + bank := env.bank + + bank.AddRestrictedDenoms(ctx, "bar") + bank.AddRestrictedDenoms(ctx, "foo") + + // Remove a single existing denom + bank.DelRestrictedDenoms(ctx, "bar") + params := bank.GetParams(ctx) + require.NotContains(t, params.RestrictedDenoms, "bar") + + // Remove multiple denoms + bank.DelRestrictedDenoms(ctx, "bar", "foo") // + params = bank.GetParams(ctx) + require.NotContains(t, params.RestrictedDenoms, "foo") + require.NotContains(t, params.RestrictedDenoms, "bar") + + // Remove from an empty list + bank.DelAllRestrictedDenoms(ctx) + bank.DelRestrictedDenoms(ctx, "bar") // Should not fail + params = bank.GetParams(ctx) + require.Len(t, params.RestrictedDenoms, 0) // Still empty +} + +// Test DelAllRestrictedDenoms +func TestDelAllRestrictedDenoms(t *testing.T) { + env := setupTestEnv() + ctx := env.ctx + bank := env.bank + + // Delete all + bank.DelAllRestrictedDenoms(ctx) + params := bank.GetParams(ctx) + require.Empty(t, params.RestrictedDenoms) // Should be empty +} diff --git a/tm2/pkg/sdk/bank/params.go b/tm2/pkg/sdk/bank/params.go new file mode 100644 index 00000000000..4049d494341 --- /dev/null +++ b/tm2/pkg/sdk/bank/params.go @@ -0,0 +1,87 @@ +package bank + +import ( + "fmt" + "strings" + + "github.com/gnolang/gno/tm2/pkg/sdk" + "github.com/gnolang/gno/tm2/pkg/std" +) + +const paramsKey = "p" + +type BankParamsContextKey struct{} + +// Params defines the parameters for the bank module. +type Params struct { + RestrictedDenoms []string `json:"restricted_denoms" yaml:"restricted_denoms"` +} + +// NewParams creates a new Params object +func NewParams(restDenoms []string) Params { + return Params{ + RestrictedDenoms: restDenoms, + } +} + +// DefaultParams returns a default set of parameters. +func DefaultParams() Params { + return NewParams([]string{}) +} + +// String implements the stringer interface. +func (p Params) String() string { + var sb strings.Builder + sb.WriteString("Params: \n") + sb.WriteString(fmt.Sprintf("RestrictedDenom: %q\n", p.RestrictedDenoms)) + return sb.String() +} + +func (p *Params) Validate() error { + for _, denom := range p.RestrictedDenoms { + err := std.ValidateDenom(denom) + if err != nil { + return fmt.Errorf("invalid restricted denom: %s", denom) + } + } + return nil +} + +func (bank BankKeeper) SetParams(ctx sdk.Context, params Params) error { + if err := params.Validate(); err != nil { + return err + } + err := bank.paramk.SetParams(ctx, ModuleName, paramsKey, params) + + return err +} + +func (bank BankKeeper) GetParams(ctx sdk.Context) Params { + params := &Params{} + // NOTE: important to not use local cached fields unless they are synchronously stored to the underlying store. + // this optimization generally only belongs in paramk.GetParams(), not here. users of paramk.GetParams() generally + // should not cache anything and instead rely on the efficiency of paramk.GetParams(). + _, err := bank.paramk.GetParams(ctx, ModuleName, paramsKey, params) + if err != nil { + panic(err) + } + return *params +} + +func (bank BankKeeper) GetParamfulKey() string { + return ModuleName +} + +// WillSetParam checks if the key contains the module's parameter key and updates the module parameter accordingly. +func (bank BankKeeper) WillSetParam(ctx sdk.Context, key string, value interface{}) { + switch key { + case lockTransferKey: + if value != "" { // lock sending denoms + bank.AddRestrictedDenoms(ctx, value.(string)) + } else { // unlock sending ugnot + bank.DelAllRestrictedDenoms(ctx) + } + default: + panic(fmt.Sprintf("invalid bank parameter key: %s", key)) + } +} diff --git a/tm2/pkg/sdk/params/doc.go b/tm2/pkg/sdk/params/doc.go index a433b5eb115..7e94e6d7e53 100644 --- a/tm2/pkg/sdk/params/doc.go +++ b/tm2/pkg/sdk/params/doc.go @@ -4,12 +4,55 @@ // It includes a keeper for managing key-value pairs with module identifiers as // prefixes, along with a global querier for retrieving any key from any module. // -// Changes: This version removes the concepts of subspaces and proposals, -// allowing the creation of multiple keepers identified by a provided prefix. -// Proposals may be added later when governance modules are introduced. The -// transient store and .Modified helper have also been removed but can be -// implemented later if needed. Keys are represented as strings instead of -// []byte. +// The Params Module provides functionalities for caching and persistent access +// to parameters across the entire chain. // +// It manages both module parameters and arbitrary parameters. +// Module parameters are sourced from all other keepers, such as AuthKeeper, +// BankKeeper, and VMKeeper.Each keeper registers its keeper keys with ParamKeeper +// using the ParamfulKeeper interface.WillSetParam() is called whenever module +// parameters need to be updated. +// +// +// NOTE: important to not use local cached fields unless they are synchronously +// stored to the underlying store. This optimization generally only belongs in paramk.GetParams(). +// users of paramk.GetParams() generally should not cache anything and +// instead rely on the efficiency of paramk.GetParams(). +// +// In other words, ParamKeeper is the only component responsible for caching and storing parameters. +// Other keepers should neither cache nor maintain these parameters as state variables. +// While store access is synchronized, keeper access is not. +// +// Here is the interval key specs that is stored + +// /pv/:(".")? +// +// +// ParamKeeper.SetParams(module_prefix, k, v) is used by each registered module keeper to set the +// module parameter struct. +// ParamKeeper.SetParamXXX() is used to set arbitrary parameters as single primitive values. +// +// A prefix, ValueStoreKeyPrefix (/pv/), is added to each key before it is stored as the internal key: +// Module parameter keys follow this format: +// /pv/: + +// For arbitrary parameter keys that set from realm. +// /pv/vm:. + +// The method for querying parameters follows this pattern: +// To query module parameters: +// gnokey query params/: +// +// Since a module parameter is a struct, a simple key "p" is used by the each module +// For example, to query the Auth module's parameters: +// gnokey query params/auth:p +// +// To query arbitrary parameters: +// gnokey query params/ +// +// For example: +// gnokey query params/gno.land/r/myrealm.foo + // XXX: removes isAlphaNum validation for keys. +// (isAlphaNum for request router validation not sure we want to change it) package params diff --git a/tm2/pkg/sdk/params/handler.go b/tm2/pkg/sdk/params/handler.go index b662fc06c58..379056ffaa2 100644 --- a/tm2/pkg/sdk/params/handler.go +++ b/tm2/pkg/sdk/params/handler.go @@ -3,6 +3,7 @@ package params import ( "fmt" "strings" + "unicode" abci "github.com/gnolang/gno/tm2/pkg/bft/abci/types" "github.com/gnolang/gno/tm2/pkg/sdk" @@ -24,33 +25,25 @@ func (bh paramsHandler) Process(ctx sdk.Context, msg std.Msg) sdk.Result { return abciResult(std.ErrUnknownRequest(errMsg)) } -//---------------------------------------- -// Query - +// ---------------------------------------- +// Query: +// - params/prefix:key for a prefixed module parameter key. +// - params/key for an arbitrary parameter key. func (bh paramsHandler) Query(ctx sdk.Context, req abci.RequestQuery) (res abci.ResponseQuery) { - switch secondPart(req.Path) { - case bh.params.prefix: - return bh.queryParam(ctx, req) - default: - res = sdk.ABCIResponseQueryFromError( - std.ErrUnknownRequest("unknown params query endpoint")) - return + prefix, paramKey := parseParamKey(req.Path) + if prefix != "" { + if bh.params.PrefixExists(prefix) == false { + res = sdk.ABCIResponseQueryFromError( + std.ErrUnknownRequest(fmt.Sprintf("unknown params query endpoint %q", prefix))) + return + } } -} - -// queryParam returns param for a key. -func (bh paramsHandler) queryParam(ctx sdk.Context, req abci.RequestQuery) (res abci.ResponseQuery) { - // parse key from path. - key := thirdPartWithSlashes(req.Path) - if key == "" { + if paramKey == "" { res = sdk.ABCIResponseQueryFromError( std.ErrUnknownRequest("param key is empty")) + return } - - // XXX: validate? - - val := bh.params.GetRaw(ctx, key) - + val := bh.params.GetRaw(ctx, paramKey) res.Data = val return } @@ -62,18 +55,34 @@ func abciResult(err error) sdk.Result { return sdk.ABCIResultFromError(err) } -// returns the second component of a path. -func secondPart(path string) string { - parts := strings.SplitN(path, "/", 3) +// paramKey may include a prefix in the format "" if a prefix is detected. +func parseParamKey(path string) (prefix, key string) { + parts := strings.SplitN(path, "/", 2) if len(parts) < 2 { - return "" - } else { - return parts[1] + return "", "" + } + remainder := parts[1] + // Look for the first colon. + colonIndex := strings.Index(remainder, ":") + if colonIndex == -1 { + // No colon found: treat entire remainder as the key. + return "", remainder + } + + candidatePrefix := remainder[:colonIndex] + // If candidatePrefix , it is not considered a valid module prefix. + + isPrefix := true + for _, char := range candidatePrefix { + if !unicode.IsLetter(char) && !unicode.IsDigit(char) && char != '_' { + isPrefix = false + } + } + + if isPrefix == false { + return "", remainder } -} -// returns the third component of a path, including other slashes. -func thirdPartWithSlashes(path string) string { - split := strings.SplitN(path, "/", 3) - return split[2] + // Otherwise, candidatePrefix is valid. + return candidatePrefix, remainder } diff --git a/tm2/pkg/sdk/params/handler_test.go b/tm2/pkg/sdk/params/handler_test.go index 071eb12b52b..d2a75e1e232 100644 --- a/tm2/pkg/sdk/params/handler_test.go +++ b/tm2/pkg/sdk/params/handler_test.go @@ -21,7 +21,7 @@ func TestInvalidMsg(t *testing.T) { require.True(t, strings.Contains(res.Log, "unrecognized params message type")) } -func TestQuery(t *testing.T) { +func TestArbitraryParamsQuery(t *testing.T) { t.Parallel() env := setupTestEnv() @@ -31,11 +31,11 @@ func TestQuery(t *testing.T) { path string expected string }{ - {path: "params/params_test/foo/bar.string", expected: `"baz"`}, - {path: "params/params_test/foo/bar.int64", expected: `"-12345"`}, - {path: "params/params_test/foo/bar.uint64", expected: `"4242"`}, - {path: "params/params_test/foo/bar.bool", expected: "true"}, - {path: "params/params_test/foo/bar.bytes", expected: `"YmF6"`}, + {path: "params/foo/bar.string", expected: `"baz"`}, + {path: "params/foo/bar.int64", expected: `"-12345"`}, + {path: "params/foo/bar.uint64", expected: `"4242"`}, + {path: "params/foo/bar.bool", expected: "true"}, + {path: "params/foo/bar.bytes", expected: `"YmF6"`}, } for _, tc := range tcs { @@ -61,17 +61,59 @@ func TestQuery(t *testing.T) { res := h.Query(env.ctx, req) require.Nil(t, res.Error) require.NotNil(t, res) - assert.Equal(t, string(res.Data), tc.expected) + assert.Equal(t, tc.expected, string(res.Data)) } } -func TestQuerierRouteNotFound(t *testing.T) { +func TestModuleParamsQuery(t *testing.T) { t.Parallel() + env := setupTestEnv() + h := NewHandler(env.keeper) + tcs := []struct { + path string + expected string + }{ + {path: "params/params_test:foo/bar.string", expected: `"baz"`}, + {path: "params/params_test:foo/bar.int64", expected: `"-12345"`}, + {path: "params/params_test:foo/bar.uint64", expected: `"4242"`}, + {path: "params/params_test:foo/bar.bool", expected: "true"}, + {path: "params/params_test:foo/bar.bytes", expected: `"YmF6"`}, + } + + for _, tc := range tcs { + req := abci.RequestQuery{ + Path: tc.path, + } + res := h.Query(env.ctx, req) + require.Nil(t, res.Error) + require.NotNil(t, res) + require.Nil(t, res.Data) + } + + env.keeper.SetString(env.ctx, "params_test:foo/bar.string", "baz") + env.keeper.SetInt64(env.ctx, "params_test:foo/bar.int64", -12345) + env.keeper.SetUint64(env.ctx, "params_test:foo/bar.uint64", 4242) + env.keeper.SetBool(env.ctx, "params_test:foo/bar.bool", true) + env.keeper.SetBytes(env.ctx, "params_test:foo/bar.bytes", []byte("baz")) + + for _, tc := range tcs { + req := abci.RequestQuery{ + Path: tc.path, + } + res := h.Query(env.ctx, req) + require.Nil(t, res.Error) + require.NotNil(t, res) + assert.Equal(t, tc.expected, string(res.Data)) + } +} + +func TestQuerierRouteNotFound(t *testing.T) { + t.Parallel() env := setupTestEnv() h := NewHandler(env.keeper) req := abci.RequestQuery{ - Path: "params/notfound", + Path: "params/notfound:", Data: []byte{}, } res := h.Query(env.ctx, req) diff --git a/tm2/pkg/sdk/params/keeper.go b/tm2/pkg/sdk/params/keeper.go index c99b9dbfde1..db01df88ad5 100644 --- a/tm2/pkg/sdk/params/keeper.go +++ b/tm2/pkg/sdk/params/keeper.go @@ -1,6 +1,7 @@ package params import ( + "fmt" "log/slog" "strings" @@ -37,47 +38,52 @@ type ParamsKeeperI interface { Has(ctx sdk.Context, key string) bool GetRaw(ctx sdk.Context, key string) []byte + GetParams(ctx sdk.Context, prefixKey string, key string, target interface{}) (bool, error) + SetParams(ctx sdk.Context, prefixKey string, key string, params interface{}) error + // XXX: ListKeys? } +type ParamfulKeeper interface { + GetParamfulKey() string + WillSetParam(ctx sdk.Context, key string, value interface{}) +} var _ ParamsKeeperI = ParamsKeeper{} // global paramstore Keeper. type ParamsKeeper struct { - key store.StoreKey - prefix string + key store.StoreKey + kprs map[string]ParamfulKeeper // Register a prefix for module parameter keys. } // NewParamsKeeper returns a new ParamsKeeper. -func NewParamsKeeper(key store.StoreKey, prefix string) ParamsKeeper { +func NewParamsKeeper(key store.StoreKey) ParamsKeeper { return ParamsKeeper{ - key: key, - prefix: prefix, + key: key, + kprs: map[string]ParamfulKeeper{}, } } -// GetParam gets a param value from the global param store. -func (pk ParamsKeeper) GetParams(ctx sdk.Context, key string, target interface{}) (bool, error) { - stor := ctx.Store(pk.key) +func (pk ParamsKeeper) GetRegisteredKeeper(keeperKey string) ParamfulKeeper { + rk, ok := pk.kprs[keeperKey] - bz := stor.Get(ValueStoreKey(key)) - if bz == nil { - return false, nil + if !ok { + panic("keeper key " + keeperKey + " does not exist") } + return rk +} - return true, amino.UnmarshalJSON(bz, target) +func (pk ParamsKeeper) Register(keeperKey string, pmk ParamfulKeeper) { + pk.kprs[keeperKey] = pmk } -// SetParam sets a param value to the global param store. -func (pk ParamsKeeper) SetParams(ctx sdk.Context, key string, param interface{}) error { - stor := ctx.Store(pk.key) - bz, err := amino.MarshalJSON(param) - if err != nil { - return err - } +func (pk ParamsKeeper) IsRegistered(keeperKey string) bool { + _, ok := pk.kprs[keeperKey] + return ok +} - stor.Set(ValueStoreKey(key), bz) - return nil +func (pk ParamsKeeper) PrefixExists(prefix string) bool { + return pk.IsRegistered(prefix) } // XXX: why do we expose this? @@ -87,12 +93,14 @@ func (pk ParamsKeeper) Logger(ctx sdk.Context) *slog.Logger { func (pk ParamsKeeper) Has(ctx sdk.Context, key string) bool { stor := ctx.Store(pk.key) - return stor.Has([]byte(key)) + vk := ValueStoreKey(key) + return stor.Has(vk) } func (pk ParamsKeeper) GetRaw(ctx sdk.Context, key string) []byte { stor := ctx.Store(pk.key) - return stor.Get([]byte(key)) + vk := ValueStoreKey(key) + return stor.Get(vk) } func (pk ParamsKeeper) GetString(ctx sdk.Context, key string, ptr *string) { @@ -145,34 +153,85 @@ func (pk ParamsKeeper) SetBytes(ctx sdk.Context, key string, value []byte) { pk.set(ctx, key, value) } -func (pk ParamsKeeper) getIfExists(ctx sdk.Context, key string, ptr interface{}) { +// GetParam gets a param value from the global param store. +// Users generally should not cache anything and instead rely on the efficiency +// of paramk.GetParams(). +func (pk ParamsKeeper) GetParams(ctx sdk.Context, moduleKey string, key string, target interface{}) (bool, error) { + if moduleKey != "" { + if pk.IsRegistered(moduleKey) { + key = moduleKey + ":" + key + } else { + return false, fmt.Errorf("params module key %q does not exisit", moduleKey) + } + } + stor := ctx.Store(pk.key) - bz := stor.Get([]byte(key)) + vk := ValueStoreKey(key) + bz := stor.Get(vk) if bz == nil { - return + return false, nil } - err := amino.UnmarshalJSON(bz, ptr) + + return true, amino.UnmarshalJSON(bz, target) +} + +// SetParam sets a param value to the global param store. +func (pk ParamsKeeper) SetParams(ctx sdk.Context, moduleKey string, key string, param interface{}) error { + if moduleKey != "" { + if pk.IsRegistered(moduleKey) { + key = moduleKey + ":" + key + } else { + return fmt.Errorf("parameter module key %q does not exist", moduleKey) + } + } + + bz, err := amino.MarshalJSON(param) if err != nil { - panic(err) + return err } -} -func (pk ParamsKeeper) get(ctx sdk.Context, key string, ptr interface{}) { stor := ctx.Store(pk.key) - bz := stor.Get([]byte(key)) - err := amino.UnmarshalJSON(bz, ptr) + vk := ValueStoreKey(key) + stor.Set(vk, bz) + return nil +} + +func (pk ParamsKeeper) getIfExists(ctx sdk.Context, key string, ptr interface{}) { + module, rawKey := parsePrefix(key) + _, err := pk.GetParams(ctx, module, rawKey, ptr) if err != nil { panic(err) } } +// set is only used for setting invidual key that func (pk ParamsKeeper) set(ctx sdk.Context, key string, value interface{}) { - stor := ctx.Store(pk.key) - bz, err := amino.MarshalJSON(value) + module, rawKey := parsePrefix(key) + if module != "" { + kpr := pk.GetRegisteredKeeper(module) + if kpr != nil { + kpr.WillSetParam(ctx, rawKey, value) + return + } + } + err := pk.SetParams(ctx, "", key, value) if err != nil { panic(err) } - stor.Set([]byte(key), bz) +} + +func parsePrefix(key string) (prefix, rawKey string) { + // Look for the first colon. + colonIndex := strings.Index(key, ":") + + if colonIndex != -1 { + // colon found: the key has a module prefix. + prefix = key[:colonIndex] + rawKey = key[colonIndex+1:] + + return + } + return "", key } func checkSuffix(key, expectedSuffix string) { diff --git a/tm2/pkg/sdk/params/keeper_test.go b/tm2/pkg/sdk/params/keeper_test.go index aedfaa9d5a3..4c97aa129f7 100644 --- a/tm2/pkg/sdk/params/keeper_test.go +++ b/tm2/pkg/sdk/params/keeper_test.go @@ -114,22 +114,14 @@ func TestKeeper_internal(t *testing.T) { for i, kv := range kvs { require.NotPanics(t, func() { keeper.getIfExists(ctx, "invalid", kv.ptr) }, "keeper.GetIfExists panics when no value exists, tc #%d", i) require.Equal(t, kv.zero, indirect(kv.ptr), "keeper.GetIfExists unmarshalls when no value exists, tc #%d", i) - require.Panics(t, func() { keeper.get(ctx, "invalid", kv.ptr) }, "invalid keeper.Get not panics when no value exists, tc #%d", i) - require.Equal(t, kv.zero, indirect(kv.ptr), "invalid keeper.Get unmarshalls when no value exists, tc #%d", i) - require.NotPanics(t, func() { keeper.getIfExists(ctx, kv.key, kv.ptr) }, "keeper.GetIfExists panics, tc #%d", i) require.Equal(t, kv.param, indirect(kv.ptr), "stored param not equal, tc #%d", i) - require.NotPanics(t, func() { keeper.get(ctx, kv.key, kv.ptr) }, "keeper.Get panics, tc #%d", i) - require.Equal(t, kv.param, indirect(kv.ptr), "stored param not equal, tc #%d", i) - - require.Panics(t, func() { keeper.get(ctx, "invalid", kv.ptr) }, "invalid keeper.Get not panics when no value exists, tc #%d", i) - require.Equal(t, kv.param, indirect(kv.ptr), "invalid keeper.Get unmarshalls when no value existt, tc #%d", i) - - require.Panics(t, func() { keeper.get(ctx, kv.key, nil) }, "invalid keeper.Get not panics when the pointer is nil, tc #%d", i) + require.Panics(t, func() { keeper.getIfExists(ctx, kv.key, nil) }, "invalid keeper.Get not panics when the pointer is nil, tc #%d", i) } for i, kv := range kvs { - bz := store.Get([]byte(kv.key)) + vk := ValueStoreKey(kv.key) + bz := store.Get(vk) require.NotNil(t, bz, "store.Get() returns nil, tc #%d", i) err := amino.UnmarshalJSON(bz, kv.ptr) require.NoError(t, err, "cdc.UnmarshalJSON() returns error, tc #%d", i) @@ -152,12 +144,12 @@ func TestGetAndSetParams(t *testing.T) { keeper := env.keeper // SetParams a := Params{p1: 1, p2: "a"} - err := keeper.SetParams(ctx, ModuleName, a) + err := keeper.SetParams(ctx, "", "p", a) require.NoError(t, err) // GetParams a1 := Params{} - _, err1 := keeper.GetParams(ctx, ModuleName, &a1) + _, err1 := keeper.GetParams(ctx, "", "p", &a1) require.NoError(t, err1) require.True(t, amino.DeepEqual(a, a1), "a and a1 should equal") } diff --git a/tm2/pkg/sdk/params/params_test.go b/tm2/pkg/sdk/params/params_test.go new file mode 100644 index 00000000000..4270fd65c55 --- /dev/null +++ b/tm2/pkg/sdk/params/params_test.go @@ -0,0 +1,63 @@ +package params + +import ( + "testing" + + "github.com/gnolang/gno/tm2/pkg/amino" + "github.com/stretchr/testify/require" +) + +// newModuleParams defines the parameters with updated fields for a module. +type newModuleParams struct { + LimitedTokens []string `json:"limited_tokens" yaml:"limited_tokens"` + Max uint64 `json:"max" yaml:"max"` +} + +// oldModuleParams defines the parameters for a module. +type oldModuleParams struct { + LimitedTokens []string `json:"limited_tokens" yaml:"limited_tokens"` +} + +// newOldModuleParams creates a new oldModuleParams object. +func newOldModuleParams(tokens []string) oldModuleParams { + return oldModuleParams{ + LimitedTokens: tokens, + } +} + +func TestBackwardCompatibility(t *testing.T) { + oldParams := newOldModuleParams([]string{"token1", "token2"}) + + // Serialize oldModuleParams to JSON + bz, err := amino.MarshalJSON(oldParams) + require.NoError(t, err, "Failed to marshal oldModuleParams") + + t.Logf("Serialized oldModuleParams: %s\n", bz) + + // Deserialize JSON into newModuleParams + newParams := &newModuleParams{} + err = amino.UnmarshalJSON(bz, newParams) + require.NoError(t, err, "Failed to unmarshal into newModuleParams") + + // Validate compatibility + require.Equal(t, oldParams.LimitedTokens, newParams.LimitedTokens, "LimitedTokens mismatch") + require.Equal(t, uint64(0), newParams.Max, "Max should default to 0 in backward compatibility") +} + +func TestForwardCompatibility(t *testing.T) { + newParams := newModuleParams{LimitedTokens: []string{"token1", "token2"}, Max: 10} + + // Serialize newModuleParams to JSON + bz, err := amino.MarshalJSON(newParams) + require.NoError(t, err, "Failed to marshal newModuleParams") + + t.Logf("Serialized newModuleParams: %s\n", bz) + + // Deserialize JSON into oldModuleParams + oldParams := &oldModuleParams{} + err = amino.UnmarshalJSON(bz, oldParams) + require.NoError(t, err, "Failed to unmarshal into oldModuleParams") + + // Validate compatibility + require.Equal(t, newParams.LimitedTokens, oldParams.LimitedTokens, "LimitedTokens mismatch") +} diff --git a/tm2/pkg/sdk/params/test_common.go b/tm2/pkg/sdk/params/test_common.go index 8243ee867de..d71b325fe80 100644 --- a/tm2/pkg/sdk/params/test_common.go +++ b/tm2/pkg/sdk/params/test_common.go @@ -6,6 +6,7 @@ import ( "github.com/gnolang/gno/tm2/pkg/db/memdb" "github.com/gnolang/gno/tm2/pkg/log" "github.com/gnolang/gno/tm2/pkg/sdk" + "github.com/gnolang/gno/tm2/pkg/store" "github.com/gnolang/gno/tm2/pkg/store/iavl" ) @@ -23,8 +24,9 @@ func setupTestEnv() testEnv { ms.MountStoreWithDB(paramsCapKey, iavl.StoreConstructor, db) ms.LoadLatestVersion() - prefix := "params_test" - keeper := NewParamsKeeper(paramsCapKey, prefix) + paramk := NewParamsKeeper(paramsCapKey) + dk := NewDummyKeeper(paramk) + paramk.Register(dk.GetParamfulKey(), dk) ctx := sdk.NewContext(sdk.RunTxModeDeliver, ms, &bft.Header{Height: 1, ChainID: "test-chain-id"}, log.NewNoopLogger()) // XXX: context key? @@ -42,5 +44,25 @@ func setupTestEnv() testEnv { }) stor := ctx.Store(paramsCapKey) - return testEnv{ctx: ctx, store: stor, keeper: keeper} + return testEnv{ctx: ctx, store: stor, keeper: paramk} +} + +const dummyModuleName = "params_test" + +type DummyKeeper struct { + prmk ParamsKeeper +} + +func NewDummyKeeper(paramk ParamsKeeper) DummyKeeper { + return DummyKeeper{ + prmk: paramk, + } +} + +func (dk DummyKeeper) GetParamfulKey() string { + return dummyModuleName +} + +func (dk DummyKeeper) WillSetParam(ctx sdk.Context, key string, value interface{}) { + dk.prmk.SetParams(ctx, dummyModuleName, key, value) } diff --git a/tm2/pkg/std/account.go b/tm2/pkg/std/account.go index c70f43d22e9..e505f6244df 100644 --- a/tm2/pkg/std/account.go +++ b/tm2/pkg/std/account.go @@ -38,6 +38,10 @@ type Account interface { String() string } +type AccountRestricter interface { + IsUnrestricted() bool +} + //---------------------------------------- // BaseAccount diff --git a/tm2/pkg/std/coin.go b/tm2/pkg/std/coin.go index fba20a5ba78..d1d20e110f8 100644 --- a/tm2/pkg/std/coin.go +++ b/tm2/pkg/std/coin.go @@ -63,7 +63,7 @@ func (coin Coin) String() string { // validate returns an error if the Coin has a negative amount or if // the denom is invalid. func validate(denom string, amount int64) error { - if err := validateDenom(denom); err != nil { + if err := ValidateDenom(denom); err != nil { return err } @@ -229,7 +229,7 @@ func (coins Coins) IsValid() bool { case 0: return true case 1: - if err := validateDenom(coins[0].Denom); err != nil { + if err := ValidateDenom(coins[0].Denom); err != nil { return false } return coins[0].IsPositive() @@ -328,6 +328,21 @@ func (coins Coins) AddUnsafe(coinsB Coins) Coins { } } +// ContainOneOfDenom check if a Coins instance contains a denom in the provided denomos +func (coins Coins) ContainOneOfDenom(denoms map[string]struct{}) bool { + if len(denoms) == 0 { + return false + } + + for _, coin := range coins { + if _, ok := denoms[coin.Denom]; ok && coin.IsPositive() { + return true + } + } + + return false +} + // DenomsSubsetOf returns true if receiver's denom set // is subset of coinsB's denoms. func (coins Coins) DenomsSubsetOf(coinsB Coins) bool { @@ -623,7 +638,7 @@ var ( reCoin = regexp.MustCompile(fmt.Sprintf(`^(%s)%s(%s)$`, reAmt, reSpc, reDnmString)) ) -func validateDenom(denom string) error { +func ValidateDenom(denom string) error { if !reDnm.MatchString(denom) { return fmt.Errorf("invalid denom: %s", denom) } @@ -631,7 +646,7 @@ func validateDenom(denom string) error { } func mustValidateDenom(denom string) { - if err := validateDenom(denom); err != nil { + if err := ValidateDenom(denom); err != nil { panic(err) } } @@ -661,7 +676,7 @@ func ParseCoin(coinStr string) (coin Coin, err error) { return Coin{}, errors.Wrapf(err, "failed to parse coin amount: %s", amountStr) } - if err := validateDenom(denomStr); err != nil { + if err := ValidateDenom(denomStr); err != nil { return Coin{}, fmt.Errorf("invalid denom cannot contain upper case characters or spaces: %w", err) } diff --git a/tm2/pkg/std/coin_test.go b/tm2/pkg/std/coin_test.go index 33ee7425a0c..ba3c8f49c54 100644 --- a/tm2/pkg/std/coin_test.go +++ b/tm2/pkg/std/coin_test.go @@ -730,3 +730,23 @@ func TestMarshalJSONCoins(t *testing.T) { }) } } + +func TestContainOneOfDenom(t *testing.T) { + restrictList := map[string]struct{}{ + "baz": {}, + "foo": {}, + } + amt := Coins{ + {"foo", int64(1)}, + {"bar", int64(1)}, + } + require.True(t, amt.ContainOneOfDenom(restrictList)) + + zero := Coins{ + {"foo", int64(0)}, + {"bar", int64(1)}, + } + + // only return true when the value is posible + require.False(t, zero.ContainOneOfDenom(restrictList)) +} diff --git a/tm2/pkg/std/errors.go b/tm2/pkg/std/errors.go index 715b27b3eb4..48bd93be1de 100644 --- a/tm2/pkg/std/errors.go +++ b/tm2/pkg/std/errors.go @@ -14,43 +14,45 @@ func (abciError) AssertABCIError() {} type InternalError struct{ abciError } type ( - TxDecodeError struct{ abciError } - InvalidSequenceError struct{ abciError } - UnauthorizedError struct{ abciError } - InsufficientFundsError struct{ abciError } - UnknownRequestError struct{ abciError } - InvalidAddressError struct{ abciError } - UnknownAddressError struct{ abciError } - InvalidPubKeyError struct{ abciError } - InsufficientCoinsError struct{ abciError } - InvalidCoinsError struct{ abciError } - InvalidGasWantedError struct{ abciError } - OutOfGasError struct{ abciError } - MemoTooLargeError struct{ abciError } - InsufficientFeeError struct{ abciError } - TooManySignaturesError struct{ abciError } - NoSignaturesError struct{ abciError } - GasOverflowError struct{ abciError } + TxDecodeError struct{ abciError } + InvalidSequenceError struct{ abciError } + UnauthorizedError struct{ abciError } + InsufficientFundsError struct{ abciError } + UnknownRequestError struct{ abciError } + InvalidAddressError struct{ abciError } + UnknownAddressError struct{ abciError } + InvalidPubKeyError struct{ abciError } + InsufficientCoinsError struct{ abciError } + InvalidCoinsError struct{ abciError } + InvalidGasWantedError struct{ abciError } + OutOfGasError struct{ abciError } + MemoTooLargeError struct{ abciError } + InsufficientFeeError struct{ abciError } + TooManySignaturesError struct{ abciError } + NoSignaturesError struct{ abciError } + GasOverflowError struct{ abciError } + RestrictedTransferError struct{ abciError } ) -func (e InternalError) Error() string { return "internal error" } -func (e TxDecodeError) Error() string { return "tx decode error" } -func (e InvalidSequenceError) Error() string { return "invalid sequence error" } -func (e UnauthorizedError) Error() string { return "unauthorized error" } -func (e InsufficientFundsError) Error() string { return "insufficient funds error" } -func (e UnknownRequestError) Error() string { return "unknown request error" } -func (e InvalidAddressError) Error() string { return "invalid address error" } -func (e UnknownAddressError) Error() string { return "unknown address error" } -func (e InvalidPubKeyError) Error() string { return "invalid pubkey error" } -func (e InsufficientCoinsError) Error() string { return "insufficient coins error" } -func (e InvalidCoinsError) Error() string { return "invalid coins error" } -func (e InvalidGasWantedError) Error() string { return "invalid gas wanted" } -func (e OutOfGasError) Error() string { return "out of gas error" } -func (e MemoTooLargeError) Error() string { return "memo too large error" } -func (e InsufficientFeeError) Error() string { return "insufficient fee error" } -func (e TooManySignaturesError) Error() string { return "too many signatures error" } -func (e NoSignaturesError) Error() string { return "no signatures error" } -func (e GasOverflowError) Error() string { return "gas overflow error" } +func (e InternalError) Error() string { return "internal error" } +func (e TxDecodeError) Error() string { return "tx decode error" } +func (e InvalidSequenceError) Error() string { return "invalid sequence error" } +func (e UnauthorizedError) Error() string { return "unauthorized error" } +func (e InsufficientFundsError) Error() string { return "insufficient funds error" } +func (e UnknownRequestError) Error() string { return "unknown request error" } +func (e InvalidAddressError) Error() string { return "invalid address error" } +func (e UnknownAddressError) Error() string { return "unknown address error" } +func (e InvalidPubKeyError) Error() string { return "invalid pubkey error" } +func (e InsufficientCoinsError) Error() string { return "insufficient coins error" } +func (e InvalidCoinsError) Error() string { return "invalid coins error" } +func (e InvalidGasWantedError) Error() string { return "invalid gas wanted" } +func (e OutOfGasError) Error() string { return "out of gas error" } +func (e MemoTooLargeError) Error() string { return "memo too large error" } +func (e InsufficientFeeError) Error() string { return "insufficient fee error" } +func (e TooManySignaturesError) Error() string { return "too many signatures error" } +func (e NoSignaturesError) Error() string { return "no signatures error" } +func (e GasOverflowError) Error() string { return "gas overflow error" } +func (e RestrictedTransferError) Error() string { return "restricted token transfer error" } // NOTE also update pkg/std/package.go registrations. diff --git a/tm2/pkg/std/package.go b/tm2/pkg/std/package.go index a1aadc17cb6..471c32f3f5e 100644 --- a/tm2/pkg/std/package.go +++ b/tm2/pkg/std/package.go @@ -36,4 +36,5 @@ var Package = amino.RegisterPackage(amino.NewPackage( TooManySignaturesError{}, "TooManySignaturesError", NoSignaturesError{}, "NoSignaturesError", GasOverflowError{}, "GasOverflowError", + RestrictedTransferError{}, "RestrictedTransferError", ))