diff --git a/baseapp/baseapp.go b/baseapp/baseapp.go index d1077fe2ca7..b850c30439e 100644 --- a/baseapp/baseapp.go +++ b/baseapp/baseapp.go @@ -1,6 +1,7 @@ package baseapp import ( + "bytes" "fmt" "strings" @@ -13,6 +14,8 @@ import ( codectypes "github.com/cosmos/cosmos-sdk/codec/types" dbm "github.com/cosmos/cosmos-sdk/db" "github.com/cosmos/cosmos-sdk/snapshots" + "github.com/cosmos/cosmos-sdk/store/tracekv" + "github.com/cosmos/cosmos-sdk/store/types" stypes "github.com/cosmos/cosmos-sdk/store/v2alpha1" "github.com/cosmos/cosmos-sdk/store/v2alpha1/multi" sdk "github.com/cosmos/cosmos-sdk/types" @@ -241,7 +244,7 @@ func (app *BaseApp) loadStore() error { return err } } - app.cms, err = multi.NewV1MultiStoreAsV2(app.db, config) + app.cms, err = multi.NewStore(app.db, config) if err != nil { return fmt.Errorf("failed to load store: %w", err) } @@ -293,6 +296,10 @@ func (app *BaseApp) setHaltHeight(haltHeight uint64) { app.haltHeight = haltHeight } +func (app *BaseApp) setInitialHeight(initialHeight int64) { + app.initialHeight = initialHeight +} + func (app *BaseApp) setHaltTime(haltTime uint64) { app.haltTime = haltTime } @@ -761,3 +768,30 @@ func (app *BaseApp) runMsgs(ctx sdk.Context, msgs []sdk.Msg, mode runTxMode) (*s func makeABCIData(msgResponses []*codectypes.Any) ([]byte, error) { return proto.Marshal(&sdk.TxMsgData{MsgResponses: msgResponses}) } + +func (app *BaseApp) generateFraudProof(storeKeyToSubstoreTraceBuf map[types.StoreKey]*bytes.Buffer) FraudProof { + var fraudProof FraudProof + fraudProof.stateWitness = make(map[string]StateWitness) + cms := app.cms.(*multi.Store) + + for storeKey, subStoreTraceBuf := range storeKeyToSubstoreTraceBuf { + keys := cms.GetKVStore(storeKey).(*tracekv.Store).GetAllKeysUsedInTrace(*subStoreTraceBuf) + + substoreSMT := cms.GetSubstoreSMT(storeKey.Name()) + stateWitness := StateWitness{ + WitnessData: make([]WitnessData, 0, keys.Len()), + } + for key := range keys { + value := substoreSMT.Get([]byte(key)) + // Assumption: The keys exist in the SMT because they were traced + // TOOD: Investigate default value leaves + proof := substoreSMT.MustGetSMTProof([]byte(key)) + bKey, bVal := []byte(key), []byte(value) + witnessData := WitnessData{bKey, bVal, *proof} + stateWitness.WitnessData = append(stateWitness.WitnessData, witnessData) + } + fraudProof.stateWitness[storeKey.Name()] = stateWitness + } + + return fraudProof +} diff --git a/baseapp/baseapp_test.go b/baseapp/baseapp_test.go index 474e84e5046..c96fbad9324 100644 --- a/baseapp/baseapp_test.go +++ b/baseapp/baseapp_test.go @@ -11,13 +11,6 @@ import ( "testing" "time" - "github.com/gogo/protobuf/jsonpb" - "github.com/stretchr/testify/assert" - "github.com/stretchr/testify/require" - abci "github.com/tendermint/tendermint/abci/types" - "github.com/tendermint/tendermint/libs/log" - tmproto "github.com/tendermint/tendermint/proto/tendermint/types" - "github.com/cosmos/cosmos-sdk/codec" codectypes "github.com/cosmos/cosmos-sdk/codec/types" dbm "github.com/cosmos/cosmos-sdk/db" @@ -33,11 +26,18 @@ import ( sdk "github.com/cosmos/cosmos-sdk/types" sdkerrors "github.com/cosmos/cosmos-sdk/types/errors" "github.com/cosmos/cosmos-sdk/x/auth/migrations/legacytx" + "github.com/gogo/protobuf/jsonpb" + "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" + abci "github.com/tendermint/tendermint/abci/types" + "github.com/tendermint/tendermint/libs/log" + tmproto "github.com/tendermint/tendermint/proto/tendermint/types" ) var ( - capKey1 = sdk.NewKVStoreKey("key1") - capKey2 = sdk.NewKVStoreKey("key2") + capKey1 = sdk.NewKVStoreKey("key1") + capKey2 = sdk.NewKVStoreKey("key2") + randSource = int64(123456789) // testTxPriority is the CheckTx priority that we set in the test // antehandler. @@ -98,6 +98,54 @@ func setupBaseApp(t *testing.T, options ...AppOption) *BaseApp { return app } +// baseapp loaded from a fraudproof +func setupBaseAppFromFraudProof(t *testing.T, fraudProof FraudProof, options ...AppOption) *BaseApp { + storeKeys := make([]stypes.StoreKey, 0, len(fraudProof.stateWitness)) + routerOpts := make([]func(bapp *BaseApp), 0, len(fraudProof.stateWitness)) + for storeKeyName := range fraudProof.stateWitness { + storeKey := sdk.NewKVStoreKey(storeKeyName) + storeKeys = append(storeKeys, storeKey) + + routerOpt := func(bapp *BaseApp) { + bapp.Router().AddRoute(sdk.NewRoute(routeMsgKeyValue, func(ctx sdk.Context, msg sdk.Msg) (*sdk.Result, error) { + kv := msg.(*msgKeyValue) + bapp.cms.GetKVStore(storeKey).Set(kv.Key, kv.Value) + return &sdk.Result{}, nil + })) + } + routerOpts = append(routerOpts, routerOpt) + + stateWitness := fraudProof.stateWitness[storeKeyName] + witnessData := stateWitness.WitnessData + for _, witness := range witnessData { + // TODO: + // Verify proof inside WitnessData + + options = append(options, SetSubstoreKVPair(storeKey, witness.Key, witness.Value)) + } + } + options = append(options, SetSubstores(storeKeys...)) + + // RouterOpts should only be called after call to `SetSubstores` + for _, routerOpt := range routerOpts { + options = append(options, AppOptionFunc(routerOpt)) + } + + // This initial height is used in `BeginBlock` in `validateHeight` + options = append(options, SetInitialHeight(fraudProof.blockHeight)) + + // make list of options to pass by parsing fraudproof + app := newBaseApp(t.Name(), options...) + require.Equal(t, t.Name(), app.Name()) + + app.SetParamStore(mock.NewParamStore(dbm.NewMemDB())) + + // stores are mounted + err := app.Init() + require.Nil(t, err) + return app +} + // simple one store baseapp with data and snapshots. Each tx is 1 MB in size (uncompressed). func setupBaseAppWithSnapshots(t *testing.T, config *setupConfig) (*BaseApp, error) { codec := codec.NewLegacyAmino() @@ -2153,3 +2201,122 @@ func TestBaseApp_Init(t *testing.T) { require.Equal(t, tc.expectedSnapshot.KeepRecent, tc.bapp.snapshotManager.GetKeepRecent()) } } + +func executeBlockWithArbitraryTxs(t *testing.T, app *BaseApp, numTransactions int, blockHeight int64) []txTest { + codec := codec.NewLegacyAmino() + registerTestCodec(codec) + r := rand.New(rand.NewSource(randSource)) + randSource += 1 + keyCounter := r.Intn(100) + txs := make([]txTest, 0) + + app.BeginBlock(abci.RequestBeginBlock{Header: tmproto.Header{Height: blockHeight}}) + for txNum := 0; txNum < numTransactions; txNum++ { + tx := txTest{Msgs: []sdk.Msg{}} + for msgNum := 0; msgNum < 2; msgNum++ { + key := []byte(fmt.Sprintf("%v", keyCounter)) + value := make([]byte, 10000) + _, err := r.Read(value) + require.NoError(t, err) + tx.Msgs = append(tx.Msgs, msgKeyValue{Key: key, Value: value}) + keyCounter++ + } + txBytes, err := codec.Marshal(tx) + require.NoError(t, err) + resp := app.DeliverTx(abci.RequestDeliverTx{Tx: txBytes}) + require.True(t, resp.IsOK(), "%v", resp.String()) + txs = append(txs, tx) + } + app.EndBlock(abci.RequestEndBlock{Height: blockHeight}) + return txs +} + +func executeBlock(t *testing.T, app *BaseApp, txs []txTest, blockHeight int64) { + codec := codec.NewLegacyAmino() + registerTestCodec(codec) + + app.BeginBlock(abci.RequestBeginBlock{Header: tmproto.Header{Height: blockHeight}}) + for _, tx := range txs { + txBytes, err := codec.Marshal(tx) + require.NoError(t, err) + resp := app.DeliverTx(abci.RequestDeliverTx{Tx: txBytes}) + require.True(t, resp.IsOK(), "%v", resp.String()) + } + app.EndBlock(abci.RequestEndBlock{Height: blockHeight}) +} + +func checkSubstoreSMTsEqual(appB1 *BaseApp, appB2 *BaseApp, storeKeyName string) bool { + cmsB1 := appB1.cms.(*multi.Store) + cmsB2 := appB2.cms.(*multi.Store) + smtB1 := cmsB1.GetSubstoreSMT(storeKeyName) + smtB2 := cmsB2.GetSubstoreSMT(storeKeyName) + + return string(smtB1.Root()) == string(smtB2.Root()) +} + +func TestGenerateAndLoadFraudProof(t *testing.T) { + /* + Tests switch between a baseapp and fraudproof and covers parts of the fraudproof cycle. Steps: + 1. Initialize a baseapp, B1, with some state, S0 + 2. Make some state transition to state S1 by doing some transactions and commit + 3. Export that state S1 into a fraudProof data structure (minimal snapshot) + 4. Load a fresh baseapp, B2, with the contents of fraud proof data structure from (3) so it can begin from state S1. + 5. If the state of both B1 and B2 has to converge at a state S2, then we the test passes. + + Note that the appHash from B1 and B2 will not be the same because the subset of storeKeys in both apps is different + so we compare subStores instead + + Tests to write in future: + + 1. Block with bad txs: Txs that exceed gas limits, validateBasic fails, unregistered messages (see TestRunInvalidTransaction) + 2. Block with invalid appHash at the end + 3. Corrupted Fraud Proof: bad SMT format, insufficient key-value pairs inside SMT needed to verify fraud + 4. Bad block, fraud proof needed, fraud proof works, chain halts (happy case) + + */ + + storeTraceBuf := &bytes.Buffer{} + subStoreTraceBuf := &bytes.Buffer{} + + routerOpt := func(bapp *BaseApp) { + bapp.Router().AddRoute(sdk.NewRoute(routeMsgKeyValue, func(ctx sdk.Context, msg sdk.Msg) (*sdk.Result, error) { + kv := msg.(*msgKeyValue) + bapp.cms.GetKVStore(capKey2).Set(kv.Key, kv.Value) + return &sdk.Result{}, nil + })) + } + + // BaseApp, B1 + + appB1 := setupBaseApp(t, + AppOptionFunc(routerOpt), + SetSubstoreTracer(storeTraceBuf), + SetTracerFor(capKey2, subStoreTraceBuf), + ) + + // B1 <- S0 + appB1.InitChain(abci.RequestInitChain{}) + + numTransactions := 5 + // B1 <- S1 + executeBlockWithArbitraryTxs(t, appB1, numTransactions, 1) + appB1.Commit() + + // Exports all data inside current multistore into a fraudProof (S0 -> S1) // + storeKeyToSubstoreTraceBuf := make(map[stypes.StoreKey]*bytes.Buffer) + storeKeyToSubstoreTraceBuf[capKey2] = subStoreTraceBuf + + // Records S1 in fraudproof + fraudProof := appB1.generateFraudProof(storeKeyToSubstoreTraceBuf) + currentBlockHeight := appB1.LastBlockHeight() // Only changes on a Commit + fraudProof.blockHeight = currentBlockHeight + 1 + + // Light Client + + // TODO: Insert fraudproof verification here + + // Now we take contents of the fraud proof which was recorded with S2 and try to populate a fresh baseapp B2 with it + // B2 <- S1 + appB2 := setupBaseAppFromFraudProof(t, fraudProof) + require.True(t, checkSubstoreSMTsEqual(appB1, appB2, capKey2.Name())) +} diff --git a/baseapp/fraudproof.go b/baseapp/fraudproof.go new file mode 100644 index 00000000000..7084c091117 --- /dev/null +++ b/baseapp/fraudproof.go @@ -0,0 +1,25 @@ +package baseapp + +import "github.com/lazyledger/smt" + +// Represents a single-round fraudProof +type FraudProof struct { + // The block height to load state of + blockHeight int64 + + // A map from module name to state witness + stateWitness map[string]StateWitness +} + +// State witness with a list of all witness data +type StateWitness struct { + // List of witness data + WitnessData []WitnessData +} + +// Witness data containing a key/value pair and a SMT proof for said key/value pair +type WitnessData struct { + Key []byte + Value []byte + proof smt.SparseMerkleProof +} diff --git a/baseapp/options.go b/baseapp/options.go index bbbaf16ea5e..ee39238782d 100644 --- a/baseapp/options.go +++ b/baseapp/options.go @@ -21,6 +21,33 @@ func SetPruning(opts pruningtypes.PruningOptions) StoreOption { return func(config *multi.StoreParams, _ uint64) error { config.Pruning = opts; return nil } } +// SetSubstoreTracer provides a BaseApp option function that sets the +// tracer for a multistore. +func SetSubstoreTracer(w io.Writer) StoreOption { + return func(cfg *multi.StoreParams, _ uint64) error { + cfg.SetTracer(w) + return nil + } +} + +// SetTracerFor provides a BaseApp option function that sets the +// tracer for a substore with given skey inside a multistore. +func SetTracerFor(skey storetypes.StoreKey, w io.Writer) StoreOption { + return func(cfg *multi.StoreParams, _ uint64) error { + cfg.SetTracerFor(skey, w) + return nil + } +} + +// SetSubstoreKVPair sets a key, value pair for the given substore inside a multistore +// Only works for v2alpha1/multi +func SetSubstoreKVPair(skey storetypes.StoreKey, key, val []byte) AppOptionOrdered { + return AppOptionOrdered{ + func(bapp *BaseApp) { bapp.cms.(*multi.Store).SetSubstoreKVPair(skey, key, val) }, + OptionOrderAfterStore, + } +} + // SetMinGasPrices returns an option that sets the minimum gas prices on the app. func SetMinGasPrices(gasPricesStr string) AppOptionFunc { gasPrices, err := sdk.ParseDecCoins(gasPricesStr) @@ -36,6 +63,11 @@ func SetHaltHeight(blockHeight uint64) AppOptionFunc { return func(bap *BaseApp) { bap.setHaltHeight(blockHeight) } } +// SetInitialHeight returns a BaseApp option function that sets the initial block height. +func SetInitialHeight(blockHeight int64) AppOptionFunc { + return func(bapp *BaseApp) { bapp.setInitialHeight(blockHeight) } +} + // SetHaltTime returns a BaseApp option function that sets the halt block time. func SetHaltTime(haltTime uint64) AppOptionFunc { return func(bap *BaseApp) { bap.setHaltTime(haltTime) } diff --git a/go.mod b/go.mod index 8a6e92d49e4..c31fcbd209b 100644 --- a/go.mod +++ b/go.mod @@ -65,7 +65,10 @@ require ( sigs.k8s.io/yaml v1.3.0 ) -require github.com/cosmos/cosmos-sdk/store/tools/ics23 v0.0.0-00010101000000-000000000000 +require ( + github.com/chrispappas/golang-generics-set v1.0.1 + github.com/cosmos/cosmos-sdk/store/tools/ics23 v0.0.0-00010101000000-000000000000 +) require ( 4d63.com/gochecknoglobals v0.1.0 // indirect diff --git a/go.sum b/go.sum index 36be613d7dc..399b5a31845 100644 --- a/go.sum +++ b/go.sum @@ -229,6 +229,8 @@ github.com/chavacava/garif v0.0.0-20220316182200-5cad0b5181d4 h1:tFXjAxje9thrTF4 github.com/chavacava/garif v0.0.0-20220316182200-5cad0b5181d4/go.mod h1:W8EnPSQ8Nv4fUjc/v1/8tHFqhuOJXnRub0dTfuAQktU= github.com/checkpoint-restore/go-criu/v5 v5.3.0/go.mod h1:E/eQpaFtUKGOOSEBZgmKAcn+zUUwWxqcaKZlF54wK8E= github.com/cheggaaa/pb v1.0.27/go.mod h1:pQciLPpbU0oxA0h+VJYYLxO+XeDQb5pZijXscXHm81s= +github.com/chrispappas/golang-generics-set v1.0.1 h1:91l8cInAWTxCPwZ8UNg7qkkPsdFdkYS9hytsd8UJsIU= +github.com/chrispappas/golang-generics-set v1.0.1/go.mod h1:cp8j73+rlDyFF9PrjUkrRvi8L4jSRIsRK6Q1nPPIoqo= github.com/chzyer/logex v1.1.10/go.mod h1:+Ywpsq7O8HXn0nuIou7OrIPyXbp3wmkHB+jjWRnGsAI= github.com/chzyer/readline v0.0.0-20180603132655-2972be24d48e/go.mod h1:nSuG5e5PlCu98SY8svDHJxuZscDgtXS6KTTbou5AhLI= github.com/chzyer/test v0.0.0-20180213035817-a1ea475d72b1/go.mod h1:Q3SI9o4m/ZMnBNeIyt5eFwwo7qiLfzFZmjNmxjkiQlU= diff --git a/store/tracekv/store.go b/store/tracekv/store.go index 91f3c657682..210835870d8 100644 --- a/store/tracekv/store.go +++ b/store/tracekv/store.go @@ -1,10 +1,13 @@ package tracekv import ( + "bytes" "encoding/base64" "encoding/json" + "fmt" "io" + "github.com/chrispappas/golang-generics-set/set" "github.com/cosmos/cosmos-sdk/store/types" "github.com/cosmos/cosmos-sdk/types/errors" ) @@ -17,6 +20,10 @@ const ( iterValueOp operation = "iterValue" ) +var ( + ErrBufferEmpty = fmt.Errorf("provided buffer is empty") +) + type ( // Store implements the KVStore interface with tracing enabled. // Operations are traced on each core KVStore call and written to the @@ -90,6 +97,28 @@ func (tkv *Store) ReverseIterator(start, end []byte) types.Iterator { return tkv.iterator(start, end, false) } +// GetAllKeysUsedInTrace reads through all traced operations and returns +// a set of all the keys inside the trace operations +func (tkv *Store) GetAllKeysUsedInTrace(buf bytes.Buffer) set.Set[string] { + + keys := make(set.Set[string], 0) + for { + traceOp, err := readOperation(&buf) + // Reached end of buffer + if err == ErrBufferEmpty { + return keys + } + if err != nil { + panic(err) + } + key, err := base64.StdEncoding.DecodeString(traceOp.Key) + if err != nil { + panic(errors.Wrap(err, "failed to decode key read from buf")) + } + keys.Add(string(key)) + } +} + // iterator facilitates iteration over a KVStore. It delegates the necessary // calls to it's parent KVStore. func (tkv *Store) iterator(start, end []byte, ascending bool) types.Iterator { @@ -202,3 +231,22 @@ func writeOperation(w io.Writer, op operation, tc types.TraceContext, key, value io.WriteString(w, "\n") } + +// readOperation reads a KVStore operation from the underlying buffer as +// JSON-encoded data where the key/value pair is base64 encoded. +func readOperation(r *bytes.Buffer) (*traceOperation, error) { + raw, err := r.ReadString('\n') + if raw == "" { + return nil, ErrBufferEmpty + } + if err != nil { + return nil, errors.Wrap(err, "failed to read trace operation") + } + traceOp := traceOperation{} + err = json.Unmarshal([]byte(raw), &traceOp) + if err != nil { + return nil, errors.Wrap(err, "failed to deserialize trace operation") + } + + return &traceOp, nil +} diff --git a/store/tracekv/store_test.go b/store/tracekv/store_test.go index 1b81e89bafd..41fe24a6d42 100644 --- a/store/tracekv/store_test.go +++ b/store/tracekv/store_test.go @@ -6,6 +6,7 @@ import ( "io" "testing" + "github.com/chrispappas/golang-generics-set/set" "github.com/stretchr/testify/require" dbm "github.com/tendermint/tm-db" @@ -113,6 +114,25 @@ func TestTraceKVStoreSet(t *testing.T) { require.Panics(t, func() { store.Set(nil, []byte("value")) }, "setting a nil key should panic") } +func TestGetAllKeysUsedInTrace(t *testing.T) { + expectedKeys := set.FromSlice([]string{ + string(kvPairs[0].Key), + string(kvPairs[1].Key), + string(kvPairs[2].Key), + }) + + var buf bytes.Buffer + store := newEmptyTraceKVStore(&buf) + buf.Reset() + + for _, kvPair := range kvPairs { + store.Set(kvPair.Key, kvPair.Value) + } + + keys := store.GetAllKeysUsedInTrace(buf) + require.Equal(t, expectedKeys, keys) +} + func TestTraceKVStoreDelete(t *testing.T) { testCases := []struct { key []byte diff --git a/store/v2alpha1/multi/cache_store.go b/store/v2alpha1/multi/cache_store.go index 390deca2836..3827ec93979 100644 --- a/store/v2alpha1/multi/cache_store.go +++ b/store/v2alpha1/multi/cache_store.go @@ -2,6 +2,8 @@ package multi import ( "github.com/cosmos/cosmos-sdk/store/cachekv" + "github.com/cosmos/cosmos-sdk/store/listenkv" + "github.com/cosmos/cosmos-sdk/store/tracekv" types "github.com/cosmos/cosmos-sdk/store/v2alpha1" ) @@ -61,3 +63,13 @@ func (noopCacheStore) Write() {} func CommitAsCacheStore(s types.CommitMultiStore) types.CacheMultiStore { return noopCacheStore{newCacheStore(s)} } + +func (cs *cacheStore) wrapTraceListen(store types.KVStore, skey types.StoreKey) types.KVStore { + if cs.TracingEnabled() { + store = tracekv.NewStore(store, cs.TraceWriter, cs.getTracingContext()) + } + if cs.ListeningEnabled(skey) { + store = listenkv.NewStore(store, skey, cs.listeners[skey]) + } + return store +} diff --git a/store/v2alpha1/multi/params.go b/store/v2alpha1/multi/params.go index 9d5fe006323..507984641b0 100644 --- a/store/v2alpha1/multi/params.go +++ b/store/v2alpha1/multi/params.go @@ -2,6 +2,7 @@ package multi import ( "fmt" + "io" pruningtypes "github.com/cosmos/cosmos-sdk/pruning/types" types "github.com/cosmos/cosmos-sdk/store/v2alpha1" @@ -11,10 +12,11 @@ import ( // pruning with PruneDefault, no listeners and no tracer. func DefaultStoreParams() StoreParams { return StoreParams{ - Pruning: pruningtypes.NewPruningOptions(pruningtypes.PruningDefault), - SchemaBuilder: newSchemaBuilder(), - storeKeys: storeKeys{}, - traceListenMixin: newTraceListenMixin(), + Pruning: pruningtypes.NewPruningOptions(pruningtypes.PruningDefault), + SchemaBuilder: newSchemaBuilder(), + storeKeys: storeKeys{}, + traceListenMixin: newTraceListenMixin(), + substoreTraceListenMixins: make(map[types.StoreKey]*traceListenMixin), } } @@ -41,6 +43,13 @@ func (par *StoreParams) RegisterSubstore(skey types.StoreKey, typ types.StoreTyp return nil } +func (par *StoreParams) SetTracerFor(skey types.StoreKey, w io.Writer) { + tlm := newTraceListenMixin() + tlm.SetTracer(w) + tlm.SetTracingContext(par.TraceContext) + par.substoreTraceListenMixins[skey] = tlm +} + func (par *StoreParams) storeKey(key string) (types.StoreKey, error) { skey, ok := par.storeKeys[key] if !ok { diff --git a/store/v2alpha1/multi/store.go b/store/v2alpha1/multi/store.go index 7a42fd33683..b33e08582aa 100644 --- a/store/v2alpha1/multi/store.go +++ b/store/v2alpha1/multi/store.go @@ -74,6 +74,7 @@ type StoreParams struct { Upgrades *types.StoreUpgrades // Contains The trace context and listeners that can also be set from store methods. *traceListenMixin + substoreTraceListenMixins map[types.StoreKey]*traceListenMixin } // StoreSchema defineds a mapping of substore keys to store types @@ -108,8 +109,9 @@ type Store struct { pruningManager *pruning.Manager - PersistentCache types.MultiStorePersistentCache - substoreCache map[string]*substore + PersistentCache types.MultiStorePersistentCache + substoreCache map[string]*substore + substoreTraceListenMixins map[types.StoreKey]*traceListenMixin } type substore struct { @@ -117,6 +119,7 @@ type substore struct { name string dataBucket dbm.ReadWriter stateCommitmentStore *smt.Store + *traceListenMixin } // Builder type used to create a valid schema with no prefix conflicts @@ -264,17 +267,18 @@ func NewStore(db dbm.Connection, opts StoreParams) (ret *Store, err error) { } ret = &Store{ - stateDB: db, - stateTxn: stateTxn, - StateCommitmentDB: opts.StateCommitmentDB, - stateCommitmentTxn: stateCommitmentTxn, - mem: mem.NewStore(), - tran: transient.NewStore(), - substoreCache: map[string]*substore{}, - traceListenMixin: opts.traceListenMixin, - PersistentCache: opts.PersistentCache, - pruningManager: pruningManager, - InitialVersion: opts.InitialVersion, + stateDB: db, + stateTxn: stateTxn, + StateCommitmentDB: opts.StateCommitmentDB, + stateCommitmentTxn: stateCommitmentTxn, + mem: mem.NewStore(), + tran: transient.NewStore(), + substoreCache: map[string]*substore{}, + traceListenMixin: opts.traceListenMixin, + PersistentCache: opts.PersistentCache, + pruningManager: pruningManager, + InitialVersion: opts.InitialVersion, + substoreTraceListenMixins: opts.substoreTraceListenMixins, } // Now load the substore schema @@ -452,6 +456,7 @@ func prefixNonpersistent(key string) []byte { func (s *Store) GetKVStore(skey types.StoreKey) types.KVStore { key := skey.Name() var parent types.KVStore + typ, has := s.schema[skey] if !has { panic(ErrStoreNotFound(key)) @@ -518,6 +523,11 @@ func (s *Store) getSubstore(key string) (*substore, error) { }, nil } +func (s *Store) SetSubstoreKVPair(skey types.StoreKey, kv, val []byte) { + sub := s.GetKVStore(skey) + sub.Set(kv, val) +} + // Resets a substore's state after commit (because root stateTxn has been discarded) func (s *substore) refresh(rootHash []byte) { pfx := prefixSubstore(s.name) @@ -1023,12 +1033,14 @@ func (tlm *traceListenMixin) getTracingContext() types.TraceContext { return ctx } -func (tlm *traceListenMixin) wrapTraceListen(store types.KVStore, skey types.StoreKey) types.KVStore { - if tlm.TracingEnabled() { - store = tracekv.NewStore(store, tlm.TraceWriter, tlm.getTracingContext()) +func (s *Store) wrapTraceListen(store types.KVStore, skey types.StoreKey) types.KVStore { + if s.TracingEnabled() { + subStoreTlm := s.substoreTraceListenMixins[skey] + store = tracekv.NewStore(store, subStoreTlm.TraceWriter, s.getTracingContext()) } - if tlm.ListeningEnabled(skey) { - store = listenkv.NewStore(store, skey, tlm.listeners[skey]) + if s.ListeningEnabled(skey) { + subStoreTlm := s.substoreTraceListenMixins[skey] + store = listenkv.NewStore(store, skey, subStoreTlm.listeners[skey]) } return store } @@ -1040,3 +1052,11 @@ func (s *Store) GetPruning() pruningtypes.PruningOptions { func (s *Store) SetPruning(po pruningtypes.PruningOptions) { s.pruningManager.SetOptions(po) } + +func (s *Store) GetSubstoreSMT(key string) *smt.Store { + sub, err := s.getSubstore(key) + if err != nil { + panic(err) + } + return sub.stateCommitmentStore +} diff --git a/store/v2alpha1/smt/store.go b/store/v2alpha1/smt/store.go index ea902ff1c58..fead1c0fcd8 100644 --- a/store/v2alpha1/smt/store.go +++ b/store/v2alpha1/smt/store.go @@ -23,8 +23,8 @@ var ( valuesPrefix = []byte{1} preimagesPrefix = []byte{2} - errKeyEmpty = errors.New("key is empty or nil") - errValueNil = errors.New("value is nil") + ErrKeyEmpty = errors.New("key is empty or nil") + ErrValueNil = errors.New("value is nil") ) // Store Implements types.BasicKVStore. @@ -63,7 +63,7 @@ func LoadStore(db dbm.ReadWriter, root []byte) *Store { func (s *Store) GetProof(key []byte) (*tmcrypto.ProofOps, error) { if len(key) == 0 { - return nil, errKeyEmpty + return nil, ErrKeyEmpty } proof, err := s.tree.Prove(key) if err != nil { @@ -77,6 +77,26 @@ func (s *Store) GetProofICS23(key []byte) (*ics23.CommitmentProof, error) { return createIcs23Proof(s, key) } +func (s *Store) GetSMTProof(key []byte) (*smt.SparseMerkleProof, error) { + if len(key) == 0 { + return nil, ErrKeyEmpty + } + proof, err := s.tree.Prove(key) + if err != nil { + return nil, err + } + return &proof, nil +} + +func (s *Store) MustGetSMTProof(key []byte) *smt.SparseMerkleProof { + proof, err := s.GetSMTProof(key) + if err != nil { + panic(err) + } + + return proof +} + func (s *Store) Root() []byte { return s.tree.Root() } // BasicKVStore interface below: @@ -84,7 +104,7 @@ func (s *Store) Root() []byte { return s.tree.Root() } // Get returns nil iff key doesn't exist. Panics on nil or empty key. func (s *Store) Get(key []byte) []byte { if len(key) == 0 { - panic(errKeyEmpty) + panic(ErrKeyEmpty) } val, err := s.tree.Get(key) if err != nil { @@ -96,7 +116,7 @@ func (s *Store) Get(key []byte) []byte { // Has checks if a key exists. Panics on nil or empty key. func (s *Store) Has(key []byte) bool { if len(key) == 0 { - panic(errKeyEmpty) + panic(ErrKeyEmpty) } has, err := s.tree.Has(key) if err != nil { @@ -108,10 +128,10 @@ func (s *Store) Has(key []byte) bool { // Set sets the key. Panics on nil key or value. func (s *Store) Set(key []byte, value []byte) { if len(key) == 0 { - panic(errKeyEmpty) + panic(ErrKeyEmpty) } if value == nil { - panic(errValueNil) + panic(ErrValueNil) } _, err := s.tree.Update(key, value) if err != nil { @@ -124,7 +144,7 @@ func (s *Store) Set(key []byte, value []byte) { // Delete deletes the key. Panics on nil key. func (s *Store) Delete(key []byte) { if len(key) == 0 { - panic(errKeyEmpty) + panic(ErrKeyEmpty) } _, _ = s.tree.Delete(key) path := sha256.Sum256(key)