diff --git a/gno.land/pkg/gnoland/app.go b/gno.land/pkg/gnoland/app.go index 2380658c6e9..f8e745b1730 100644 --- a/gno.land/pkg/gnoland/app.go +++ b/gno.land/pkg/gnoland/app.go @@ -2,6 +2,7 @@ package gnoland import ( + "errors" "fmt" "log/slog" "path/filepath" @@ -71,6 +72,7 @@ func NewAppWithOptions(cfg *AppOptions) (abci.Application, error) { if err := cfg.validate(); err != nil { return nil, err } + cfg.setDefaults() // Capabilities keys. mainKey := store.NewStoreKey("main") @@ -121,6 +123,9 @@ func NewAppWithOptions(cfg *AppOptions) (abci.Application, error) { // the tx - in other words, data from a failing transaction won't be persisted // to the gno store caches. baseApp.SetBeginTxHook(func(ctx sdk.Context) sdk.Context { + if icc.beginTxHook != nil && ctx.BlockHeight() == 0 { + ctx = icc.beginTxHook(ctx) + } // Create Gno transaction store. return vmk.MakeGnoTransactionStore(ctx) }) @@ -229,10 +234,16 @@ type InitChainerConfig struct { vmKpr vm.VMKeeperI acctKpr auth.AccountKeeperI bankKpr bank.BankKeeperI + + // This is used by InitChainer, to set a different beginTxHook on each + // transaction. This allows the InitChainer to pass additional data down + // to the underlying app, such as custom Time/Height for genesis. + // (See [GnoGenesisState]). + beginTxHook func(sdk.Context) sdk.Context } // InitChainer is the function that can be used as a [sdk.InitChainer]. -func (cfg InitChainerConfig) InitChainer(ctx sdk.Context, req abci.RequestInitChain) abci.ResponseInitChain { +func (cfg *InitChainerConfig) InitChainer(ctx sdk.Context, req abci.RequestInitChain) abci.ResponseInitChain { start := time.Now() ctx.Logger().Debug("InitChainer: started") @@ -263,7 +274,7 @@ func (cfg InitChainerConfig) InitChainer(ctx sdk.Context, req abci.RequestInitCh } } -func (cfg InitChainerConfig) loadStdlibs(ctx sdk.Context) { +func (cfg *InitChainerConfig) loadStdlibs(ctx sdk.Context) { // cache-wrapping is necessary for non-validator nodes; in the tm2 BaseApp, // this is done using BaseApp.cacheTxContext; so we replicate it here. ms := ctx.MultiStore() @@ -281,7 +292,12 @@ func (cfg InitChainerConfig) loadStdlibs(ctx sdk.Context) { msCache.MultiWrite() } -func (cfg InitChainerConfig) loadAppState(ctx sdk.Context, appState any) ([]abci.ResponseDeliverTx, error) { +func (cfg *InitChainerConfig) loadAppState(ctx sdk.Context, appState any) ([]abci.ResponseDeliverTx, error) { + defer func() { + // Ensure to reset beginTxHook to nil when wrapping up. + cfg.beginTxHook = nil + }() + state, ok := appState.(GnoGenesisState) if !ok { return nil, fmt.Errorf("invalid AppState of type %T", appState) @@ -297,9 +313,23 @@ func (cfg InitChainerConfig) loadAppState(ctx sdk.Context, appState any) ([]abci } } + if len(state.TxContexts) != 0 && len(state.Txs) != len(state.TxContexts) { + return nil, errors.New("genesis state tx_contexts, if given, should be of same length as txs") + } + txResponses := make([]abci.ResponseDeliverTx, 0, len(state.Txs)) // Run genesis txs - for _, tx := range state.Txs { + for idx, tx := range state.Txs { + if len(state.TxContexts) > 0 { + customExecCtx := state.TxContexts[idx] + if customExecCtx.Timestamp == 0 { + customExecCtx.Timestamp = ctx.BlockTime().Unix() + } + cfg.beginTxHook = func(ctx sdk.Context) sdk.Context { + return vm.InjectExecContextCustom(ctx, customExecCtx) + } + } + res := cfg.baseApp.Deliver(tx) if res.IsErr() { ctx.Logger().Error( diff --git a/gno.land/pkg/gnoland/app_test.go b/gno.land/pkg/gnoland/app_test.go index 193ff0b0b14..b401df81859 100644 --- a/gno.land/pkg/gnoland/app_test.go +++ b/gno.land/pkg/gnoland/app_test.go @@ -4,6 +4,7 @@ import ( "context" "errors" "fmt" + "strconv" "strings" "testing" "time" @@ -26,6 +27,18 @@ import ( "github.com/stretchr/testify/require" ) +func testRequestInitChain(genState GnoGenesisState) abci.RequestInitChain { + return abci.RequestInitChain{ + Time: time.Now(), + ChainID: "dev", + ConsensusParams: &abci.ConsensusParams{ + Block: defaultBlockParams(), + }, + Validators: []abci.ValidatorUpdate{}, + AppState: genState, + } +} + // Tests that NewAppWithOptions works even when only providing a simple DB. func TestNewAppWithOptions(t *testing.T) { t.Parallel() @@ -37,34 +50,26 @@ func TestNewAppWithOptions(t *testing.T) { assert.Equal(t, "gnoland", bapp.Name()) addr := crypto.AddressFromPreimage([]byte("test1")) - resp := bapp.InitChain(abci.RequestInitChain{ - Time: time.Now(), - ChainID: "dev", - ConsensusParams: &abci.ConsensusParams{ - Block: defaultBlockParams(), - }, - Validators: []abci.ValidatorUpdate{}, - AppState: GnoGenesisState{ - Balances: []Balance{ - { - Address: addr, - Amount: []std.Coin{{Amount: 1e15, Denom: "ugnot"}}, - }, + resp := bapp.InitChain(testRequestInitChain(GnoGenesisState{ + Balances: []Balance{ + { + Address: addr, + Amount: []std.Coin{{Amount: 1e15, Denom: "ugnot"}}, }, - Txs: []std.Tx{ - { - Msgs: []std.Msg{vm.NewMsgAddPackage(addr, "gno.land/r/demo", []*std.MemFile{ - { - Name: "demo.gno", - Body: "package demo; func Hello() string { return `hello`; }", - }, - })}, - Fee: std.Fee{GasWanted: 1e6, GasFee: std.Coin{Amount: 1e6, Denom: "ugnot"}}, - Signatures: []std.Signature{{}}, // one empty signature - }, + }, + Txs: []std.Tx{ + { + Msgs: []std.Msg{vm.NewMsgAddPackage(addr, "gno.land/r/demo", []*std.MemFile{ + { + Name: "demo.gno", + Body: "package demo; func Hello() string { return `hello`; }", + }, + })}, + Fee: std.Fee{GasWanted: 1e6, GasFee: std.Coin{Amount: 1e6, Denom: "ugnot"}}, + Signatures: []std.Signature{{}}, // one empty signature }, }, - }) + })) require.True(t, resp.IsOK(), "InitChain response: %v", resp) tx := amino.MustMarshal(std.Tx{ @@ -116,6 +121,115 @@ func TestNewApp(t *testing.T) { assert.True(t, resp.IsOK(), "resp is not OK: %v", resp) } +func TestNewAppWithOptions_WithTxContexts(t *testing.T) { + t.Parallel() + + appOpts := TestAppOptions(memdb.NewMemDB()) + called := 0 + appOpts.GenesisTxResultHandler = func(ctx sdk.Context, _ std.Tx, res sdk.Result) { + if !res.IsOK() { + t.Fatal(res) + } + called++ + switch called { + case 1: + assert.Equal(t, string(res.Data), "time.Now() 7331\nstd.GetHeight() -1337\n") + case 2: + assert.Equal(t, string(res.Data), "time.Now() "+strconv.FormatInt(ctx.BlockTime().Unix(), 10)+"\nstd.GetHeight() 13377331\n") + } + } + app, err := NewAppWithOptions(appOpts) + require.NoError(t, err) + bapp := app.(*sdk.BaseApp) + + const pkgFile = `package main + +import ( + "std" + "time" +) + +func main() { + println("time.Now()", time.Now().Unix()) + println("std.GetHeight()", std.GetHeight()) +} +` + + addr := crypto.AddressFromPreimage([]byte("test1")) + resp := bapp.InitChain(testRequestInitChain(GnoGenesisState{ + Balances: []Balance{ + { + Address: addr, + Amount: []std.Coin{{Amount: 1e15, Denom: "ugnot"}}, + }, + }, + Txs: []std.Tx{ + { + Msgs: []std.Msg{vm.NewMsgRun(addr, std.Coins{}, []*std.MemFile{ + { + Name: "demo.gno", + Body: pkgFile, + }, + })}, + Fee: std.Fee{GasWanted: 1e6, GasFee: std.Coin{Amount: 1e6, Denom: "ugnot"}}, + Signatures: []std.Signature{{}}, // one empty signature + }, + { + // same as previous, but will have different TxContext. + Msgs: []std.Msg{vm.NewMsgRun(addr, std.Coins{}, []*std.MemFile{ + { + Name: "demo.gno", + Body: pkgFile, + }, + })}, + Fee: std.Fee{GasWanted: 1e6, GasFee: std.Coin{Amount: 1e6, Denom: "ugnot"}}, + Signatures: []std.Signature{{}}, // one empty signature + }, + }, + TxContexts: []vm.ExecContextCustom{ + { + Height: -1337, + Timestamp: 7331, + }, + { + // different height; timestamp=0 will mean that InitChain will auto-set it + // to the block time. + Height: 13377331, + Timestamp: 0, + }, + }, + })) + require.True(t, resp.IsOK(), "InitChain response: %v", resp) +} + +func TestNewAppWithOptions_InvalidTxContexts(t *testing.T) { + t.Parallel() + + app, err := NewAppWithOptions(testAppOptions()) + require.NoError(t, err) + bapp := app.(*sdk.BaseApp) + + addr := crypto.AddressFromPreimage([]byte("test1")) + resp := bapp.InitChain(testRequestInitChain(GnoGenesisState{ + Balances: []Balance{ + { + Address: addr, + Amount: []std.Coin{{Amount: 1e15, Denom: "ugnot"}}, + }, + }, + Txs: []std.Tx{ + // one tx + {}, + }, + TxContexts: []vm.ExecContextCustom{ + // two contexts + {}, {}, + }, + })) + assert.True(t, resp.IsErr()) + assert.ErrorContains(t, resp.Error, "genesis state tx_contexts, if given, should be of same length as txs") +} + // Test whether InitChainer calls to load the stdlibs correctly. func TestInitChainer_LoadStdlib(t *testing.T) { t.Parallel() diff --git a/gno.land/pkg/gnoland/types.go b/gno.land/pkg/gnoland/types.go index 016f3279dbd..c12bea05d83 100644 --- a/gno.land/pkg/gnoland/types.go +++ b/gno.land/pkg/gnoland/types.go @@ -3,6 +3,7 @@ package gnoland import ( "errors" + "github.com/gnolang/gno/gno.land/pkg/sdk/vm" "github.com/gnolang/gno/tm2/pkg/std" ) @@ -22,4 +23,6 @@ func ProtoGnoAccount() std.Account { type GnoGenesisState struct { Balances []Balance `json:"balances"` Txs []std.Tx `json:"txs"` + // Should match len(Txs), or be null + TxContexts []vm.ExecContextCustom `json:"tx_contexts"` } diff --git a/gno.land/pkg/sdk/vm/keeper.go b/gno.land/pkg/sdk/vm/keeper.go index 40d253ed456..628d970c7d5 100644 --- a/gno.land/pkg/sdk/vm/keeper.go +++ b/gno.land/pkg/sdk/vm/keeper.go @@ -198,9 +198,12 @@ func loadStdlibPackage(pkgPath, stdlibDir string, store gno.Store) { m.RunMemPackage(memPkg, true) } -type gnoStoreContextKeyType struct{} +type keeperContextKey string -var gnoStoreContextKey gnoStoreContextKeyType +const ( + keeperKeyGnoStore keeperContextKey = "keeper:gno_store" + keeperKeyCustomContext keeperContextKey = "keeper:exec_context" +) func (vm *VMKeeper) newGnoTransactionStore(ctx sdk.Context) gno.TransactionStore { base := ctx.Store(vm.baseKey) @@ -210,7 +213,7 @@ func (vm *VMKeeper) newGnoTransactionStore(ctx sdk.Context) gno.TransactionStore } func (vm *VMKeeper) MakeGnoTransactionStore(ctx sdk.Context) sdk.Context { - return ctx.WithValue(gnoStoreContextKey, vm.newGnoTransactionStore(ctx)) + return ctx.WithValue(keeperKeyGnoStore, vm.newGnoTransactionStore(ctx)) } func (vm *VMKeeper) CommitGnoTransactionStore(ctx sdk.Context) { @@ -218,7 +221,7 @@ func (vm *VMKeeper) CommitGnoTransactionStore(ctx sdk.Context) { } func (vm *VMKeeper) getGnoTransactionStore(ctx sdk.Context) gno.TransactionStore { - txStore := ctx.Value(gnoStoreContextKey).(gno.TransactionStore) + txStore := ctx.Value(keeperKeyGnoStore).(gno.TransactionStore) txStore.ClearObjectCache() return txStore } @@ -226,6 +229,49 @@ 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]+)`) +// ExecContextCustom is a subset of [stdlibs.ExecContext] which can be used to +// inject custom fields at genesis. +type ExecContextCustom struct { + // Height is the BlockHeight: it is not recommended to set the same + // BlockHeight as the one used by the previous iteration of the chain, as + // from the realm's point of view it will eventually be as if a height + // occurred twice at different times: instead, use negative heights. + // (Last block should be -1, as 0 is the genesis block). + Height int64 `json:"height"` + Timestamp int64 `json:"timestamp"` +} + +// InjectExecContextCustom returns a new sdk.Context, which contains the given +// [ExecContextCustom]. This can be used to modify Height and timestamp in +// genesis transactions. +func InjectExecContextCustom(ctx sdk.Context, e ExecContextCustom) sdk.Context { + return ctx.WithValue(keeperKeyCustomContext, e) +} + +func (vm *VMKeeper) newExecContext(ctx sdk.Context, creator, pkgAddr crypto.Address) stdlibs.ExecContext { + execCtx := stdlibs.ExecContext{ + ChainID: ctx.ChainID(), + Height: ctx.BlockHeight(), + Timestamp: ctx.BlockTime().Unix(), + OrigCaller: creator.Bech32(), + OrigSendSpent: new(std.Coins), + OrigPkgAddr: pkgAddr.Bech32(), + Banker: NewSDKBanker(vm, ctx), + EventLogger: ctx.EventLogger(), + } + + // if we're at genesis, ctx.Context() can change Height and Timestamp. + if ctx.BlockHeight() == 0 { + v, ok := ctx.Value(keeperKeyCustomContext).(ExecContextCustom) + if ok { + execCtx.Height = v.Height + execCtx.Timestamp = v.Timestamp + } + } + + return execCtx +} + // 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" @@ -253,17 +299,8 @@ func (vm *VMKeeper) checkNamespacePermission(ctx sdk.Context, creator crypto.Add // Parse and run the files, construct *PV. pkgAddr := gno.DerivePkgAddr(pkgPath) - msgCtx := stdlibs.ExecContext{ - ChainID: ctx.ChainID(), - Height: ctx.BlockHeight(), - Timestamp: ctx.BlockTime().Unix(), - OrigCaller: creator.Bech32(), - OrigSendSpent: new(std.Coins), - OrigPkgAddr: pkgAddr.Bech32(), - // XXX: should we remove the banker ? - Banker: NewSDKBanker(vm, ctx), - EventLogger: ctx.EventLogger(), - } + msgCtx := vm.newExecContext(ctx, creator, pkgAddr) + // XXX: should we remove the banker from this ExecContext? m := gno.NewMachineWithOptions( gno.MachineOptions{ @@ -353,18 +390,8 @@ func (vm *VMKeeper) AddPackage(ctx sdk.Context, msg MsgAddPackage) (err error) { } // Parse and run the files, construct *PV. - msgCtx := stdlibs.ExecContext{ - ChainID: ctx.ChainID(), - Height: ctx.BlockHeight(), - Timestamp: ctx.BlockTime().Unix(), - Msg: msg, - OrigCaller: creator.Bech32(), - OrigSend: deposit, - OrigSendSpent: new(std.Coins), - OrigPkgAddr: pkgAddr.Bech32(), - Banker: NewSDKBanker(vm, ctx), - EventLogger: ctx.EventLogger(), - } + msgCtx := vm.newExecContext(ctx, creator, pkgAddr) + msgCtx.OrigSend = deposit // Parse and run the files, construct *PV. m2 := gno.NewMachineWithOptions( gno.MachineOptions{ @@ -454,18 +481,8 @@ func (vm *VMKeeper) Call(ctx sdk.Context, msg MsgCall) (res string, err error) { // Make context. // NOTE: if this is too expensive, // could it be safely partially memoized? - msgCtx := stdlibs.ExecContext{ - ChainID: ctx.ChainID(), - Height: ctx.BlockHeight(), - Timestamp: ctx.BlockTime().Unix(), - Msg: msg, - OrigCaller: caller.Bech32(), - OrigSend: send, - OrigSendSpent: new(std.Coins), - OrigPkgAddr: pkgAddr.Bech32(), - Banker: NewSDKBanker(vm, ctx), - EventLogger: ctx.EventLogger(), - } + msgCtx := vm.newExecContext(ctx, caller, pkgAddr) + msgCtx.OrigSend = send // Construct machine and evaluate. m := gno.NewMachineWithOptions( gno.MachineOptions{ @@ -553,18 +570,8 @@ func (vm *VMKeeper) Run(ctx sdk.Context, msg MsgRun) (res string, err error) { } // Parse and run the files, construct *PV. - msgCtx := stdlibs.ExecContext{ - ChainID: ctx.ChainID(), - Height: ctx.BlockHeight(), - Timestamp: ctx.BlockTime().Unix(), - Msg: msg, - OrigCaller: caller.Bech32(), - OrigSend: send, - OrigSendSpent: new(std.Coins), - OrigPkgAddr: pkgAddr.Bech32(), - Banker: NewSDKBanker(vm, ctx), - EventLogger: ctx.EventLogger(), - } + msgCtx := vm.newExecContext(ctx, caller, pkgAddr) + msgCtx.OrigSend = send // Parse and run the files, construct *PV. buf := new(bytes.Buffer) m := gno.NewMachineWithOptions( @@ -714,18 +721,8 @@ func (vm *VMKeeper) QueryEval(ctx sdk.Context, pkgPath string, expr string) (res return "", err } // Construct new machine. - msgCtx := stdlibs.ExecContext{ - ChainID: ctx.ChainID(), - Height: ctx.BlockHeight(), - Timestamp: ctx.BlockTime().Unix(), - // Msg: msg, - // OrigCaller: caller, - // OrigSend: send, - // OrigSendSpent: nil, - OrigPkgAddr: pkgAddr.Bech32(), - Banker: NewSDKBanker(vm, ctx), // safe as long as ctx is a fork to be discarded. - EventLogger: ctx.EventLogger(), - } + // safe to pass ctx as long as it is a fork to be discarded. + msgCtx := vm.newExecContext(ctx, crypto.Address{}, pkgAddr) m := gno.NewMachineWithOptions( gno.MachineOptions{ PkgPath: pkgPath, @@ -781,18 +778,7 @@ func (vm *VMKeeper) QueryEvalString(ctx sdk.Context, pkgPath string, expr string return "", err } // Construct new machine. - msgCtx := stdlibs.ExecContext{ - ChainID: ctx.ChainID(), - Height: ctx.BlockHeight(), - Timestamp: ctx.BlockTime().Unix(), - // Msg: msg, - // OrigCaller: caller, - // OrigSend: jsend, - // OrigSendSpent: nil, - OrigPkgAddr: pkgAddr.Bech32(), - Banker: NewSDKBanker(vm, ctx), // safe as long as ctx is a fork to be discarded. - EventLogger: ctx.EventLogger(), - } + msgCtx := vm.newExecContext(ctx, crypto.Address{}, pkgAddr) m := gno.NewMachineWithOptions( gno.MachineOptions{ PkgPath: pkgPath, diff --git a/gnovm/stdlibs/std/context.go b/gnovm/stdlibs/std/context.go index ff5c91a14eb..966173b6d72 100644 --- a/gnovm/stdlibs/std/context.go +++ b/gnovm/stdlibs/std/context.go @@ -12,7 +12,6 @@ type ExecContext struct { Height int64 Timestamp int64 // seconds TimestampNano int64 // nanoseconds, only used for testing. - Msg sdk.Msg OrigCaller crypto.Bech32Address OrigPkgAddr crypto.Bech32Address OrigSend std.Coins diff --git a/gnovm/tests/file.go b/gnovm/tests/file.go index f6bd789f1bf..a5a70a251d9 100644 --- a/gnovm/tests/file.go +++ b/gnovm/tests/file.go @@ -60,7 +60,6 @@ func TestContext(pkgPath string, send std.Coins) *teststd.TestExecContext { ChainID: "dev", Height: 123, Timestamp: 1234567890, - Msg: nil, OrigCaller: caller.Bech32(), OrigPkgAddr: pkgAddr.Bech32(), OrigSend: send,