diff --git a/gno.land/cmd/gnoland/testdata/params.txtar b/gno.land/cmd/gnoland/testdata/params.txtar new file mode 100644 index 00000000000..30363aa6369 --- /dev/null +++ b/gno.land/cmd/gnoland/testdata/params.txtar @@ -0,0 +1,65 @@ +# test for https://github.com/gnolang/gno/pull/2920 + +gnoland start + +# query before adding the package +gnokey query params/vm/gno.land/r/sys/setter.foo.string +stdout 'data: $' +gnokey query params/vm/gno.land/r/sys/setter.bar.bool +stdout 'data: $' +gnokey query params/vm/gno.land/r/sys/setter.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 + +# query after adding the package, but before setting values +gnokey query params/vm/gno.land/r/sys/setter.foo.string +stdout 'data: $' +gnokey query params/vm/gno.land/r/sys/setter.bar.bool +stdout 'data: $' +gnokey query params/vm/gno.land/r/sys/setter.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 +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 +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 +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 +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 +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 +stdout 'data: "31337"' + +-- setter/setter.gno -- +package setter + +import ( + "std" +) + +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) } diff --git a/gno.land/pkg/gnoland/app.go b/gno.land/pkg/gnoland/app.go index ca746dbe386..e784f2148aa 100644 --- a/gno.land/pkg/gnoland/app.go +++ b/gno.land/pkg/gnoland/app.go @@ -19,6 +19,7 @@ import ( "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/std" "github.com/gnolang/gno/tm2/pkg/store" "github.com/gnolang/gno/tm2/pkg/store/dbadapter" @@ -87,12 +88,13 @@ func NewAppWithOptions(cfg *AppOptions) (abci.Application, error) { // Construct keepers. acctKpr := auth.NewAccountKeeper(mainKey, ProtoGnoAccount) bankKpr := bank.NewBankKeeper(acctKpr) - vmk := vm.NewVMKeeper(baseKey, mainKey, acctKpr, bankKpr) + paramsKpr := params.NewParamsKeeper(mainKey, "vm") + vmk := vm.NewVMKeeper(baseKey, mainKey, acctKpr, bankKpr, paramsKpr) // Set InitChainer icc := cfg.InitChainerConfig icc.baseApp = baseApp - icc.acctKpr, icc.bankKpr, icc.vmKpr = acctKpr, bankKpr, vmk + icc.acctKpr, icc.bankKpr, icc.vmKpr, icc.paramsKpr = acctKpr, bankKpr, vmk, paramsKpr baseApp.SetInitChainer(icc.InitChainer) // Set AnteHandler @@ -147,6 +149,7 @@ func NewAppWithOptions(cfg *AppOptions) (abci.Application, error) { // Set a handler Route. baseApp.Router().AddRoute("auth", auth.NewHandler(acctKpr)) baseApp.Router().AddRoute("bank", bank.NewHandler(bankKpr)) + baseApp.Router().AddRoute("params", params.NewHandler(paramsKpr)) baseApp.Router().AddRoute("vm", vm.NewHandler(vmk)) // Load latest version. @@ -224,10 +227,11 @@ type InitChainerConfig struct { // These fields are passed directly by NewAppWithOptions, and should not be // configurable by end-users. - baseApp *sdk.BaseApp - vmKpr vm.VMKeeperI - acctKpr auth.AccountKeeperI - bankKpr bank.BankKeeperI + baseApp *sdk.BaseApp + vmKpr vm.VMKeeperI + acctKpr auth.AccountKeeperI + bankKpr bank.BankKeeperI + paramsKpr params.ParamsKeeperI } // InitChainer is the function that can be used as a [sdk.InitChainer]. diff --git a/gno.land/pkg/sdk/vm/builtins.go b/gno.land/pkg/sdk/vm/builtins.go index d4d6b6032b2..161e459873d 100644 --- a/gno.land/pkg/sdk/vm/builtins.go +++ b/gno.land/pkg/sdk/vm/builtins.go @@ -55,3 +55,26 @@ func (bnk *SDKBanker) RemoveCoin(b32addr crypto.Bech32Address, denom string, amo panic(err) } } + +// ---------------------------------------- +// SDKParams + +type SDKParams struct { + vmk *VMKeeper + ctx sdk.Context +} + +func NewSDKParams(vmk *VMKeeper, ctx sdk.Context) *SDKParams { + return &SDKParams{ + vmk: vmk, + 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) SetBytes(key string, value []byte) { prm.vmk.prmk.SetBytes(prm.ctx, key, value) } diff --git a/gno.land/pkg/sdk/vm/common_test.go b/gno.land/pkg/sdk/vm/common_test.go index 66975fba923..7380d3e0f72 100644 --- a/gno.land/pkg/sdk/vm/common_test.go +++ b/gno.land/pkg/sdk/vm/common_test.go @@ -11,6 +11,7 @@ import ( "github.com/gnolang/gno/tm2/pkg/sdk" authm "github.com/gnolang/gno/tm2/pkg/sdk/auth" bankm "github.com/gnolang/gno/tm2/pkg/sdk/bank" + paramsm "github.com/gnolang/gno/tm2/pkg/sdk/params" "github.com/gnolang/gno/tm2/pkg/std" "github.com/gnolang/gno/tm2/pkg/store" "github.com/gnolang/gno/tm2/pkg/store/dbadapter" @@ -47,7 +48,8 @@ func _setupTestEnv(cacheStdlibs bool) testEnv { ctx := sdk.NewContext(sdk.RunTxModeDeliver, ms, &bft.Header{ChainID: "test-chain-id"}, log.NewNoopLogger()) acck := authm.NewAccountKeeper(iavlCapKey, std.ProtoBaseAccount) bank := bankm.NewBankKeeper(acck) - vmk := NewVMKeeper(baseCapKey, iavlCapKey, acck, bank) + prmk := paramsm.NewParamsKeeper(iavlCapKey, "params") + vmk := NewVMKeeper(baseCapKey, iavlCapKey, acck, bank, prmk) mcw := ms.MultiCacheWrap() vmk.Initialize(log.NewNoopLogger(), mcw) diff --git a/gno.land/pkg/sdk/vm/gas_test.go b/gno.land/pkg/sdk/vm/gas_test.go index 4171b1cdbc3..53809a7f223 100644 --- a/gno.land/pkg/sdk/vm/gas_test.go +++ b/gno.land/pkg/sdk/vm/gas_test.go @@ -74,7 +74,7 @@ func TestAddPkgDeliverTx(t *testing.T) { assert.True(t, res.IsOK()) // NOTE: let's try to keep this bellow 100_000 :) - assert.Equal(t, int64(92825), gasDeliver) + assert.Equal(t, int64(93825), gasDeliver) } // Enough gas for a failed transaction. diff --git a/gno.land/pkg/sdk/vm/keeper.go b/gno.land/pkg/sdk/vm/keeper.go index f069cce3723..ef1705c7ae9 100644 --- a/gno.land/pkg/sdk/vm/keeper.go +++ b/gno.land/pkg/sdk/vm/keeper.go @@ -23,6 +23,7 @@ import ( "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/std" "github.com/gnolang/gno/tm2/pkg/store" "github.com/gnolang/gno/tm2/pkg/store/dbadapter" @@ -59,6 +60,7 @@ type VMKeeper struct { iavlKey store.StoreKey acck auth.AccountKeeper bank bank.BankKeeper + prmk params.ParamsKeeper // cached, the DeliverTx persistent state. gnoStore gno.Store @@ -70,13 +72,14 @@ func NewVMKeeper( iavlKey store.StoreKey, acck auth.AccountKeeper, bank bank.BankKeeper, + prmk params.ParamsKeeper, ) *VMKeeper { - // TODO: create an Options struct to avoid too many constructor parameters vmk := &VMKeeper{ baseKey: baseKey, iavlKey: iavlKey, acck: acck, bank: bank, + prmk: prmk, } return vmk } @@ -222,9 +225,15 @@ func (vm *VMKeeper) getGnoTransactionStore(ctx sdk.Context) gno.TransactionStore // Namespace can be either a user or crypto address. var reNamespace = regexp.MustCompile(`^gno.land/(?:r|p)/([\.~_a-zA-Z0-9]+)`) +const ( + sysUsersPkgParamKey = "vm/gno.land/r/sys/params.string" + sysUsersPkgDefault = "gno.land/r/sys/users" +) + // checkNamespacePermission check if the user as given has correct permssion to on the given pkg path func (vm *VMKeeper) checkNamespacePermission(ctx sdk.Context, creator crypto.Address, pkgPath string) error { - const sysUsersPkg = "gno.land/r/sys/users" + sysUsersPkg := sysUsersPkgDefault + vm.prmk.GetString(ctx, sysUsersPkgParamKey, &sysUsersPkg) store := vm.getGnoTransactionStore(ctx) @@ -258,6 +267,7 @@ func (vm *VMKeeper) checkNamespacePermission(ctx sdk.Context, creator crypto.Add OrigPkgAddr: pkgAddr.Bech32(), // XXX: should we remove the banker ? Banker: NewSDKBanker(vm, ctx), + Params: NewSDKParams(vm, ctx), EventLogger: ctx.EventLogger(), } @@ -358,6 +368,7 @@ func (vm *VMKeeper) AddPackage(ctx sdk.Context, msg MsgAddPackage) (err error) { OrigSendSpent: new(std.Coins), OrigPkgAddr: pkgAddr.Bech32(), Banker: NewSDKBanker(vm, ctx), + Params: NewSDKParams(vm, ctx), EventLogger: ctx.EventLogger(), } // Parse and run the files, construct *PV. @@ -458,6 +469,7 @@ func (vm *VMKeeper) Call(ctx sdk.Context, msg MsgCall) (res string, err error) { OrigSendSpent: new(std.Coins), OrigPkgAddr: pkgAddr.Bech32(), Banker: NewSDKBanker(vm, ctx), + Params: NewSDKParams(vm, ctx), EventLogger: ctx.EventLogger(), } // Construct machine and evaluate. @@ -556,6 +568,7 @@ func (vm *VMKeeper) Run(ctx sdk.Context, msg MsgRun) (res string, err error) { OrigSendSpent: new(std.Coins), OrigPkgAddr: pkgAddr.Bech32(), Banker: NewSDKBanker(vm, ctx), + Params: NewSDKParams(vm, ctx), EventLogger: ctx.EventLogger(), } // Parse and run the files, construct *PV. @@ -715,6 +728,7 @@ func (vm *VMKeeper) QueryEval(ctx sdk.Context, pkgPath string, expr string) (res // OrigSendSpent: nil, OrigPkgAddr: pkgAddr.Bech32(), Banker: NewSDKBanker(vm, ctx), // safe as long as ctx is a fork to be discarded. + Params: NewSDKParams(vm, ctx), EventLogger: ctx.EventLogger(), } m := gno.NewMachineWithOptions( @@ -781,6 +795,7 @@ func (vm *VMKeeper) QueryEvalString(ctx sdk.Context, pkgPath string, expr string // OrigSendSpent: nil, OrigPkgAddr: pkgAddr.Bech32(), Banker: NewSDKBanker(vm, ctx), // safe as long as ctx is a fork to be discarded. + Params: NewSDKParams(vm, 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 f6c6b87419d..c6d8e3f5fa0 100644 --- a/gno.land/pkg/sdk/vm/keeper_test.go +++ b/gno.land/pkg/sdk/vm/keeper_test.go @@ -298,6 +298,60 @@ func Echo(msg string) string { assert.Error(t, err) } +// Using x/params from a realm. +func TestVMKeeperParams(t *testing.T) { + env := setupTestEnv() + ctx := env.vmk.MakeGnoTransactionStore(env.ctx) + + // Give "addr1" some gnots. + addr := crypto.AddressFromPreimage([]byte("addr1")) + acc := env.acck.NewAccountWithAddress(ctx, addr) + env.acck.SetAccount(ctx, acc) + env.bank.SetCoins(ctx, addr, std.MustParseCoins(coinsString)) + // env.prmk. + assert.True(t, env.bank.GetCoins(ctx, addr).IsEqual(std.MustParseCoins(coinsString))) + + // Create test package. + files := []*std.MemFile{ + {"init.gno", ` +package test + +import "std" + +func init() { + std.SetParamString("foo.string", "foo1") +} + +func Do() string { + std.SetParamInt64("bar.int64", int64(1337)) + std.SetParamString("foo.string", "foo2") // override init + + 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" + msg1 := NewMsgAddPackage(addr, pkgPath, files) + err := env.vmk.AddPackage(ctx, msg1) + assert.NoError(t, err) + + // Run Echo function. + coins := std.MustParseCoins(ugnot.ValueString(9_000_000)) + msg2 := NewMsgCall(addr, coins, pkgPath, "Do", []string{}) + + res, err := env.vmk.Call(ctx, msg2) + assert.NoError(t, err) + _ = res + expected := fmt.Sprintf("(\"%s\" string)\n\n", "XXX") // XXX: return something more useful + assert.Equal(t, expected, res) + + 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) + assert.Equal(t, "foo2", foo) + assert.Equal(t, int64(1337), bar) +} + // Assign admin as OrigCaller on deploying the package. func TestVMKeeperOrigCallerInit(t *testing.T) { env := setupTestEnv() @@ -320,7 +374,7 @@ import "std" var admin std.Address func init() { - admin = std.GetOrigCaller() + admin = std.GetOrigCaller() } func Echo(msg string) string { diff --git a/gnovm/stdlibs/generated.go b/gnovm/stdlibs/generated.go index 7693e9d6e70..b0788fc6d1b 100644 --- a/gnovm/stdlibs/generated.go +++ b/gnovm/stdlibs/generated.go @@ -720,6 +720,136 @@ var nativeFuncs = [...]NativeFunc{ )) }, }, + { + "std", + "setParamString", + []gno.FieldTypeExpr{ + {Name: gno.N("p0"), Type: gno.X("string")}, + {Name: gno.N("p1"), 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() + ) + + gno.Gno2GoValue(b.GetPointerTo(nil, gno.NewValuePathBlock(1, 0, "")).TV, rp0) + gno.Gno2GoValue(b.GetPointerTo(nil, gno.NewValuePathBlock(1, 1, "")).TV, rp1) + + libs_std.X_setParamString( + m, + p0, p1) + }, + }, + { + "std", + "setParamBool", + []gno.FieldTypeExpr{ + {Name: gno.N("p0"), Type: gno.X("string")}, + {Name: gno.N("p1"), Type: gno.X("bool")}, + }, + []gno.FieldTypeExpr{}, + true, + func(m *gno.Machine) { + b := m.LastBlock() + var ( + p0 string + rp0 = reflect.ValueOf(&p0).Elem() + p1 bool + rp1 = reflect.ValueOf(&p1).Elem() + ) + + gno.Gno2GoValue(b.GetPointerTo(nil, gno.NewValuePathBlock(1, 0, "")).TV, rp0) + gno.Gno2GoValue(b.GetPointerTo(nil, gno.NewValuePathBlock(1, 1, "")).TV, rp1) + + libs_std.X_setParamBool( + m, + p0, p1) + }, + }, + { + "std", + "setParamInt64", + []gno.FieldTypeExpr{ + {Name: gno.N("p0"), Type: gno.X("string")}, + {Name: gno.N("p1"), Type: gno.X("int64")}, + }, + []gno.FieldTypeExpr{}, + true, + func(m *gno.Machine) { + b := m.LastBlock() + var ( + p0 string + rp0 = reflect.ValueOf(&p0).Elem() + p1 int64 + rp1 = reflect.ValueOf(&p1).Elem() + ) + + gno.Gno2GoValue(b.GetPointerTo(nil, gno.NewValuePathBlock(1, 0, "")).TV, rp0) + gno.Gno2GoValue(b.GetPointerTo(nil, gno.NewValuePathBlock(1, 1, "")).TV, rp1) + + libs_std.X_setParamInt64( + m, + p0, p1) + }, + }, + { + "std", + "setParamUint64", + []gno.FieldTypeExpr{ + {Name: gno.N("p0"), Type: gno.X("string")}, + {Name: gno.N("p1"), Type: gno.X("uint64")}, + }, + []gno.FieldTypeExpr{}, + true, + func(m *gno.Machine) { + b := m.LastBlock() + var ( + p0 string + rp0 = reflect.ValueOf(&p0).Elem() + p1 uint64 + rp1 = reflect.ValueOf(&p1).Elem() + ) + + gno.Gno2GoValue(b.GetPointerTo(nil, gno.NewValuePathBlock(1, 0, "")).TV, rp0) + gno.Gno2GoValue(b.GetPointerTo(nil, gno.NewValuePathBlock(1, 1, "")).TV, rp1) + + libs_std.X_setParamUint64( + m, + p0, p1) + }, + }, + { + "std", + "setParamBytes", + []gno.FieldTypeExpr{ + {Name: gno.N("p0"), Type: gno.X("string")}, + {Name: gno.N("p1"), Type: gno.X("[]byte")}, + }, + []gno.FieldTypeExpr{}, + true, + func(m *gno.Machine) { + b := m.LastBlock() + var ( + p0 string + rp0 = reflect.ValueOf(&p0).Elem() + p1 []byte + rp1 = reflect.ValueOf(&p1).Elem() + ) + + gno.Gno2GoValue(b.GetPointerTo(nil, gno.NewValuePathBlock(1, 0, "")).TV, rp0) + gno.Gno2GoValue(b.GetPointerTo(nil, gno.NewValuePathBlock(1, 1, "")).TV, rp1) + + libs_std.X_setParamBytes( + m, + p0, p1) + }, + }, { "testing", "unixNano", diff --git a/gnovm/stdlibs/std/context.go b/gnovm/stdlibs/std/context.go index ff5c91a14eb..a0dafe5dc44 100644 --- a/gnovm/stdlibs/std/context.go +++ b/gnovm/stdlibs/std/context.go @@ -18,6 +18,7 @@ type ExecContext struct { OrigSend std.Coins OrigSendSpent *std.Coins // mutable Banker BankerInterface + Params ParamsInterface EventLogger *sdk.EventLogger } diff --git a/gnovm/stdlibs/std/params.gno b/gnovm/stdlibs/std/params.gno new file mode 100644 index 00000000000..ce400270cda --- /dev/null +++ b/gnovm/stdlibs/std/params.gno @@ -0,0 +1,13 @@ +package std + +func setParamString(key string, val string) +func setParamBool(key string, val bool) +func setParamInt64(key string, val int64) +func setParamUint64(key string, val uint64) +func setParamBytes(key string, val []byte) + +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) } +func SetParamUint64(key string, val uint64) { setParamUint64(key, val) } +func SetParamBytes(key string, val []byte) { setParamBytes(key, val) } diff --git a/gnovm/stdlibs/std/params.go b/gnovm/stdlibs/std/params.go new file mode 100644 index 00000000000..e21bd9912dd --- /dev/null +++ b/gnovm/stdlibs/std/params.go @@ -0,0 +1,72 @@ +package std + +import ( + "fmt" + "strings" + "unicode" + + gno "github.com/gnolang/gno/gnovm/pkg/gnolang" +) + +// ParamsInterface 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) +} + +func X_setParamString(m *gno.Machine, key, val string) { + pk := pkey(m, key, "string") + GetContext(m).Params.SetString(pk, val) +} + +func X_setParamBool(m *gno.Machine, key string, val bool) { + pk := pkey(m, key, "bool") + GetContext(m).Params.SetBool(pk, val) +} + +func X_setParamInt64(m *gno.Machine, key string, val int64) { + pk := pkey(m, key, "int64") + GetContext(m).Params.SetInt64(pk, val) +} + +func X_setParamUint64(m *gno.Machine, key string, val uint64) { + pk := pkey(m, key, "uint64") + GetContext(m).Params.SetUint64(pk, val) +} + +func X_setParamBytes(m *gno.Machine, key string, val []byte) { + pk := pkey(m, key, "bytes") + GetContext(m).Params.SetBytes(pk, val) +} + +func pkey(m *gno.Machine, key string, kind string) string { + // validate key. + untypedKey := strings.TrimSuffix(key, "."+kind) + if key == untypedKey { + m.Panic(typedString("invalid param key: " + key)) + } + + if len(key) == 0 { + m.Panic(typedString("empty param key")) + } + first := rune(key[0]) + if !unicode.IsLetter(first) && first != '_' { + m.Panic(typedString("invalid param key: " + key)) + } + for _, char := range untypedKey[1:] { + if !unicode.IsLetter(char) && !unicode.IsDigit(char) && char != '_' { + m.Panic(typedString("invalid param key: " + key)) + } + } + + // decorate key with realm and type. + _, rlmPath := currentRealm(m) + return fmt.Sprintf("%s.%s", rlmPath, key) +} diff --git a/misc/genstd/Makefile b/misc/genstd/Makefile new file mode 100644 index 00000000000..2022a6cc2b4 --- /dev/null +++ b/misc/genstd/Makefile @@ -0,0 +1,6 @@ +run: + cd ../../gnovm/stdlibs && go run ../../misc/genstd + cd ../../gnovm/tests/stdlibs && go run ../../../misc/genstd + +test: + go test -v . diff --git a/tm2/pkg/sdk/auth/keeper.go b/tm2/pkg/sdk/auth/keeper.go index e43b5389844..7669b8ace73 100644 --- a/tm2/pkg/sdk/auth/keeper.go +++ b/tm2/pkg/sdk/auth/keeper.go @@ -31,11 +31,6 @@ func NewAccountKeeper( } } -// Logger returns a module-specific logger. -func (ak AccountKeeper) Logger(ctx sdk.Context) *slog.Logger { - return ctx.Logger().With("module", fmt.Sprintf("auth")) -} - // NewAccountWithAddress implements AccountKeeper. func (ak AccountKeeper) NewAccountWithAddress(ctx sdk.Context, addr crypto.Address) std.Account { acc := ak.proto() @@ -53,7 +48,12 @@ func (ak AccountKeeper) NewAccountWithAddress(ctx sdk.Context, addr crypto.Addre return acc } -// GetAccount implements AccountKeeper. +// Logger returns a module-specific logger. +func (ak AccountKeeper) Logger(ctx sdk.Context) *slog.Logger { + return ctx.Logger().With("module", ModuleName) +} + +// GetAccount returns a specific account in the AccountKeeper. func (ak AccountKeeper) GetAccount(ctx sdk.Context, addr crypto.Address) std.Account { stor := ctx.Store(ak.key) bz := stor.Get(AddressStoreKey(addr)) diff --git a/tm2/pkg/sdk/bank/keeper.go b/tm2/pkg/sdk/bank/keeper.go index 5d3699c99ef..f98e6b3e225 100644 --- a/tm2/pkg/sdk/bank/keeper.go +++ b/tm2/pkg/sdk/bank/keeper.go @@ -25,8 +25,8 @@ type BankKeeperI interface { var _ BankKeeperI = BankKeeper{} -// BBankKeeper only allows transfers between accounts without the possibility of -// creating coins. It implements the BankKeeper interface. +// BankKeeper only allows transfers between accounts without the possibility of +// creating coins. It implements the BankKeeperI interface. type BankKeeper struct { ViewKeeper diff --git a/tm2/pkg/sdk/params/doc.go b/tm2/pkg/sdk/params/doc.go new file mode 100644 index 00000000000..a433b5eb115 --- /dev/null +++ b/tm2/pkg/sdk/params/doc.go @@ -0,0 +1,15 @@ +// Package params provides a lightweight implementation inspired by the x/params +// module of the Cosmos SDK. +// +// 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. +// +// XXX: removes isAlphaNum validation for keys. +package params diff --git a/tm2/pkg/sdk/params/handler.go b/tm2/pkg/sdk/params/handler.go new file mode 100644 index 00000000000..b662fc06c58 --- /dev/null +++ b/tm2/pkg/sdk/params/handler.go @@ -0,0 +1,79 @@ +package params + +import ( + "fmt" + "strings" + + abci "github.com/gnolang/gno/tm2/pkg/bft/abci/types" + "github.com/gnolang/gno/tm2/pkg/sdk" + "github.com/gnolang/gno/tm2/pkg/std" +) + +type paramsHandler struct { + params ParamsKeeper +} + +func NewHandler(params ParamsKeeper) paramsHandler { + return paramsHandler{ + params: params, + } +} + +func (bh paramsHandler) Process(ctx sdk.Context, msg std.Msg) sdk.Result { + errMsg := fmt.Sprintf("unrecognized params message type: %T", msg) + return abciResult(std.ErrUnknownRequest(errMsg)) +} + +//---------------------------------------- +// Query + +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 + } +} + +// 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 == "" { + res = sdk.ABCIResponseQueryFromError( + std.ErrUnknownRequest("param key is empty")) + } + + // XXX: validate? + + val := bh.params.GetRaw(ctx, key) + + res.Data = val + return +} + +//---------------------------------------- +// misc + +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) + if len(parts) < 2 { + return "" + } else { + return parts[1] + } +} + +// returns the third component of a path, including other slashes. +func thirdPartWithSlashes(path string) string { + split := strings.SplitN(path, "/", 3) + return split[2] +} diff --git a/tm2/pkg/sdk/params/handler_test.go b/tm2/pkg/sdk/params/handler_test.go new file mode 100644 index 00000000000..1fff5d007d3 --- /dev/null +++ b/tm2/pkg/sdk/params/handler_test.go @@ -0,0 +1,58 @@ +package params + +import ( + "strings" + "testing" + + "github.com/stretchr/testify/require" + + 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/sdk" + tu "github.com/gnolang/gno/tm2/pkg/sdk/testutils" +) + +func TestInvalidMsg(t *testing.T) { + t.Parallel() + + h := NewHandler(ParamsKeeper{}) + res := h.Process(sdk.NewContext(sdk.RunTxModeDeliver, nil, &bft.Header{ChainID: "test-chain"}, nil), tu.NewTestMsg()) + require.False(t, res.IsOK()) + require.True(t, strings.Contains(res.Log, "unrecognized params message type")) +} + +func TestQuery(t *testing.T) { + t.Parallel() + + env := setupTestEnv() + h := NewHandler(env.keeper) + + req := abci.RequestQuery{ + Path: "params/params_test/foo/bar.string", + } + + 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, "foo/bar.string", "baz") + + res = h.Query(env.ctx, req) + require.Nil(t, res.Error) + require.NotNil(t, res) + require.Equal(t, string(res.Data), `"baz"`) +} + +func TestQuerierRouteNotFound(t *testing.T) { + t.Parallel() + + env := setupTestEnv() + h := NewHandler(env.keeper) + req := abci.RequestQuery{ + Path: "params/notfound", + Data: []byte{}, + } + res := h.Query(env.ctx, req) + require.Error(t, res.Error) +} diff --git a/tm2/pkg/sdk/params/keeper.go b/tm2/pkg/sdk/params/keeper.go new file mode 100644 index 00000000000..ffeb1775acb --- /dev/null +++ b/tm2/pkg/sdk/params/keeper.go @@ -0,0 +1,157 @@ +package params + +import ( + "log/slog" + "strings" + + "github.com/gnolang/gno/tm2/pkg/amino" + "github.com/gnolang/gno/tm2/pkg/sdk" + "github.com/gnolang/gno/tm2/pkg/store" +) + +const ( + ModuleName = "params" + StoreKey = ModuleName +) + +type ParamsKeeperI interface { + GetString(ctx sdk.Context, key string, ptr *string) + GetInt64(ctx sdk.Context, key string, ptr *int64) + GetUint64(ctx sdk.Context, key string, ptr *uint64) + GetBool(ctx sdk.Context, key string, ptr *bool) + GetBytes(ctx sdk.Context, key string, ptr *[]byte) + + SetString(ctx sdk.Context, key string, value string) + SetInt64(ctx sdk.Context, key string, value int64) + SetUint64(ctx sdk.Context, key string, value uint64) + SetBool(ctx sdk.Context, key string, value bool) + SetBytes(ctx sdk.Context, key string, value []byte) + + Has(ctx sdk.Context, key string) bool + GetRaw(ctx sdk.Context, key string) []byte + + // XXX: ListKeys? +} + +var _ ParamsKeeperI = ParamsKeeper{} + +// global paramstore Keeper. +type ParamsKeeper struct { + key store.StoreKey + prefix string +} + +// NewParamsKeeper returns a new ParamsKeeper. +func NewParamsKeeper(key store.StoreKey, prefix string) ParamsKeeper { + return ParamsKeeper{ + key: key, + prefix: prefix, + } +} + +// Logger returns a module-specific logger. +// XXX: why do we expose this? +func (pk ParamsKeeper) Logger(ctx sdk.Context) *slog.Logger { + return ctx.Logger().With("module", ModuleName) +} + +func (pk ParamsKeeper) Has(ctx sdk.Context, key string) bool { + stor := ctx.Store(pk.key) + return stor.Has([]byte(key)) +} + +func (pk ParamsKeeper) GetRaw(ctx sdk.Context, key string) []byte { + stor := ctx.Store(pk.key) + return stor.Get([]byte(key)) +} + +func (pk ParamsKeeper) GetString(ctx sdk.Context, key string, ptr *string) { + checkSuffix(key, ".string") + pk.getIfExists(ctx, key, ptr) +} + +func (pk ParamsKeeper) GetBool(ctx sdk.Context, key string, ptr *bool) { + checkSuffix(key, ".bool") + pk.getIfExists(ctx, key, ptr) +} + +func (pk ParamsKeeper) GetInt64(ctx sdk.Context, key string, ptr *int64) { + checkSuffix(key, ".int64") + pk.getIfExists(ctx, key, ptr) +} + +func (pk ParamsKeeper) GetUint64(ctx sdk.Context, key string, ptr *uint64) { + checkSuffix(key, ".uint64") + pk.getIfExists(ctx, key, ptr) +} + +func (pk ParamsKeeper) GetBytes(ctx sdk.Context, key string, ptr *[]byte) { + checkSuffix(key, ".bytes") + pk.getIfExists(ctx, key, ptr) +} + +func (pk ParamsKeeper) SetString(ctx sdk.Context, key, value string) { + checkSuffix(key, ".string") + pk.set(ctx, key, value) +} + +func (pk ParamsKeeper) SetBool(ctx sdk.Context, key string, value bool) { + checkSuffix(key, ".bool") + pk.set(ctx, key, value) +} + +func (pk ParamsKeeper) SetInt64(ctx sdk.Context, key string, value int64) { + checkSuffix(key, ".int64") + pk.set(ctx, key, value) +} + +func (pk ParamsKeeper) SetUint64(ctx sdk.Context, key string, value uint64) { + checkSuffix(key, ".uint64") + pk.set(ctx, key, value) +} + +func (pk ParamsKeeper) SetBytes(ctx sdk.Context, key string, value []byte) { + checkSuffix(key, ".bytes") + pk.set(ctx, key, value) +} + +func (pk ParamsKeeper) getIfExists(ctx sdk.Context, key string, ptr interface{}) { + stor := ctx.Store(pk.key) + bz := stor.Get([]byte(key)) + if bz == nil { + return + } + err := amino.UnmarshalJSON(bz, ptr) + if err != nil { + panic(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) + if err != nil { + panic(err) + } +} + +func (pk ParamsKeeper) set(ctx sdk.Context, key string, value interface{}) { + stor := ctx.Store(pk.key) + bz, err := amino.MarshalJSON(value) + if err != nil { + panic(err) + } + stor.Set([]byte(key), bz) +} + +func checkSuffix(key, expectedSuffix string) { + var ( + noSuffix = !strings.HasSuffix(key, expectedSuffix) + noName = len(key) == len(expectedSuffix) + // XXX: additional sanity checks? + ) + if noSuffix || noName { + panic(`key should be like "` + expectedSuffix + `"`) + } +} diff --git a/tm2/pkg/sdk/params/keeper_test.go b/tm2/pkg/sdk/params/keeper_test.go new file mode 100644 index 00000000000..45a97ae44ad --- /dev/null +++ b/tm2/pkg/sdk/params/keeper_test.go @@ -0,0 +1,142 @@ +package params + +import ( + "reflect" + "testing" + + "github.com/gnolang/gno/tm2/pkg/amino" + "github.com/stretchr/testify/require" +) + +func TestKeeper(t *testing.T) { + env := setupTestEnv() + ctx, store, keeper := env.ctx, env.store, env.keeper + _ = store // XXX: add store tests? + + require.False(t, keeper.Has(ctx, "param1.string")) + require.False(t, keeper.Has(ctx, "param2.bool")) + require.False(t, keeper.Has(ctx, "param3.uint64")) + require.False(t, keeper.Has(ctx, "param4.int64")) + require.False(t, keeper.Has(ctx, "param5.bytes")) + + // initial set + require.NotPanics(t, func() { keeper.SetString(ctx, "param1.string", "foo") }) + require.NotPanics(t, func() { keeper.SetBool(ctx, "param2.bool", true) }) + require.NotPanics(t, func() { keeper.SetUint64(ctx, "param3.uint64", 42) }) + require.NotPanics(t, func() { keeper.SetInt64(ctx, "param4.int64", -1337) }) + require.NotPanics(t, func() { keeper.SetBytes(ctx, "param5.bytes", []byte("hello world!")) }) + + require.True(t, keeper.Has(ctx, "param1.string")) + require.True(t, keeper.Has(ctx, "param2.bool")) + require.True(t, keeper.Has(ctx, "param3.uint64")) + require.True(t, keeper.Has(ctx, "param4.int64")) + require.True(t, keeper.Has(ctx, "param5.bytes")) + + var ( + param1 string + param2 bool + param3 uint64 + param4 int64 + param5 []byte + ) + + require.NotPanics(t, func() { keeper.GetString(ctx, "param1.string", ¶m1) }) + require.NotPanics(t, func() { keeper.GetBool(ctx, "param2.bool", ¶m2) }) + require.NotPanics(t, func() { keeper.GetUint64(ctx, "param3.uint64", ¶m3) }) + require.NotPanics(t, func() { keeper.GetInt64(ctx, "param4.int64", ¶m4) }) + require.NotPanics(t, func() { keeper.GetBytes(ctx, "param5.bytes", ¶m5) }) + + require.Equal(t, param1, "foo") + require.Equal(t, param2, true) + require.Equal(t, param3, uint64(42)) + require.Equal(t, param4, int64(-1337)) + require.Equal(t, param5, []byte("hello world!")) + + // reset + require.NotPanics(t, func() { keeper.SetString(ctx, "param1.string", "bar") }) + require.NotPanics(t, func() { keeper.SetBool(ctx, "param2.bool", false) }) + require.NotPanics(t, func() { keeper.SetUint64(ctx, "param3.uint64", 12345) }) + require.NotPanics(t, func() { keeper.SetInt64(ctx, "param4.int64", 1000) }) + require.NotPanics(t, func() { keeper.SetBytes(ctx, "param5.bytes", []byte("bye")) }) + + require.True(t, keeper.Has(ctx, "param1.string")) + require.True(t, keeper.Has(ctx, "param2.bool")) + require.True(t, keeper.Has(ctx, "param3.uint64")) + require.True(t, keeper.Has(ctx, "param4.int64")) + require.True(t, keeper.Has(ctx, "param5.bytes")) + + require.NotPanics(t, func() { keeper.GetString(ctx, "param1.string", ¶m1) }) + require.NotPanics(t, func() { keeper.GetBool(ctx, "param2.bool", ¶m2) }) + require.NotPanics(t, func() { keeper.GetUint64(ctx, "param3.uint64", ¶m3) }) + require.NotPanics(t, func() { keeper.GetInt64(ctx, "param4.int64", ¶m4) }) + require.NotPanics(t, func() { keeper.GetBytes(ctx, "param5.bytes", ¶m5) }) + + require.Equal(t, param1, "bar") + require.Equal(t, param2, false) + require.Equal(t, param3, uint64(12345)) + require.Equal(t, param4, int64(1000)) + require.Equal(t, param5, []byte("bye")) + + // invalid sets + require.PanicsWithValue(t, `key should be like ".string"`, func() { keeper.SetString(ctx, "invalid.int64", "hello") }) + require.PanicsWithValue(t, `key should be like ".int64"`, func() { keeper.SetInt64(ctx, "invalid.string", int64(42)) }) + require.PanicsWithValue(t, `key should be like ".uint64"`, func() { keeper.SetUint64(ctx, "invalid.int64", uint64(42)) }) + require.PanicsWithValue(t, `key should be like ".bool"`, func() { keeper.SetBool(ctx, "invalid.int64", true) }) + require.PanicsWithValue(t, `key should be like ".bytes"`, func() { keeper.SetBytes(ctx, "invalid.int64", []byte("hello")) }) +} + +// adapted from TestKeeperSubspace from Cosmos SDK, but adapted to a subspace-less Keeper. +func TestKeeper_internal(t *testing.T) { + env := setupTestEnv() + ctx, store, keeper := env.ctx, env.store, env.keeper + + kvs := []struct { + key string + param interface{} + zero interface{} + ptr interface{} + }{ + {"string", "test", "", new(string)}, + {"bool", true, false, new(bool)}, + {"int16", int16(1), int16(0), new(int16)}, + {"int32", int32(1), int32(0), new(int32)}, + {"int64", int64(1), int64(0), new(int64)}, + {"uint16", uint16(1), uint16(0), new(uint16)}, + {"uint32", uint32(1), uint32(0), new(uint32)}, + {"uint64", uint64(1), uint64(0), new(uint64)}, + {"struct", s{1}, s{0}, new(s)}, + } + + for i, kv := range kvs { + require.NotPanics(t, func() { keeper.set(ctx, kv.key, kv.param) }, "keeper.Set panics, tc #%d", i) + } + + 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) + } + + for i, kv := range kvs { + bz := store.Get([]byte(kv.key)) + 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) + require.Equal(t, kv.param, indirect(kv.ptr), "stored param not equal, tc #%d", i) + } +} + +type s struct{ I int } + +func indirect(ptr interface{}) interface{} { return reflect.ValueOf(ptr).Elem().Interface() } diff --git a/tm2/pkg/sdk/params/test_common.go b/tm2/pkg/sdk/params/test_common.go new file mode 100644 index 00000000000..8243ee867de --- /dev/null +++ b/tm2/pkg/sdk/params/test_common.go @@ -0,0 +1,46 @@ +package params + +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/store" + "github.com/gnolang/gno/tm2/pkg/store/iavl" +) + +type testEnv struct { + ctx sdk.Context + store store.Store + keeper ParamsKeeper +} + +func setupTestEnv() testEnv { + db := memdb.NewMemDB() + paramsCapKey := store.NewStoreKey("paramsCapKey") + ms := store.NewCommitMultiStore(db) + ms.MountStoreWithDB(paramsCapKey, iavl.StoreConstructor, db) + ms.LoadLatestVersion() + + prefix := "params_test" + keeper := NewParamsKeeper(paramsCapKey, prefix) + + ctx := sdk.NewContext(sdk.RunTxModeDeliver, ms, &bft.Header{Height: 1, ChainID: "test-chain-id"}, log.NewNoopLogger()) + // XXX: context key? + 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 + }, + }) + + stor := ctx.Store(paramsCapKey) + return testEnv{ctx: ctx, store: stor, keeper: keeper} +} diff --git a/tm2/pkg/store/README.md b/tm2/pkg/store/README.md index abf5c26bc07..24ae0c805ac 100644 --- a/tm2/pkg/store/README.md +++ b/tm2/pkg/store/README.md @@ -116,15 +116,3 @@ type traceOperation struct { ``` `traceOperation.Metadata` is filled with `Store.context` when it is not nil. `TraceContext` is a `map[string]interface{}`. - -## Transient - -`transient.Store` is a base-layer `KVStore` which is automatically discarded at the end of the block. - -```go -type Store struct { - dbadapter.Store -} -``` - -`Store.Store` is a `dbadapter.Store` with a `memdb.NewMemDB()`. All `KVStore` methods are reused. When `Store.Commit()` is called, new `dbadapter.Store` is assigned, discarding previous reference and making it garbage collected.