diff --git a/contribs/gnodev/pkg/dev/node.go b/contribs/gnodev/pkg/dev/node.go index 7f0c266bf48..35484caf0d4 100644 --- a/contribs/gnodev/pkg/dev/node.go +++ b/contribs/gnodev/pkg/dev/node.go @@ -468,7 +468,8 @@ func (n *Node) rebuildNode(ctx context.Context, genesis gnoland.GnoGenesisState) // Setup node config nodeConfig := newNodeConfig(n.config.TMConfig, n.config.ChainID, genesis) - nodeConfig.GenesisTxHandler = n.genesisTxHandler + nodeConfig.GenesisTxResultHandler = n.genesisTxResultHandler + nodeConfig.CacheStdlibLoad = true nodeConfig.Genesis.ConsensusParams.Block.MaxGas = n.config.MaxGasPerBlock // recoverFromError handles panics and converts them to errors. @@ -511,7 +512,7 @@ func (n *Node) rebuildNode(ctx context.Context, genesis gnoland.GnoGenesisState) return nil } -func (n *Node) genesisTxHandler(ctx sdk.Context, tx std.Tx, res sdk.Result) { +func (n *Node) genesisTxResultHandler(ctx sdk.Context, tx std.Tx, res sdk.Result) { if !res.IsErr() { return } diff --git a/gno.land/cmd/gnoland/testdata/issue_2283_cacheTypes.txtar b/gno.land/cmd/gnoland/testdata/issue_2283_cacheTypes.txtar index 38b0c8fe865..95bd48c0144 100644 --- a/gno.land/cmd/gnoland/testdata/issue_2283_cacheTypes.txtar +++ b/gno.land/cmd/gnoland/testdata/issue_2283_cacheTypes.txtar @@ -101,4 +101,3 @@ import ( func Call(s string) { base64.StdEncoding.DecodeString("hey") } - diff --git a/gno.land/pkg/gnoland/app.go b/gno.land/pkg/gnoland/app.go index f4d353411f8..115b947de27 100644 --- a/gno.land/pkg/gnoland/app.go +++ b/gno.land/pkg/gnoland/app.go @@ -5,6 +5,7 @@ import ( "log/slog" "path/filepath" "strconv" + "time" "github.com/gnolang/gno/gno.land/pkg/sdk/vm" "github.com/gnolang/gno/gnovm/pkg/gnoenv" @@ -29,28 +30,22 @@ import ( ) type AppOptions struct { - DB dbm.DB - // `gnoRootDir` should point to the local location of the gno repository. - // It serves as the gno equivalent of GOROOT. - GnoRootDir string - GenesisTxHandler GenesisTxHandler - Logger *slog.Logger - EventSwitch events.EventSwitch - MaxCycles int64 - // Whether to cache the result of loading the standard libraries. - // This is useful if you have to start many nodes, like in testing. - // This disables loading existing packages; so it should only be used - // on a fresh database. - CacheStdlibLoad bool + DB dbm.DB + Logger *slog.Logger + EventSwitch events.EventSwitch + MaxCycles int64 + InitChainerConfig } func NewAppOptions() *AppOptions { return &AppOptions{ - GenesisTxHandler: PanicOnFailingTxHandler, - Logger: log.NewNoopLogger(), - DB: memdb.NewMemDB(), - GnoRootDir: gnoenv.RootDir(), - EventSwitch: events.NilEventSwitch(), + Logger: log.NewNoopLogger(), + DB: memdb.NewMemDB(), + EventSwitch: events.NilEventSwitch(), + InitChainerConfig: InitChainerConfig{ + GenesisTxResultHandler: PanicOnFailingTxResultHandler, + StdlibDir: filepath.Join(gnoenv.RootDir(), "gnovm", "stdlibs"), + }, } } @@ -88,13 +83,13 @@ func NewAppWithOptions(cfg *AppOptions) (abci.Application, error) { // Construct keepers. acctKpr := auth.NewAccountKeeper(mainKey, ProtoGnoAccount) bankKpr := bank.NewBankKeeper(acctKpr) - - // XXX: Embed this ? - stdlibsDir := filepath.Join(cfg.GnoRootDir, "gnovm", "stdlibs") - vmk := vm.NewVMKeeper(baseKey, mainKey, acctKpr, bankKpr, stdlibsDir, cfg.MaxCycles) + vmk := vm.NewVMKeeper(baseKey, mainKey, acctKpr, bankKpr, cfg.MaxCycles) // Set InitChainer - baseApp.SetInitChainer(InitChainer(baseApp, acctKpr, bankKpr, cfg.GenesisTxHandler)) + icc := cfg.InitChainerConfig + icc.baseApp = baseApp + icc.acctKpr, icc.bankKpr, icc.vmKpr = acctKpr, bankKpr, vmk + baseApp.SetInitChainer(icc.InitChainer) // Set AnteHandler authOptions := auth.AnteOptions{ @@ -108,13 +103,22 @@ func NewAppWithOptions(cfg *AppOptions) (abci.Application, error) { newCtx sdk.Context, res sdk.Result, abort bool, ) { // Override auth params. - ctx = ctx.WithValue( - auth.AuthParamsContextKey{}, auth.DefaultParams()) + ctx = ctx. + WithValue(auth.AuthParamsContextKey{}, auth.DefaultParams()) // Continue on with default auth ante handler. newCtx, res, abort = authAnteHandler(ctx, tx, simulate) return }, ) + baseApp.SetBeginTxHook(func(ctx sdk.Context) sdk.Context { + // Create Gno transaction store. + return vmk.MakeGnoTransactionStore(ctx) + }) + baseApp.SetEndTxHook(func(ctx sdk.Context, result sdk.Result) { + if result.IsOK() { + vmk.CommitGnoTransactionStore(ctx) + } + }) // Set up the event collector c := newCollector[validatorUpdate]( @@ -143,7 +147,7 @@ func NewAppWithOptions(cfg *AppOptions) (abci.Application, error) { // Initialize the VMKeeper. ms := baseApp.GetCacheMultiStore() - vmk.Initialize(cfg.Logger, ms, cfg.CacheStdlibLoad) + vmk.Initialize(cfg.Logger, ms) ms.MultiWrite() // XXX why was't this needed? return baseApp, nil @@ -160,7 +164,7 @@ func NewApp( cfg := NewAppOptions() if skipFailingGenesisTxs { - cfg.GenesisTxHandler = NoopGenesisTxHandler + cfg.GenesisTxResultHandler = NoopGenesisTxResultHandler } // Get main DB. @@ -175,68 +179,110 @@ func NewApp( return NewAppWithOptions(cfg) } -type GenesisTxHandler func(ctx sdk.Context, tx std.Tx, res sdk.Result) +// GenesisTxResultHandler is called in the InitChainer after a genesis +// transaction is executed. +type GenesisTxResultHandler func(ctx sdk.Context, tx std.Tx, res sdk.Result) -func NoopGenesisTxHandler(_ sdk.Context, _ std.Tx, _ sdk.Result) {} +// NoopGenesisTxResultHandler is a no-op GenesisTxResultHandler. +func NoopGenesisTxResultHandler(_ sdk.Context, _ std.Tx, _ sdk.Result) {} -func PanicOnFailingTxHandler(_ sdk.Context, _ std.Tx, res sdk.Result) { +// PanicOnFailingTxResultHandler handles genesis transactions by panicking if +// res.IsErr() returns true. +func PanicOnFailingTxResultHandler(_ sdk.Context, _ std.Tx, res sdk.Result) { if res.IsErr() { panic(res.Log) } } -// InitChainer returns a function that can initialize the chain with genesis. -func InitChainer( - baseApp *sdk.BaseApp, - acctKpr auth.AccountKeeperI, - bankKpr bank.BankKeeperI, - resHandler GenesisTxHandler, -) func(sdk.Context, abci.RequestInitChain) abci.ResponseInitChain { - return func(ctx sdk.Context, req abci.RequestInitChain) abci.ResponseInitChain { - txResponses := []abci.ResponseDeliverTx{} - - if req.AppState != nil { - // Get genesis state - genState := req.AppState.(GnoGenesisState) - - // Parse and set genesis state balances - for _, bal := range genState.Balances { - acc := acctKpr.NewAccountWithAddress(ctx, bal.Address) - acctKpr.SetAccount(ctx, acc) - err := bankKpr.SetCoins(ctx, bal.Address, bal.Amount) - if err != nil { - panic(err) - } - } +// InitChainerConfig keeps the configuration for the InitChainer. +type InitChainerConfig struct { + // Handles the results of each genesis transaction. + GenesisTxResultHandler + + // Standard library directory. + StdlibDir string + // Whether to keep a record of the DB operations to load standard libraries, + // so they can be quickly replicated on additional genesis executions. + // This should be used for integration testing, where InitChainer will be + // called several times. + CacheStdlibLoad bool + + // 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 +} + +// InitChainer is the function that can be used as a [sdk.InitChainer]. +func (cfg InitChainerConfig) InitChainer(ctx sdk.Context, req abci.RequestInitChain) abci.ResponseInitChain { + start := time.Now() + ctx.Logger().Debug("InitChainer: started") + + { + // load standard libraries + // need to write to the MultiStore directly - so that the standard + // libraries are available when we process genesis txs + stdlibCtx := cfg.vmKpr.MakeGnoTransactionStore(ctx) + if cfg.CacheStdlibLoad { + cfg.vmKpr.LoadStdlibCached(stdlibCtx, cfg.StdlibDir) + } else { + cfg.vmKpr.LoadStdlib(stdlibCtx, cfg.StdlibDir) + } + cfg.vmKpr.CommitGnoTransactionStore(stdlibCtx) + stdlibCtx.MultiStore().MultiWrite() + } + + ctx.Logger().Debug("InitChainer: standard libraries loaded", + "elapsed", time.Since(start)) + + txResponses := []abci.ResponseDeliverTx{} - // Run genesis txs - for _, tx := range genState.Txs { - res := baseApp.Deliver(tx) - if res.IsErr() { - ctx.Logger().Error( - "Unable to deliver genesis tx", - "log", res.Log, - "error", res.Error, - "gas-used", res.GasUsed, - ) - } - - txResponses = append(txResponses, abci.ResponseDeliverTx{ - ResponseBase: res.ResponseBase, - GasWanted: res.GasWanted, - GasUsed: res.GasUsed, - }) - - resHandler(ctx, tx, res) + if req.AppState != nil { + // Get genesis state + genState := req.AppState.(GnoGenesisState) + + // Parse and set genesis state balances + for _, bal := range genState.Balances { + acc := cfg.acctKpr.NewAccountWithAddress(ctx, bal.Address) + cfg.acctKpr.SetAccount(ctx, acc) + err := cfg.bankKpr.SetCoins(ctx, bal.Address, bal.Amount) + if err != nil { + panic(err) } } - // Done! - return abci.ResponseInitChain{ - Validators: req.Validators, - TxResponses: txResponses, + // Run genesis txs + for _, tx := range genState.Txs { + res := cfg.baseApp.Deliver(tx) + if res.IsErr() { + ctx.Logger().Error( + "Unable to deliver genesis tx", + "log", res.Log, + "error", res.Error, + "gas-used", res.GasUsed, + ) + } + + txResponses = append(txResponses, abci.ResponseDeliverTx{ + ResponseBase: res.ResponseBase, + GasWanted: res.GasWanted, + GasUsed: res.GasUsed, + }) + + cfg.GenesisTxResultHandler(ctx, tx, res) } } + + ctx.Logger().Debug("InitChainer: genesis transactions loaded", + "elapsed", time.Since(start)) + + // Done! + return abci.ResponseInitChain{ + Validators: req.Validators, + TxResponses: txResponses, + } } // endBlockerApp is the app abstraction required by any EndBlocker diff --git a/gno.land/pkg/gnoland/mock_test.go b/gno.land/pkg/gnoland/mock_test.go index 1ff9f168bd1..53aed44a21f 100644 --- a/gno.land/pkg/gnoland/mock_test.go +++ b/gno.land/pkg/gnoland/mock_test.go @@ -53,6 +53,7 @@ type ( ) type mockVMKeeper struct { + vm.VMKeeperI addPackageFn addPackageDelegate callFn callDelegate queryFn queryEvalDelegate diff --git a/gno.land/pkg/gnoland/node_inmemory.go b/gno.land/pkg/gnoland/node_inmemory.go index 02691f89c3e..c4a35b2b1de 100644 --- a/gno.land/pkg/gnoland/node_inmemory.go +++ b/gno.land/pkg/gnoland/node_inmemory.go @@ -3,6 +3,7 @@ package gnoland import ( "fmt" "log/slog" + "path/filepath" "time" abci "github.com/gnolang/gno/tm2/pkg/bft/abci/types" @@ -22,8 +23,10 @@ type InMemoryNodeConfig struct { PrivValidator bft.PrivValidator // identity of the validator Genesis *bft.GenesisDoc TMConfig *tmcfg.Config - GenesisTxHandler GenesisTxHandler GenesisMaxVMCycles int64 + + // If StdlibDir not set, then it's filepath.Join(TMConfig.RootDir, "gnovm", "stdlibs") + InitChainerConfig } // NewMockedPrivValidator generate a new key @@ -70,7 +73,7 @@ func (cfg *InMemoryNodeConfig) validate() error { return fmt.Errorf("`TMConfig.RootDir` is required to locate `stdlibs` directory") } - if cfg.GenesisTxHandler == nil { + if cfg.GenesisTxResultHandler == nil { return fmt.Errorf("`GenesisTxHandler` is required but not provided") } @@ -87,15 +90,17 @@ func NewInMemoryNode(logger *slog.Logger, cfg *InMemoryNodeConfig) (*node.Node, evsw := events.NewEventSwitch() + if cfg.StdlibDir == "" { + cfg.StdlibDir = filepath.Join(cfg.TMConfig.RootDir, "gnovm", "stdlibs") + } + // Initialize the application with the provided options gnoApp, err := NewAppWithOptions(&AppOptions{ - Logger: logger, - GnoRootDir: cfg.TMConfig.RootDir, - GenesisTxHandler: cfg.GenesisTxHandler, - MaxCycles: cfg.GenesisMaxVMCycles, - DB: memdb.NewMemDB(), - EventSwitch: evsw, - CacheStdlibLoad: true, + Logger: logger, + MaxCycles: cfg.GenesisMaxVMCycles, + DB: memdb.NewMemDB(), + EventSwitch: evsw, + InitChainerConfig: cfg.InitChainerConfig, }) if err != nil { return nil, fmt.Errorf("error initializing new app: %w", err) diff --git a/gno.land/pkg/integration/testing_node.go b/gno.land/pkg/integration/testing_node.go index 993386f6b04..4b90ddac7c4 100644 --- a/gno.land/pkg/integration/testing_node.go +++ b/gno.land/pkg/integration/testing_node.go @@ -71,10 +71,14 @@ func TestingMinimalNodeConfig(t TestingTS, gnoroot string) *gnoland.InMemoryNode genesis := DefaultTestingGenesisConfig(t, gnoroot, pv.GetPubKey(), tmconfig) return &gnoland.InMemoryNodeConfig{ - PrivValidator: pv, - Genesis: genesis, - TMConfig: tmconfig, - GenesisTxHandler: gnoland.PanicOnFailingTxHandler, + PrivValidator: pv, + Genesis: genesis, + TMConfig: tmconfig, + InitChainerConfig: gnoland.InitChainerConfig{ + GenesisTxResultHandler: gnoland.PanicOnFailingTxResultHandler, + CacheStdlibLoad: true, + // StdlibDir automatically set + }, } } diff --git a/gno.land/pkg/sdk/vm/common_test.go b/gno.land/pkg/sdk/vm/common_test.go index 6dd8050d6b6..2af3fc1dac6 100644 --- a/gno.land/pkg/sdk/vm/common_test.go +++ b/gno.land/pkg/sdk/vm/common_test.go @@ -35,22 +35,24 @@ func setupTestEnvCold() testEnv { func _setupTestEnv(cacheStdlibs bool) testEnv { db := memdb.NewMemDB() - baseCapKey := store.NewStoreKey("baseCapKey") iavlCapKey := store.NewStoreKey("iavlCapKey") + baseCapKey := store.NewStoreKey("baseCapKey") + // Mount db store and iavlstore ms := store.NewCommitMultiStore(db) - ms.MountStoreWithDB(baseCapKey, dbadapter.StoreConstructor, db) ms.MountStoreWithDB(iavlCapKey, iavl.StoreConstructor, db) + ms.MountStoreWithDB(baseCapKey, dbadapter.StoreConstructor, db) ms.LoadLatestVersion() ctx := sdk.NewContext(sdk.RunTxModeDeliver, ms, &bft.Header{ChainID: "test-chain-id"}, log.NewNoopLogger()) acck := authm.NewAccountKeeper(iavlCapKey, std.ProtoBaseAccount) bank := bankm.NewBankKeeper(acck) - stdlibsDir := filepath.Join("..", "..", "..", "..", "gnovm", "stdlibs") - vmk := NewVMKeeper(baseCapKey, iavlCapKey, acck, bank, stdlibsDir, 100_000_000) + vmk := NewVMKeeper(baseCapKey, iavlCapKey, acck, bank, 100_000_000) mcw := ms.MultiCacheWrap() - vmk.Initialize(log.NewNoopLogger(), mcw, cacheStdlibs) + vmk.Initialize(log.NewNoopLogger(), mcw) + stdlibsDir := filepath.Join("..", "..", "..", "..", "gnovm", "stdlibs") + vmk.LoadStdlibCached(ctx, stdlibsDir) mcw.MultiWrite() return testEnv{ctx: ctx, vmk: vmk, bank: bank, acck: acck} diff --git a/gno.land/pkg/sdk/vm/keeper.go b/gno.land/pkg/sdk/vm/keeper.go index 934c4557dd0..fa73e0c75fe 100644 --- a/gno.land/pkg/sdk/vm/keeper.go +++ b/gno.land/pkg/sdk/vm/keeper.go @@ -34,8 +34,8 @@ import ( ) const ( - maxAllocTx = 500 * 1000 * 1000 - maxAllocQuery = 1500 * 1000 * 1000 // higher limit for queries + maxAllocTx = 500_000_000 + maxAllocQuery = 1_500_000_000 // higher limit for queries ) // vm.VMKeeperI defines a module interface that supports Gno @@ -45,17 +45,20 @@ type VMKeeperI interface { Call(ctx sdk.Context, msg MsgCall) (res string, err error) QueryEval(ctx sdk.Context, pkgPath string, expr string) (res string, err error) Run(ctx sdk.Context, msg MsgRun) (res string, err error) + LoadStdlib(ctx sdk.Context, stdlibDir string) + LoadStdlibCached(ctx sdk.Context, stdlibDir string) + MakeGnoTransactionStore(ctx sdk.Context) sdk.Context + CommitGnoTransactionStore(ctx sdk.Context) } var _ VMKeeperI = &VMKeeper{} // VMKeeper holds all package code and store state. type VMKeeper struct { - baseKey store.StoreKey - iavlKey store.StoreKey - acck auth.AccountKeeper - bank bank.BankKeeper - stdlibsDir string + baseKey store.StoreKey + iavlKey store.StoreKey + acck auth.AccountKeeper + bank bank.BankKeeper // cached, the DeliverTx persistent state. gnoStore gno.Store @@ -69,17 +72,15 @@ func NewVMKeeper( iavlKey store.StoreKey, acck auth.AccountKeeper, bank bank.BankKeeper, - stdlibsDir string, maxCycles int64, ) *VMKeeper { // TODO: create an Options struct to avoid too many constructor parameters vmk := &VMKeeper{ - baseKey: baseKey, - iavlKey: iavlKey, - acck: acck, - bank: bank, - stdlibsDir: stdlibsDir, - maxCycles: maxCycles, + baseKey: baseKey, + iavlKey: iavlKey, + acck: acck, + bank: bank, + maxCycles: maxCycles, } return vmk } @@ -87,7 +88,6 @@ func NewVMKeeper( func (vm *VMKeeper) Initialize( logger *slog.Logger, ms store.MultiStore, - cacheStdlibLoad bool, ) { if vm.gnoStore != nil { panic("should not happen") @@ -95,61 +95,21 @@ func (vm *VMKeeper) Initialize( baseSDKStore := ms.GetStore(vm.baseKey) iavlSDKStore := ms.GetStore(vm.iavlKey) - if cacheStdlibLoad { - // Testing case (using the cache speeds up starting many nodes) - vm.gnoStore = cachedStdlibLoad(vm.stdlibsDir, baseSDKStore, iavlSDKStore) - } else { - // On-chain case - vm.gnoStore = uncachedPackageLoad(logger, vm.stdlibsDir, baseSDKStore, iavlSDKStore) - } -} - -func uncachedPackageLoad( - logger *slog.Logger, - stdlibsDir string, - baseStore, iavlStore store.Store, -) gno.Store { alloc := gno.NewAllocator(maxAllocTx) - gnoStore := gno.NewStore(alloc, baseStore, iavlStore) - gnoStore.SetNativeStore(stdlibs.NativeStore) - if gnoStore.NumMemPackages() == 0 { - // No packages in the store; set up the stdlibs. - start := time.Now() - - loadStdlib(stdlibsDir, gnoStore) + vm.gnoStore = gno.NewStore(alloc, baseSDKStore, iavlSDKStore) + vm.gnoStore.SetNativeStore(stdlibs.NativeStore) - // XXX Quick and dirty to make this function work on non-validator nodes - iter := iavlStore.Iterator(nil, nil) - for ; iter.Valid(); iter.Next() { - baseStore.Set(append(iavlBackupPrefix, iter.Key()...), iter.Value()) - } - iter.Close() - - logger.Debug("Standard libraries initialized", - "elapsed", time.Since(start)) - } else { + if vm.gnoStore.NumMemPackages() > 0 { // for now, all mem packages must be re-run after reboot. // TODO remove this, and generally solve for in-mem garbage collection // and memory management across many objects/types/nodes/packages. start := time.Now() - // XXX Quick and dirty to make this function work on non-validator nodes - if isStoreEmpty(iavlStore) { - iter := baseStore.Iterator(iavlBackupPrefix, nil) - for ; iter.Valid(); iter.Next() { - if !bytes.HasPrefix(iter.Key(), iavlBackupPrefix) { - break - } - iavlStore.Set(iter.Key()[len(iavlBackupPrefix):], iter.Value()) - } - iter.Close() - } - m2 := gno.NewMachineWithOptions( gno.MachineOptions{ PkgPath: "", Output: os.Stdout, // XXX - Store: gnoStore, + Store: vm.gnoStore, }) defer m2.Release() gno.DisableDebug() @@ -159,57 +119,38 @@ func uncachedPackageLoad( logger.Debug("GnoVM packages preprocessed", "elapsed", time.Since(start)) } - return gnoStore + return } -var iavlBackupPrefix = []byte("init_iavl_backup:") - -func isStoreEmpty(st store.Store) bool { - iter := st.Iterator(nil, nil) - defer iter.Close() - for ; iter.Valid(); iter.Next() { - return false - } - return true -} +var ( + cachedStdlibOnce sync.Once + cachedStdlibBase store.Store + cachedStdlibIavl store.Store + cachedGnoStore gno.Store +) -func cachedStdlibLoad(stdlibsDir string, baseStore, iavlStore store.Store) gno.Store { +// LoadStdlib loads the Gno standard library into the given store. +func (vm *VMKeeper) LoadStdlibCached(ctx sdk.Context, stdlibDir string) { cachedStdlibOnce.Do(func() { - cachedStdlibBase = memdb.NewMemDB() - cachedStdlibIavl = memdb.NewMemDB() + cachedStdlibBase = dbadapter.StoreConstructor(memdb.NewMemDB(), types.StoreOptions{}) + cachedStdlibIavl = dbadapter.StoreConstructor(memdb.NewMemDB(), types.StoreOptions{}) - cachedGnoStore = gno.NewStore(nil, - dbadapter.StoreConstructor(cachedStdlibBase, types.StoreOptions{}), - dbadapter.StoreConstructor(cachedStdlibIavl, types.StoreOptions{})) + cachedGnoStore = gno.NewStore(nil, cachedStdlibBase, cachedStdlibIavl) cachedGnoStore.SetNativeStore(stdlibs.NativeStore) - loadStdlib(stdlibsDir, cachedGnoStore) + loadStdlib(cachedGnoStore, stdlibDir) }) - itr := cachedStdlibBase.Iterator(nil, nil) - for ; itr.Valid(); itr.Next() { - baseStore.Set(itr.Key(), itr.Value()) - } - - itr = cachedStdlibIavl.Iterator(nil, nil) - for ; itr.Valid(); itr.Next() { - iavlStore.Set(itr.Key(), itr.Value()) - } - - alloc := gno.NewAllocator(maxAllocTx) - gs := gno.NewStore(alloc, baseStore, iavlStore) - gs.SetNativeStore(stdlibs.NativeStore) - gno.CopyCachesFromStore(gs, cachedGnoStore) - return gs + gs := vm.getGnoTransactionStore(ctx) + gno.CopyFromCachedStore(gs, cachedGnoStore, cachedStdlibBase, cachedStdlibIavl) } -var ( - cachedStdlibOnce sync.Once - cachedStdlibBase *memdb.MemDB - cachedStdlibIavl *memdb.MemDB - cachedGnoStore gno.Store -) +// LoadStdlib loads the Gno standard library into the given store. +func (vm *VMKeeper) LoadStdlib(ctx sdk.Context, stdlibDir string) { + gs := vm.getGnoTransactionStore(ctx) + loadStdlib(gs, stdlibDir) +} -func loadStdlib(stdlibsDir string, store gno.Store) { +func loadStdlib(store gno.Store, stdlibDir string) { stdlibInitList := stdlibs.InitOrder() for _, lib := range stdlibInitList { if lib == "testing" { @@ -217,12 +158,12 @@ func loadStdlib(stdlibsDir string, store gno.Store) { // like fmt and encoding/json continue } - loadStdlibPackage(lib, stdlibsDir, store) + loadStdlibPackage(lib, stdlibDir, store) } } -func loadStdlibPackage(pkgPath, stdlibsDir string, store gno.Store) { - stdlibPath := filepath.Join(stdlibsDir, pkgPath) +func loadStdlibPackage(pkgPath, stdlibDir string, store gno.Store) { + stdlibPath := filepath.Join(stdlibDir, pkgPath) if !osm.DirExists(stdlibPath) { // does not exist. panic(fmt.Sprintf("failed loading stdlib %q: does not exist", pkgPath)) @@ -243,40 +184,24 @@ func loadStdlibPackage(pkgPath, stdlibsDir string, store gno.Store) { m.RunMemPackage(memPkg, true) } -func (vm *VMKeeper) getGnoStore(ctx sdk.Context) gno.Store { - // construct main store if nil. - if vm.gnoStore == nil { - panic("VMKeeper must first be initialized") - } - switch ctx.Mode() { - case sdk.RunTxModeDeliver: - // swap sdk store of existing store. - // this is needed due to e.g. gas wrappers. - baseSDKStore := ctx.Store(vm.baseKey) - iavlSDKStore := ctx.Store(vm.iavlKey) - vm.gnoStore.SwapStores(baseSDKStore, iavlSDKStore) - // clear object cache for every transaction. - // NOTE: this is inefficient, but simple. - // in the future, replace with more advanced caching strategy. - vm.gnoStore.ClearObjectCache() - return vm.gnoStore - case sdk.RunTxModeCheck: - // For query??? XXX Why not RunTxModeQuery? - simStore := vm.gnoStore.Fork() - baseSDKStore := ctx.Store(vm.baseKey) - iavlSDKStore := ctx.Store(vm.iavlKey) - simStore.SwapStores(baseSDKStore, iavlSDKStore) - return simStore - case sdk.RunTxModeSimulate: - // always make a new store for simulate for isolation. - simStore := vm.gnoStore.Fork() - baseSDKStore := ctx.Store(vm.baseKey) - iavlSDKStore := ctx.Store(vm.iavlKey) - simStore.SwapStores(baseSDKStore, iavlSDKStore) - return simStore - default: - panic("should not happen") - } +type gnoStoreContextKeyType struct{} + +var gnoStoreContextKey gnoStoreContextKeyType + +func (vm *VMKeeper) MakeGnoTransactionStore(ctx sdk.Context) sdk.Context { + ms := ctx.MultiStore() + base := ms.GetStore(vm.baseKey) + iavl := ms.GetStore(vm.iavlKey) + st := vm.gnoStore.BeginTransaction(base, iavl) + return ctx.WithValue(gnoStoreContextKey, st) +} + +func (vm *VMKeeper) CommitGnoTransactionStore(ctx sdk.Context) { + vm.getGnoTransactionStore(ctx).Write() +} + +func (vm *VMKeeper) getGnoTransactionStore(ctx sdk.Context) gno.TransactionStore { + return ctx.Value(gnoStoreContextKey).(gno.TransactionStore) } // Namespace can be either a user or crypto address. @@ -286,7 +211,7 @@ var reNamespace = regexp.MustCompile(`^gno.land/(?:r|p)/([\.~_a-zA-Z0-9]+)`) func (vm *VMKeeper) checkNamespacePermission(ctx sdk.Context, creator crypto.Address, pkgPath string) error { const sysUsersPkg = "gno.land/r/sys/users" - store := vm.getGnoStore(ctx) + store := vm.getGnoTransactionStore(ctx) match := reNamespace.FindStringSubmatch(pkgPath) switch len(match) { @@ -367,7 +292,7 @@ func (vm *VMKeeper) AddPackage(ctx sdk.Context, msg MsgAddPackage) (err error) { pkgPath := msg.Package.Path memPkg := msg.Package deposit := msg.Deposit - gnostore := vm.getGnoStore(ctx) + gnostore := vm.getGnoTransactionStore(ctx) // Validate arguments. if creator.IsZero() { @@ -464,7 +389,7 @@ func (vm *VMKeeper) AddPackage(ctx sdk.Context, msg MsgAddPackage) (err error) { func (vm *VMKeeper) Call(ctx sdk.Context, msg MsgCall) (res string, err error) { pkgPath := msg.PkgPath // to import fnc := msg.Func - gnostore := vm.getGnoStore(ctx) + gnostore := vm.getGnoTransactionStore(ctx) // Get the package and function type. pv := gnostore.GetPackage(pkgPath, false) pl := gno.PackageNodeLocation(pkgPath) @@ -575,7 +500,7 @@ func (vm *VMKeeper) Call(ctx sdk.Context, msg MsgCall) (res string, err error) { func (vm *VMKeeper) Run(ctx sdk.Context, msg MsgRun) (res string, err error) { caller := msg.Caller pkgAddr := caller - gnostore := vm.getGnoStore(ctx) + gnostore := vm.getGnoTransactionStore(ctx) send := msg.Send memPkg := msg.Package @@ -689,7 +614,8 @@ func (vm *VMKeeper) Run(ctx sdk.Context, msg MsgRun) (res string, err error) { // QueryFuncs returns public facing function signatures. func (vm *VMKeeper) QueryFuncs(ctx sdk.Context, pkgPath string) (fsigs FunctionSignatures, err error) { - store := vm.getGnoStore(ctx) + ctx = vm.MakeGnoTransactionStore(ctx) // throwaway (never committed) + store := vm.getGnoTransactionStore(ctx) // Ensure pkgPath is realm. if !gno.IsRealmPath(pkgPath) { err = ErrInvalidPkgPath(fmt.Sprintf( @@ -751,8 +677,9 @@ func (vm *VMKeeper) QueryFuncs(ctx sdk.Context, pkgPath string) (fsigs FunctionS // TODO: modify query protocol to allow MsgEval. // TODO: then, rename to "Eval". func (vm *VMKeeper) QueryEval(ctx sdk.Context, pkgPath string, expr string) (res string, err error) { + ctx = vm.MakeGnoTransactionStore(ctx) // throwaway (never committed) alloc := gno.NewAllocator(maxAllocQuery) - gnostore := vm.getGnoStore(ctx) + gnostore := vm.getGnoTransactionStore(ctx) pkgAddr := gno.DerivePkgAddr(pkgPath) // Get Package. pv := gnostore.GetPackage(pkgPath, false) @@ -818,8 +745,9 @@ func (vm *VMKeeper) QueryEval(ctx sdk.Context, pkgPath string, expr string) (res // TODO: modify query protocol to allow MsgEval. // TODO: then, rename to "EvalString". func (vm *VMKeeper) QueryEvalString(ctx sdk.Context, pkgPath string, expr string) (res string, err error) { + ctx = vm.MakeGnoTransactionStore(ctx) // throwaway (never committed) alloc := gno.NewAllocator(maxAllocQuery) - gnostore := vm.getGnoStore(ctx) + gnostore := vm.getGnoTransactionStore(ctx) pkgAddr := gno.DerivePkgAddr(pkgPath) // Get Package. pv := gnostore.GetPackage(pkgPath, false) @@ -880,7 +808,8 @@ func (vm *VMKeeper) QueryEvalString(ctx sdk.Context, pkgPath string, expr string } func (vm *VMKeeper) QueryFile(ctx sdk.Context, filepath string) (res string, err error) { - store := vm.getGnoStore(ctx) + ctx = vm.MakeGnoTransactionStore(ctx) // throwaway (never committed) + store := vm.getGnoTransactionStore(ctx) dirpath, filename := std.SplitFilepath(filepath) if filename != "" { memFile := store.GetMemFile(dirpath, filename) diff --git a/gno.land/pkg/sdk/vm/keeper_test.go b/gno.land/pkg/sdk/vm/keeper_test.go index a86ca5e4a97..30e7a5018fe 100644 --- a/gno.land/pkg/sdk/vm/keeper_test.go +++ b/gno.land/pkg/sdk/vm/keeper_test.go @@ -36,12 +36,12 @@ func Echo() string {return "hello world"}`, } pkgPath := "gno.land/r/test" msg1 := NewMsgAddPackage(addr, pkgPath, files) - assert.Nil(t, env.vmk.gnoStore.GetPackage(pkgPath, false)) + assert.Nil(t, env.vmk.getGnoTransactionStore(ctx).GetPackage(pkgPath, false)) err := env.vmk.AddPackage(ctx, msg1) assert.NoError(t, err) - assert.NotNil(t, env.vmk.gnoStore.GetPackage(pkgPath, false)) + assert.NotNil(t, env.vmk.getGnoTransactionStore(ctx).GetPackage(pkgPath, false)) err = env.vmk.AddPackage(ctx, msg1) @@ -49,7 +49,7 @@ func Echo() string {return "hello world"}`, assert.True(t, errors.Is(err, InvalidPkgPathError{})) // added package is formatted - store := vmk.getGnoStore(ctx) + store := vmk.getGnoTransactionStore(ctx) memFile := store.GetMemFile("gno.land/r/test", "test.gno") assert.NotNil(t, memFile) expected := `package test diff --git a/gnovm/pkg/gnolang/preprocess.go b/gnovm/pkg/gnolang/preprocess.go index d21e9bf0efd..225f1fe3a8b 100644 --- a/gnovm/pkg/gnolang/preprocess.go +++ b/gnovm/pkg/gnolang/preprocess.go @@ -2405,7 +2405,7 @@ func evalStaticType(store Store, last BlockNode, x Expr) Type { // See comment in evalStaticTypeOfRaw. if store != nil && pn.PkgPath != uversePkgPath { pv := pn.NewPackage() // temporary - store = store.Fork() + store = store.preprocessFork() store.SetCachePackage(pv) } m := NewMachine(pn.PkgPath, store) @@ -2479,7 +2479,7 @@ func evalStaticTypeOfRaw(store Store, last BlockNode, x Expr) (t Type) { // yet predefined this time around. if store != nil && pn.PkgPath != uversePkgPath { pv := pn.NewPackage() // temporary - store = store.Fork() + store = store.preprocessFork() store.SetCachePackage(pv) } m := NewMachine(pn.PkgPath, store) diff --git a/gnovm/pkg/gnolang/realm.go b/gnovm/pkg/gnolang/realm.go index 3710524130a..9492740c19d 100644 --- a/gnovm/pkg/gnolang/realm.go +++ b/gnovm/pkg/gnolang/realm.go @@ -1234,7 +1234,7 @@ func copyValueWithRefs(val Value) Value { // fillTypes // (fully) fills the type. -func fillType(store Store, typ Type) Type { +func fillType(store TypeStore, typ Type) Type { switch ct := typ.(type) { case nil: return nil @@ -1317,14 +1317,14 @@ func fillType(store Store, typ Type) Type { } } -func fillTypesTV(store Store, tv *TypedValue) { +func fillTypesTV(store ObjectTypeStore, tv *TypedValue) { tv.T = fillType(store, tv.T) tv.V = fillTypesOfValue(store, tv.V) } // Partially fills loaded objects shallowly, similarly to // getUnsavedTypes. Replaces all RefTypes with corresponding types. -func fillTypesOfValue(store Store, val Value) Value { +func fillTypesOfValue(store ObjectTypeStore, val Value) Value { switch cv := val.(type) { case nil: // do nothing return cv diff --git a/gnovm/pkg/gnolang/store.go b/gnovm/pkg/gnolang/store.go index 038f4ba894b..944f98399ca 100644 --- a/gnovm/pkg/gnolang/store.go +++ b/gnovm/pkg/gnolang/store.go @@ -12,7 +12,6 @@ import ( "github.com/gnolang/gno/tm2/pkg/colors" "github.com/gnolang/gno/tm2/pkg/std" "github.com/gnolang/gno/tm2/pkg/store" - "github.com/gnolang/gno/tm2/pkg/store/types" "github.com/gnolang/gno/tm2/pkg/store/utils" stringz "github.com/gnolang/gno/tm2/pkg/strings" ) @@ -32,8 +31,12 @@ type PackageInjector func(store Store, pn *PackageNode) // NativeStore is a function which can retrieve native bodies of native functions. type NativeStore func(pkgName string, name Name) func(m *Machine) +// Store is the central interface that specifies the communications between the +// GnoVM and the underlying data store; currently, generally the Gno.land +// blockchain, or the file system. type Store interface { // STABLE + BeginTransaction(baseStore, iavlStore store.Store) TransactionStore SetPackageGetter(PackageGetter) GetPackage(pkgPath string, isImport bool) *PackageValue SetCachePackage(*PackageValue) @@ -62,9 +65,6 @@ type Store interface { GetMemPackage(path string) *std.MemPackage GetMemFile(path string, name string) *std.MemFile IterMemPackage() <-chan *std.MemPackage - ClearObjectCache() // for each delivertx. - Fork() Store // for checktx, simulate, and queries. - SwapStores(baseStore, iavlStore store.Store) // for gas wrappers. SetPackageInjector(PackageInjector) // for natives SetNativeStore(NativeStore) // for "new" natives XXX GetNative(pkgPath string, name Name) func(m *Machine) // for "new" natives XXX @@ -73,53 +73,238 @@ type Store interface { LogSwitchRealm(rlmpath string) // to mark change of realm boundaries ClearCache() Print() + + preprocessFork() Store +} + +// TransactionStore is a store where the operations modifying the underlying store's +// caches are temporarily held in a buffer, and then executed together after +// executing Write. +type TransactionStore interface { + Store + + // Write commits the current buffered transaction data to the underlying store. + // It also clears the current buffer of the transaction. Write() - Flush() } -// Used to keep track of in-mem objects during tx. type defaultStore struct { - alloc *Allocator // for accounting for cached items - pkgGetter PackageGetter // non-realm packages - cacheObjects map[ObjectID]Object - cacheTypes map[TypeID]Type - cacheNodes map[Location]BlockNode - cacheNativeTypes map[reflect.Type]Type // go spec: reflect.Type are comparable - baseStore store.Store // for objects, types, nodes - iavlStore store.Store // for escaped object hashes + // underlying stores used to keep data + baseStore store.Store // for objects, types, nodes + iavlStore store.Store // for escaped object hashes + + // transaction-scoped + parentStore *defaultStore // set only during transactions. + cacheObjects map[ObjectID]Object // this is a real cache, reset with every transaction. + cacheTypes bufferedTxMap[TypeID, Type] // this re-uses the parent store's. + cacheNodes bufferedTxMap[Location, BlockNode] // until BlockNode persistence is implemented, this is an actual store. + alloc *Allocator // for accounting for cached items + + // store configuration; cannot be modified in a transaction + pkgGetter PackageGetter // non-realm packages + cacheNativeTypes map[reflect.Type]Type // reflect doc: reflect.Type are comparable pkgInjector PackageInjector // for injecting natives nativeStore NativeStore // for injecting natives go2gnoStrict bool // if true, native->gno type conversion must be registered. + // XXX panic when changing these and parentStore != nil // transient - opslog []StoreOp // for debugging and testing. current []string // for detecting import cycles. + opslog []StoreOp // for debugging and testing. +} + +type bufferedTxMap[K comparable, V any] struct { + source map[K]V + dirty map[K]deletable[V] +} + +// init should be called when creating the bufferedTxMap, in a non-buffered +// context. +func (b *bufferedTxMap[K, V]) init() { + if b.dirty != nil { + panic("cannot init with a dirty buffer") + } + b.source = make(map[K]V) +} + +// buffered creates a copy of b, which has a usable dirty map. +func (b bufferedTxMap[K, V]) buffered() bufferedTxMap[K, V] { + if b.dirty != nil { + panic("cannot stack buffered tx maps") + } + return bufferedTxMap[K, V]{ + source: b.source, + dirty: make(map[K]deletable[V]), + } +} + +// write commits the data in dirty to the map in source. +func (b *bufferedTxMap[K, V]) write() { + for k, v := range b.dirty { + if v.deleted { + delete(b.source, k) + } else { + b.source[k] = v.v + } + } + b.dirty = make(map[K]deletable[V]) +} + +func (b bufferedTxMap[K, V]) Get(k K) (V, bool) { + if b.dirty != nil { + if bufValue, ok := b.dirty[k]; ok { + if bufValue.deleted { + var zeroV V + return zeroV, false + } + return bufValue.v, true + } + } + v, ok := b.source[k] + return v, ok +} + +func (b bufferedTxMap[K, V]) Set(k K, v V) { + if b.dirty == nil { + b.source[k] = v + return + } + b.dirty[k] = deletable[V]{v: v} +} + +func (b bufferedTxMap[K, V]) Delete(k K) { + if b.dirty == nil { + delete(b.source, k) + return + } + b.dirty[k] = deletable[V]{deleted: true} +} + +type deletable[V any] struct { + v V + deleted bool } func NewStore(alloc *Allocator, baseStore, iavlStore store.Store) *defaultStore { ds := &defaultStore{ - alloc: alloc, + baseStore: baseStore, + iavlStore: iavlStore, + alloc: alloc, + + // cacheObjects is set; objects in the store will be copied over for any transaction. + cacheObjects: make(map[ObjectID]Object), + + // store configuration pkgGetter: nil, - cacheObjects: make(map[ObjectID]Object), - cacheTypes: make(map[TypeID]Type), - cacheNodes: make(map[Location]BlockNode), cacheNativeTypes: make(map[reflect.Type]Type), - baseStore: baseStore, - iavlStore: iavlStore, + pkgInjector: nil, + nativeStore: nil, go2gnoStrict: true, } + ds.cacheTypes.init() + ds.cacheNodes.init() InitStoreCaches(ds) return ds } +// If nil baseStore and iavlStore, the baseStores are re-used. +func (ds *defaultStore) BeginTransaction(baseStore, iavlStore store.Store) TransactionStore { + if baseStore == nil { + baseStore = ds.baseStore + } + if iavlStore == nil { + iavlStore = ds.iavlStore + } + ds2 := &defaultStore{ + // underlying stores + baseStore: baseStore, + iavlStore: iavlStore, + + // transaction-scoped + parentStore: ds, + cacheObjects: maps.Clone(ds.cacheObjects), + cacheTypes: ds.cacheTypes.buffered(), + cacheNodes: ds.cacheNodes.buffered(), + alloc: ds.alloc.Fork().Reset(), + + // store configuration + pkgGetter: ds.pkgGetter, + cacheNativeTypes: ds.cacheNativeTypes, + pkgInjector: ds.pkgInjector, + nativeStore: ds.nativeStore, + go2gnoStrict: ds.go2gnoStrict, + + // transient + current: nil, + opslog: nil, + } + return transactionStore{ds2} +} + +func (ds *defaultStore) preprocessFork() Store { + // XXX IMPROVE + ds2 := &defaultStore{ + // underlying stores + baseStore: ds.baseStore, + iavlStore: ds.iavlStore, + + // transaction-scoped + parentStore: ds, + cacheObjects: make(map[ObjectID]Object), + cacheTypes: ds.cacheTypes, + cacheNodes: ds.cacheNodes, + alloc: ds.alloc.Fork().Reset(), + + // store configuration + pkgGetter: ds.pkgGetter, + cacheNativeTypes: ds.cacheNativeTypes, + pkgInjector: ds.pkgInjector, + nativeStore: ds.nativeStore, + go2gnoStrict: ds.go2gnoStrict, + + // transient + current: nil, + opslog: nil, + } + ds2.SetCachePackage(Uverse()) + return ds2 +} + +type transactionStore struct{ *defaultStore } + +func (t transactionStore) Write() { t.write() } + +// writes to parentStore. +func (ds *defaultStore) write() { + ds.cacheTypes.write() + ds.cacheNodes.write() +} + // CopyCachesFromStore allows to copy a store's internal object, type and // BlockNode cache into the dst store. // This is mostly useful for testing, where many stores have to be initialized. -func CopyCachesFromStore(dst, src Store) { - ds, ss := dst.(*defaultStore), src.(*defaultStore) - ds.cacheObjects = maps.Clone(ss.cacheObjects) - ds.cacheTypes = maps.Clone(ss.cacheTypes) - ds.cacheNodes = maps.Clone(ss.cacheNodes) +func CopyFromCachedStore(destStore, cachedStore Store, cachedBase, cachedIavl store.Store) { + ds, ss := destStore.(transactionStore), cachedStore.(*defaultStore) + + iter := cachedBase.Iterator(nil, nil) + for ; iter.Valid(); iter.Next() { + ds.baseStore.Set(iter.Key(), iter.Value()) + } + iter = cachedIavl.Iterator(nil, nil) + for ; iter.Valid(); iter.Next() { + ds.iavlStore.Set(iter.Key(), iter.Value()) + } + + if ss.cacheTypes.dirty != nil || + ss.cacheNodes.dirty != nil { + panic("cacheTypes and cacheNodes should be unbuffered") + } + for k, v := range ss.cacheTypes.source { + ds.cacheTypes.Set(k, v) + } + for k, v := range ss.cacheNodes.source { + ds.cacheNodes.Set(k, v) + } } func (ds *defaultStore) GetAllocator() *Allocator { @@ -127,6 +312,9 @@ func (ds *defaultStore) GetAllocator() *Allocator { } func (ds *defaultStore) SetPackageGetter(pg PackageGetter) { + if ds.parentStore != nil { + panic("package getter cannot be modified in a transaction") + } ds.pkgGetter = pg } @@ -407,7 +595,7 @@ func (ds *defaultStore) GetType(tid TypeID) Type { func (ds *defaultStore) GetTypeSafe(tid TypeID) Type { // check cache. - if tt, exists := ds.cacheTypes[tid]; exists { + if tt, exists := ds.cacheTypes.Get(tid); exists { return tt } // check backend. @@ -424,7 +612,7 @@ func (ds *defaultStore) GetTypeSafe(tid TypeID) Type { } } // set in cache. - ds.cacheTypes[tid] = tt + ds.cacheTypes.Set(tid, tt) // after setting in cache, fill tt. fillType(ds, tt) return tt @@ -435,7 +623,7 @@ func (ds *defaultStore) GetTypeSafe(tid TypeID) Type { func (ds *defaultStore) SetCacheType(tt Type) { tid := tt.TypeID() - if tt2, exists := ds.cacheTypes[tid]; exists { + if tt2, exists := ds.cacheTypes.Get(tid); exists { if tt != tt2 { // NOTE: not sure why this would happen. panic("should not happen") @@ -443,14 +631,14 @@ func (ds *defaultStore) SetCacheType(tt Type) { // already set. } } else { - ds.cacheTypes[tid] = tt + ds.cacheTypes.Set(tid, tt) } } func (ds *defaultStore) SetType(tt Type) { tid := tt.TypeID() // return if tid already known. - if tt2, exists := ds.cacheTypes[tid]; exists { + if tt2, exists := ds.cacheTypes.Get(tid); exists { if tt != tt2 { // this can happen for a variety of reasons. // TODO classify them and optimize. @@ -465,7 +653,7 @@ func (ds *defaultStore) SetType(tt Type) { ds.baseStore.Set([]byte(key), bz) } // save type to cache. - ds.cacheTypes[tid] = tt + ds.cacheTypes.Set(tid, tt) } func (ds *defaultStore) GetBlockNode(loc Location) BlockNode { @@ -478,7 +666,7 @@ func (ds *defaultStore) GetBlockNode(loc Location) BlockNode { func (ds *defaultStore) GetBlockNodeSafe(loc Location) BlockNode { // check cache. - if bn, exists := ds.cacheNodes[loc]; exists { + if bn, exists := ds.cacheNodes.Get(loc); exists { return bn } // check backend. @@ -494,7 +682,7 @@ func (ds *defaultStore) GetBlockNodeSafe(loc Location) BlockNode { loc, bn.GetLocation())) } } - ds.cacheNodes[loc] = bn + ds.cacheNodes.Set(loc, bn) return bn } } @@ -513,7 +701,7 @@ func (ds *defaultStore) SetBlockNode(bn BlockNode) { // ds.backend.Set([]byte(key), bz) } // save node to cache. - ds.cacheNodes[loc] = bn + ds.cacheNodes.Set(loc, bn) // XXX duplicate? // XXX } @@ -553,7 +741,7 @@ func (ds *defaultStore) incGetPackageIndexCounter() uint64 { func (ds *defaultStore) AddMemPackage(memPkg *std.MemPackage) { memPkg.Validate() // NOTE: duplicate validation. ctr := ds.incGetPackageIndexCounter() - idxkey := []byte(backendPackageIndexKey(ctr)) + idxkey := []byte(backendPackageIndexKey(int64(ctr))) bz := amino.MustMarshal(memPkg) ds.baseStore.Set(idxkey, []byte(memPkg.Path)) pathkey := []byte(backendPackagePathKey(memPkg.Path)) @@ -582,6 +770,7 @@ func (ds *defaultStore) getMemPackage(path string, isRetry bool) *std.MemPackage } return nil } + var memPkg *std.MemPackage amino.MustUnmarshal(bz, &memPkg) return memPkg @@ -612,7 +801,7 @@ func (ds *defaultStore) IterMemPackage() <-chan *std.MemPackage { ch := make(chan *std.MemPackage, 0) go func() { for i := uint64(1); i <= uint64(ctr); i++ { - idxkey := []byte(backendPackageIndexKey(i)) + idxkey := []byte(backendPackageIndexKey(int64(i))) path := ds.baseStore.Get(idxkey) if path == nil { panic(fmt.Sprintf( @@ -627,51 +816,6 @@ func (ds *defaultStore) IterMemPackage() <-chan *std.MemPackage { } } -// Unstable. -// This function is used to clear the object cache every transaction. -// It also sets a new allocator. -func (ds *defaultStore) ClearObjectCache() { - ds.alloc.Reset() - ds.cacheObjects = make(map[ObjectID]Object) // new cache. - ds.opslog = nil // new ops log. - ds.SetCachePackage(Uverse()) -} - -// Unstable. -// This function is used to handle queries and checktx transactions. -func (ds *defaultStore) Fork() Store { - ds2 := &defaultStore{ - alloc: ds.alloc.Fork().Reset(), - - // Re-initialize caches. Some are cloned for speed. - cacheObjects: make(map[ObjectID]Object), - cacheTypes: maps.Clone(ds.cacheTypes), - // XXX: This is bad to say the least (ds.cacheNodes is shared with a - // child Store); however, cacheNodes is _not_ a cache, but a proper - // data store instead. SetBlockNode does not write anything to - // the underlying baseStore, and cloning this map makes everything run - // 4x slower, so here we are, copying the reference. - cacheNodes: ds.cacheNodes, - cacheNativeTypes: maps.Clone(ds.cacheNativeTypes), - - // baseStore and iavlStore should generally be changed using SwapStores. - baseStore: ds.baseStore, - iavlStore: ds.iavlStore, - - // native injections / store "config" - pkgGetter: ds.pkgGetter, - pkgInjector: ds.pkgInjector, - nativeStore: ds.nativeStore, - go2gnoStrict: ds.go2gnoStrict, - - // reset opslog and current. - opslog: nil, - current: nil, - } - ds2.SetCachePackage(Uverse()) - return ds2 -} - // TODO: consider a better/faster/simpler way of achieving the overall same goal? func (ds *defaultStore) SwapStores(baseStore, iavlStore store.Store) { ds.baseStore = baseStore @@ -693,18 +837,6 @@ func (ds *defaultStore) GetNative(pkgPath string, name Name) func(m *Machine) { return nil } -// Writes one level of cache to store. -func (ds *defaultStore) Write() { - ds.baseStore.(types.Writer).Write() - ds.iavlStore.(types.Writer).Write() -} - -// Flush cached writes to disk. -func (ds *defaultStore) Flush() { - ds.baseStore.(types.Flusher).Flush() - ds.iavlStore.(types.Flusher).Flush() -} - // ---------------------------------------- // StoreOp @@ -774,9 +906,12 @@ func (ds *defaultStore) LogSwitchRealm(rlmpath string) { } func (ds *defaultStore) ClearCache() { + if ds.parentStore != nil { + panic("ClearCache can only be called on non-transactional stores") + } ds.cacheObjects = make(map[ObjectID]Object) - ds.cacheTypes = make(map[TypeID]Type) - ds.cacheNodes = make(map[Location]BlockNode) + ds.cacheTypes.init() + ds.cacheNodes.init() ds.cacheNativeTypes = make(map[reflect.Type]Type) // restore builtin types to cache. InitStoreCaches(ds) @@ -792,16 +927,38 @@ func (ds *defaultStore) Print() { utils.Print(ds.iavlStore) fmt.Println(colors.Yellow("//----------------------------------------")) fmt.Println(colors.Green("defaultStore:cacheTypes...")) - for tid, typ := range ds.cacheTypes { + for tid, typ := range ds.cacheTypes.source { fmt.Printf("- %v: %v\n", tid, stringz.TrimN(fmt.Sprintf("%v", typ), 50)) } + if len(ds.cacheTypes.dirty) > 0 { + fmt.Println(colors.Green("defaultStore:cacheTypes (pending)...")) + for tid, typ := range ds.cacheTypes.dirty { + if typ.deleted { + fmt.Printf("- %v (deleted)\n", tid) + } else { + fmt.Printf("- %v: %v\n", tid, + stringz.TrimN(fmt.Sprintf("%v", typ.v), 50)) + } + } + } fmt.Println(colors.Yellow("//----------------------------------------")) fmt.Println(colors.Green("defaultStore:cacheNodes...")) - for loc, bn := range ds.cacheNodes { + for loc, bn := range ds.cacheNodes.source { fmt.Printf("- %v: %v\n", loc, stringz.TrimN(fmt.Sprintf("%v", bn), 50)) } + if len(ds.cacheNodes.dirty) > 0 { + fmt.Println(colors.Green("defaultStore:cacheNodes (pending)...")) + for tid, typ := range ds.cacheNodes.dirty { + if typ.deleted { + fmt.Printf("- %v (deleted)\n", tid) + } else { + fmt.Printf("- %v: %v\n", tid, + stringz.TrimN(fmt.Sprintf("%v", typ.v), 50)) + } + } + } fmt.Println(colors.Red("//----------------------------------------")) } @@ -829,7 +986,7 @@ func backendPackageIndexCtrKey() string { return fmt.Sprintf("pkgidx:counter") } -func backendPackageIndexKey(index uint64) string { +func backendPackageIndexKey(index int64) string { return fmt.Sprintf("pkgidx:%020d", index) } diff --git a/gnovm/pkg/gnolang/store2.go b/gnovm/pkg/gnolang/store2.go new file mode 100644 index 00000000000..07c522dcf47 --- /dev/null +++ b/gnovm/pkg/gnolang/store2.go @@ -0,0 +1,524 @@ +package gnolang + +import ( + "fmt" + "reflect" + "strconv" + + "github.com/gnolang/gno/tm2/pkg/amino" + "github.com/gnolang/gno/tm2/pkg/std" +) + +// KeyValueStore is a subset of tm2's store.Store. +type KeyValueStore interface { + // Get returns nil iff key doesn't exist. Panics on nil key. + Get(key []byte) []byte + + // Has checks if a key exists. Panics on nil key. + Has(key []byte) bool + + // Set sets the key. Panics on nil key or value. + Set(key, value []byte) + + // Delete deletes the key. Panics on nil key. + Delete(key []byte) +} + +// StoreOptions are the options which may be passed when creating a new store. +type StoreOptions struct { + // PackageInjector is deprecated. + // It is an old method to inject native code into existing packages. New code + // should use NativeResolver instead. + // + // TODO(morgan): remove with https://github.com/gnolang/gno/pull/1464 + PackageInjector func(pn *PackageNode) + + // NativeResolver is called to resolve the given combination of a pkgPath + // and function name to a native function. + NativeResolver func(pkgPath string, name Name) func(m *Machine) + + // Allocator is the store's allocator, which can limit how much the VM allocates. + // May be nil. + Allocator *Allocator + + // Go2GnoDefined allows mappings on defined types in the store. + // By default, Go2GnoType only works on unnamed types. If this is enabled, + // mappings will also be created and registered for named types. + // + // TODO(morgan): remove with https://github.com/gnolang/gno/issues/1361 + Go2GnoDefined bool + + // CachePackage is a package that should be directly added into the store's + // cache. Can be set for throwaway packages. + CachePackage *PackageValue +} + +// GetNative uses StoreOptions' NativeResolver to resolve a symbol to a native function. +// Implements FullStore. +func (s *StoreOptions) GetNative(pkgPath string, name Name) func(m *Machine) { + return s.NativeResolver(pkgPath, name) +} + +// GetAllocator returns the StoreOptions' allocator. +// Implements FullStore. +func (s *StoreOptions) GetAllocator() *Allocator { + return s.Allocator +} + +// PackageStore keeps GnoVM's packages and realms. +type PackageStore interface { + GetPackage(pkgPath string) *PackageValue + SetPackageRealm(*Realm) + + // getPackageRealm exists, but is currently unexported as it is only used + // internally in the store. You can likely use PackageValue.Realm. + // SetPackage does not exist - use SetObject instead. +} + +// NewPackageStore returns a new PackageStore. +// packageInjector is optional; all stores are required. +// realmStore is used to store the data for [Realm] values, typically a base store. +func NewPackageStore( + os ObjectStore, + bns BlockNodeStore, + realmStore KeyValueStore, + packageInjector func(pn *PackageNode), +) PackageStore { + return &packageStore{ + os: os, + bns: bns, + realmStore: realmStore, + packageInjector: packageInjector, + } +} + +type packageStore struct { + os ObjectStore + bns BlockNodeStore + realmStore KeyValueStore + packageInjector func(pn *PackageNode) +} + +var _ PackageStore = (*packageStore)(nil) + +func (ps *packageStore) GetPackage(pkgPath string) *PackageValue { + oid := ObjectIDFromPkgPath(pkgPath) + oo := ps.os.GetObject(oid) + if oo == nil { + // *PackageValue does not exist. + return nil + } + pv := oo.(*PackageValue) + if pv.fBlocksMap != nil { + // *PackageValue is already loaded. + return pv + } + // fBlocksMap == nil; the PackageValue needs to be initialized. + + // Resolve pv.Block to a *Block if it is a RefValue. + _ = pv.GetBlock(ps.os) + + // Get associated realm. + if pv.IsRealm() { + rlm := ps.getPackageRealm(pkgPath) + pv.Realm = rlm + } + + pl := PackageNodeLocation(pkgPath) + pn := ps.bns.GetBlockNode(pl).(*PackageNode) + + // Inject natives if applicable, and PrepareNewValues so we make sure + // PackageNode is up-to-date. Finally, re-derive FBlocksMap. + if ps.packageInjector != nil { + ps.packageInjector(pn) + pn.PrepareNewValues(pv) + } + pv.deriveFBlocksMap(ps.os) + + return pv +} + +func (ps *packageStore) getPackageRealm(pkgPath string) (rlm *Realm) { + oid := ObjectIDFromPkgPath(pkgPath) + key := backendRealmKey(oid) + bz := ps.realmStore.Get([]byte(key)) + if bz == nil { + return nil + } + amino.MustUnmarshal(bz, &rlm) + if debug { + if rlm.ID != oid.PkgID { + panic(fmt.Sprintf("unexpected realm id: expected %v but got %v", + oid.PkgID, rlm.ID)) + } + } + return rlm +} + +func (ps *packageStore) SetPackageRealm(rlm *Realm) { + oid := ObjectIDFromPkgPath(rlm.Path) + key := backendRealmKey(oid) + bz := amino.MustMarshal(rlm) + ps.realmStore.Set([]byte(key), bz) +} + +// MemPackageStore is a store which keeps track of all of all the underlying +// packages' source code. +type MemPackageStore interface { + NumMemPackages() int64 + AddMemPackage(memPkg *std.MemPackage) + GetMemPackage(path string) *std.MemPackage + // Can be used with Rangefunc: https://go.dev/wiki/RangefuncExperiment + IterMemPackage() func(yield func(*std.MemPackage) bool) +} + +// NewMemPackageStore creates a new [MemPackageStore], storing data in the given +// key/value stores. +// +// controlStore keeps track of the number of current mempackages and keeps an +// ordered list of all MemPackages in the order they were added. +// packageStore contains the actual package data. +func NewMemPackageStore(controlStore, packageStore KeyValueStore) MemPackageStore { + return &memPackageStore{controlStore, packageStore} +} + +type memPackageStore struct { + controlStore KeyValueStore + packageStore KeyValueStore +} + +var _ MemPackageStore = (*memPackageStore)(nil) + +func (mps *memPackageStore) NumMemPackages() int64 { + ctrkey := []byte(backendPackageIndexCtrKey()) + ctrbz := mps.controlStore.Get(ctrkey) + if ctrbz == nil { + return 0 + } else { + ctr, err := strconv.ParseInt(string(ctrbz), 10, 64) + if err != nil { + panic(err) + } + return ctr + } +} + +func (mps *memPackageStore) incrNumMemPackages() int64 { + num := mps.NumMemPackages() + num++ + + bz := strconv.FormatInt(num, 10) + ctrkey := []byte(backendPackageIndexCtrKey()) + mps.controlStore.Set(ctrkey, []byte(bz)) + return num +} + +func (mps *memPackageStore) AddMemPackage(memPkg *std.MemPackage) { + memPkg.Validate() // NOTE: duplicate validation. + ctr := mps.incrNumMemPackages() + idxkey := []byte(backendPackageIndexKey(ctr)) + bz := amino.MustMarshal(memPkg) + mps.controlStore.Set(idxkey, []byte(memPkg.Path)) + pathkey := []byte(backendPackagePathKey(memPkg.Path)) + mps.packageStore.Set(pathkey, bz) +} + +func (mps *memPackageStore) GetMemPackage(path string) *std.MemPackage { + pathkey := []byte(backendPackagePathKey(path)) + bz := mps.packageStore.Get(pathkey) + if bz == nil { + return nil + } + + var memPkg *std.MemPackage + amino.MustUnmarshal(bz, &memPkg) + return memPkg +} + +func (mps *memPackageStore) IterMemPackage() func(yield func(*std.MemPackage) bool) { + num := mps.NumMemPackages() + if num == 0 { + return func(_ func(*std.MemPackage) bool) {} + } + + return func(yield func(*std.MemPackage) bool) { + for i := int64(1); i <= num; i++ { + idxkey := []byte(backendPackageIndexKey(int64(i))) + path := mps.controlStore.Get(idxkey) + if path == nil { + panic(fmt.Sprintf( + "missing package index %d", i)) + } + + memPkg := mps.GetMemPackage(string(path)) + if !yield(memPkg) { + return + } + } + } +} + +// ObjectTypeStore is a combination of the Object and Type store, sometimes +// required by some functions. +type ObjectTypeStore interface { + ObjectStore + TypeStore +} + +type objectTypeStore struct { + ObjectStore + TypeStore +} + +// ObjectStore is a store which manages all [Object] values. +type ObjectStore interface { + // NOTE: does not initialize *PackageValues, so instead call GetPackage() + // for packages. + // NOTE: current implementation behavior requires + // all []TypedValue types and TypeValue{} types to be + // loaded (non-ref) types. + GetObject(oid ObjectID) Object + // NOTE: unlike GetObject(), SetObject() is also used to persist updated + // package values. + SetObject(Object) + DelObject(Object) +} + +var _ ObjectStore = (*objectStore)(nil) + +// NewObjectStore creates a new [ObjectStore]. +// kvStore is used to keep the objects themselves; escapedStore instead just +// contains the hashes of escaped objects. +// +// To correctly recover objects from the database, a typeStore is required. +// An allocator may be provided. +func NewObjectStore( + kvStore KeyValueStore, + escapedStore KeyValueStore, + ts TypeStore, + alloc *Allocator, +) ObjectStore { + return &objectStore{ + kvStore: kvStore, + escapedStore: escapedStore, + ts: ts, + alloc: alloc, + } +} + +type objectStoreCache struct { + parent ObjectStore + m map[ObjectID]Object +} + +var _ ObjectStore = (*objectStoreCache)(nil) + +func (os *objectStoreCache) GetObject(oid ObjectID) Object { + if obj, ok := os.m[oid]; ok { + return obj + } + os.parent.GetObject(oid) +} + +type objectStore struct { + kvStore KeyValueStore + escapedStore KeyValueStore + ts TypeStore + alloc *Allocator +} + +var _ ObjectStore = (*objectStore)(nil) + +func (os *objectStore) GetObject(oid ObjectID) Object { + key := backendObjectKey(oid) + hashbz := os.kvStore.Get([]byte(key)) + if hashbz == nil { + panic(fmt.Sprintf("unexpected object with id %s", oid.String())) + } + hash := hashbz[:HashSize] + bz := hashbz[HashSize:] + + var oo Object + os.alloc.AllocateAmino(int64(len(bz))) + amino.MustUnmarshal(bz, &oo) + if debug { + if oo.GetObjectID() != oid { + panic(fmt.Sprintf("unexpected object id: expected %v but got %v", + oid, oo.GetObjectID())) + } + } + oo.SetHash(ValueHash{NewHashlet(hash)}) + _ = fillTypesOfValue(objectTypeStore{ + ObjectStore: os, + TypeStore: os.ts, + }, oo) // XXX: os should be a cached type - find a way to have fillTypesOfValue cleanly. + if debug { + if _, ok := oo.(*PackageValue); ok { + panic("packages must be fetched with GetPackage()") + } + } + return oo +} + +func (ds *objectStore) SetObject(oo Object) { + oid := oo.GetObjectID() + // replace children/fields with Ref. + o2 := copyValueWithRefs(oo) + // marshal to binary. + bz := amino.MustMarshalAny(o2) + // set hash. + hash := HashBytes(bz) // XXX objectHash(bz)??? + if len(hash) != HashSize { + panic("should not happen") + } + oo.SetHash(ValueHash{hash}) + // save bytes to backend. + if ds.kvStore != nil { + key := backendObjectKey(oid) + hashbz := make([]byte, len(hash)+len(bz)) + copy(hashbz, hash.Bytes()) + copy(hashbz[HashSize:], bz) + ds.kvStore.Set([]byte(key), hashbz) + } + // save object to cache. + if debug { + if oid.IsZero() { + panic("object id cannot be zero") + } + } + // XXX: opslog + // if escaped, add hash to iavl. + if oo.GetIsEscaped() { + var key, value []byte + key = []byte(oid.String()) + value = hash.Bytes() + ds.escapedStore.Set(key, value) + } +} + +func (os *objectStore) DelObject(oo Object) { + oid := oo.GetObjectID() + // delete from backend. + key := backendObjectKey(oid) + os.kvStore.Delete([]byte(key)) + // XXX OPSLOG +} + +type TypeStore interface { + GetType(tid TypeID) Type + HasType(tid TypeID) bool + SetType(Type) +} + +// NewTypeStore creates a new [TypeStore], storing the types in the given +// kvStore. +func NewTypeStore(kvStore KeyValueStore) TypeStore { + return &typeStore{kvStore} +} + +type typeStore struct { + kvStore KeyValueStore +} + +func (ts *typeStore) GetType(tid TypeID) Type { + tt := ts.getTypeSafe(tid) + if tt == nil { + panic(fmt.Sprintf("unexpected type with id %s", tid.String())) + } + return tt +} + +func (ts *typeStore) HasType(tid TypeID) bool { + return ts.getTypeSafe(tid) != nil +} + +func (ts *typeStore) getTypeSafe(tid TypeID) Type { + // check backend. + key := backendTypeKey(tid) + bz := ts.kvStore.Get([]byte(key)) + if bz == nil { + return nil + } + + var tt Type + amino.MustUnmarshal(bz, &tt) + if debug { + if tt.TypeID() != tid { + panic(fmt.Sprintf("unexpected type id: expected %v but got %v", + tid, tt.TypeID())) + } + } + fillType(ts, tt) + + return tt +} + +func (ts *typeStore) SetType(tt Type) { + tid := tt.TypeID() + // save type to backend. + key := backendTypeKey(tid) + tcopy := copyTypeWithRefs(tt) + bz := amino.MustMarshalAny(tcopy) + ts.kvStore.Set([]byte(key), bz) +} + +type BlockNodeStore interface { + GetBlockNode(Location) BlockNode + SetBlockNode(BlockNode) +} + +func NewBlockNodeStore() BlockNodeStore { + return &blockNodeStore{ + m: make(map[Location]BlockNode), + } +} + +type blockNodeStore struct { + // XXX: this implementation should be changed + m map[Location]BlockNode +} + +func (bns *blockNodeStore) GetBlockNode(l Location) BlockNode { + return bns.m[l] +} + +func (bns *blockNodeStore) SetBlockNode(bn BlockNode) { + loc := bn.GetLocation() + if loc.IsZero() { + panic("unexpected zero location in blocknode") + } + bns.m[loc] = bn +} + +type FullStore interface { + PackageStore + MemPackageStore + ObjectStore + TypeStore + BlockNodeStore + + GetNative(pkgPath string, name Name) func(m *Machine) + GetAllocator() *Allocator + Go2GnoType(rt reflect.Type) Type +} + +// DebuggingStore is an interface with the debugging features of a store. +// A Store does not always implement this; to call a debugging function, +// a store should be type asserted to a DebuggingStore first. +type DebuggingStore interface { + SetLogStoreOps(enabled bool) + SprintStoreOps() string + LogSwitchRealm(rlmpath string) // to mark change of realm boundaries + ClearCache() + Print() +} + +type LocalStore interface { + FullStore + Begin(baseStore, iavlStore KeyValueStore) TransactionStore2 +} + +type TransactionStore2 interface { + FullStore + Write() +} diff --git a/gnovm/pkg/gnolang/values.go b/gnovm/pkg/gnolang/values.go index 5da7c15bb05..70a269900e3 100644 --- a/gnovm/pkg/gnolang/values.go +++ b/gnovm/pkg/gnolang/values.go @@ -767,7 +767,7 @@ func (mv *MapValue) GetLength() int { // Gno will, but here we just use this method signature as we // do for structs and arrays for assigning new entries. If key // doesn't exist, a new slot is created. -func (mv *MapValue) GetPointerForKey(alloc *Allocator, store Store, key *TypedValue) PointerValue { +func (mv *MapValue) GetPointerForKey(alloc *Allocator, store ObjectTypeStore, key *TypedValue) PointerValue { kmk := key.ComputeMapKey(store, false) if mli, ok := mv.vmap[kmk]; ok { key2 := key.Copy(alloc) @@ -829,6 +829,8 @@ type PackageValue struct { Realm *Realm `json:"-"` // if IsRealmPath(PkgPath), otherwise nil. // NOTE: Realm is persisted separately. + // Used to map ecah filename to its own FileBlock. + // Also used to determine whether a *PackageValue is loaded or not. fBlocksMap map[Name]*Block } @@ -845,7 +847,7 @@ func (pv *PackageValue) getFBlocksMap() map[Name]*Block { } // to call after loading *PackageValue. -func (pv *PackageValue) deriveFBlocksMap(store Store) { +func (pv *PackageValue) deriveFBlocksMap(store ObjectStore) { if pv.fBlocksMap != nil { panic("should not happen") } @@ -857,7 +859,7 @@ func (pv *PackageValue) deriveFBlocksMap(store Store) { } } -func (pv *PackageValue) GetBlock(store Store) *Block { +func (pv *PackageValue) GetBlock(store ObjectStore) *Block { bv := pv.Block switch bv := bv.(type) { case RefValue: @@ -892,7 +894,7 @@ func (pv *PackageValue) AddFileBlock(fn Name, fb *Block) { fb.SetOwner(pv) } -func (pv *PackageValue) GetFileBlock(store Store, fname Name) *Block { +func (pv *PackageValue) GetFileBlock(store ObjectStore, fname Name) *Block { if fb, ex := pv.getFBlocksMap()[fname]; ex { return fb } @@ -1591,7 +1593,7 @@ func (tv *TypedValue) isZero() bool { return false } -func (tv *TypedValue) ComputeMapKey(store Store, omitType bool) MapKey { +func (tv *TypedValue) ComputeMapKey(store ObjectTypeStore, omitType bool) MapKey { // Special case when nil: has no separator. if tv.T == nil { if debug { @@ -2620,7 +2622,7 @@ func typedString(s string) TypedValue { return tv } -func fillValueTV(store Store, tv *TypedValue) *TypedValue { +func fillValueTV(store ObjectTypeStore, tv *TypedValue) *TypedValue { switch cv := tv.V.(type) { case RefValue: if cv.PkgPath != "" { // load package diff --git a/tm2/pkg/sdk/abci.go b/tm2/pkg/sdk/abci.go index a9cd14e9ed3..4857541ea41 100644 --- a/tm2/pkg/sdk/abci.go +++ b/tm2/pkg/sdk/abci.go @@ -16,3 +16,11 @@ type BeginBlocker func(ctx Context, req abci.RequestBeginBlock) abci.ResponseBeg // Note: applications which set create_empty_blocks=false will not have regular block timing and should use // e.g. BFT timestamps rather than block height for any periodic EndBlock logic type EndBlocker func(ctx Context, req abci.RequestEndBlock) abci.ResponseEndBlock + +// BeginTxHook is a BaseApp-specific hook, called to modify the context with any +// additional application-specific information. +type BeginTxHook func(ctx Context) Context + +// EndTxHook is a BaseApp-specific hook, called after all the messages in a +// transaction have terminated. +type EndTxHook func(ctx Context, result Result) diff --git a/tm2/pkg/sdk/baseapp.go b/tm2/pkg/sdk/baseapp.go index 0fa26b817e1..386a5fb206f 100644 --- a/tm2/pkg/sdk/baseapp.go +++ b/tm2/pkg/sdk/baseapp.go @@ -42,6 +42,9 @@ type BaseApp struct { beginBlocker BeginBlocker // logic to run before any txs endBlocker EndBlocker // logic to run after all txs, and to determine valset changes + beginTxHook BeginTxHook // BaseApp-specific hook run before running transaction messages. + endTxHook EndTxHook // BaseApp-specific hook run after a transaction + // -------------------- // Volatile state // checkState is set on initialization and reset on Commit. @@ -820,6 +823,11 @@ func (app *BaseApp) runTx(mode RunTxMode, txBytes []byte, tx Tx) (result Result) // Create a new context based off of the existing context with a cache wrapped // multi-store in case message processing fails. runMsgCtx, msCache := app.cacheTxContext(ctx) + + if app.beginTxHook != nil { + runMsgCtx = app.beginTxHook(runMsgCtx) + } + result = app.runMsgs(runMsgCtx, msgs, mode) result.GasWanted = gasWanted @@ -828,6 +836,10 @@ func (app *BaseApp) runTx(mode RunTxMode, txBytes []byte, tx Tx) (result Result) return result } + if app.endTxHook != nil { + app.endTxHook(runMsgCtx, result) + } + // only update state if all messages pass if result.IsOK() { msCache.MultiWrite() diff --git a/tm2/pkg/sdk/options.go b/tm2/pkg/sdk/options.go index f174b5501a2..b9840a7510b 100644 --- a/tm2/pkg/sdk/options.go +++ b/tm2/pkg/sdk/options.go @@ -85,3 +85,17 @@ func (app *BaseApp) SetAnteHandler(ah AnteHandler) { } app.anteHandler = ah } + +func (app *BaseApp) SetBeginTxHook(beginTx BeginTxHook) { + if app.sealed { + panic("SetBeginTxHook() on sealed BaseApp") + } + app.beginTxHook = beginTx +} + +func (app *BaseApp) SetEndTxHook(endTx EndTxHook) { + if app.sealed { + panic("SetEndTxHook() on sealed BaseApp") + } + app.endTxHook = endTx +}