diff --git a/app/seidb.go b/app/seidb.go index 448de9960f..ed1fdb7353 100644 --- a/app/seidb.go +++ b/app/seidb.go @@ -30,6 +30,7 @@ const ( FlagSCHistoricalProofBurst = "state-commit.sc-historical-proof-burst" FlagSCWriteMode = "state-commit.sc-write-mode" FlagSCReadMode = "state-commit.sc-read-mode" + FlagSCEnableLatticeHash = "state-commit.sc-enable-lattice-hash" // SS Store configs FlagSSEnable = "state-store.ss-enable" @@ -120,6 +121,8 @@ func parseSCConfigs(appOpts servertypes.AppOptions) config.StateCommitConfig { scConfig.ReadMode = parsedRM } + scConfig.EnableLatticeHash = cast.ToBool(appOpts.Get(FlagSCEnableLatticeHash)) + if v := appOpts.Get(FlagSCHistoricalProofMaxInFlight); v != nil { scConfig.HistoricalProofMaxInFlight = cast.ToInt(v) } diff --git a/app/seidb_test.go b/app/seidb_test.go index fa91e1436e..8504f32848 100644 --- a/app/seidb_test.go +++ b/app/seidb_test.go @@ -37,6 +37,8 @@ func (t TestSeiDBAppOpts) Get(s string) interface{} { return defaultSCConfig.MemIAVLConfig.SnapshotPrefetchThreshold case FlagSCSnapshotWriteRateMBps: return defaultSCConfig.MemIAVLConfig.SnapshotWriteRateMBps + case FlagSCEnableLatticeHash: + return defaultSCConfig.EnableLatticeHash case FlagSSEnable: return defaultSSConfig.Enable case FlagSSBackend: diff --git a/sei-cosmos/server/config/config.go b/sei-cosmos/server/config/config.go index 7e9b2e383a..b3ac1b746c 100644 --- a/sei-cosmos/server/config/config.go +++ b/sei-cosmos/server/config/config.go @@ -406,10 +406,11 @@ func GetConfig(v *viper.Viper) (Config, error) { SnapshotDirectory: v.GetString("state-sync.snapshot-directory"), }, StateCommit: config.StateCommitConfig{ - Enable: v.GetBool("state-commit.sc-enable"), - Directory: v.GetString("state-commit.sc-directory"), - WriteMode: config.WriteMode(v.GetString("state-commit.sc-write-mode")), - ReadMode: config.ReadMode(v.GetString("state-commit.sc-read-mode")), + Enable: v.GetBool("state-commit.sc-enable"), + Directory: v.GetString("state-commit.sc-directory"), + WriteMode: config.WriteMode(v.GetString("state-commit.sc-write-mode")), + ReadMode: config.ReadMode(v.GetString("state-commit.sc-read-mode")), + EnableLatticeHash: v.GetBool("state-commit.sc-enable-lattice-hash"), MemIAVLConfig: memiavl.Config{ AsyncCommitBuffer: v.GetInt("state-commit.sc-async-commit-buffer"), SnapshotKeepRecent: v.GetUint32("state-commit.sc-keep-recent"), diff --git a/sei-db/config/sc_config.go b/sei-db/config/sc_config.go index b32995a77d..830cc7ab48 100644 --- a/sei-db/config/sc_config.go +++ b/sei-db/config/sc_config.go @@ -32,12 +32,15 @@ type StateCommitConfig struct { // WriteMode defines the write routing mode for EVM data // Valid values: cosmos_only, dual_write, split_write, evm_only // defaults to cosmos_only - WriteMode WriteMode `mapstructure:"write_mode"` + WriteMode WriteMode `mapstructure:"write-mode"` // ReadMode defines the read routing mode for EVM data // Valid values: cosmos_only, evm_first, split_read // defaults to cosmos_only - ReadMode ReadMode `mapstructure:"read_mode"` + ReadMode ReadMode `mapstructure:"read-mode"` + + // EnableLatticeHash controls whether lattice hash will be participating in final app hash or not + EnableLatticeHash bool `mapstructure:"enable-lattice-hash"` // MemIAVLConfig is the configuration for the MemIAVL (Cosmos) backend MemIAVLConfig memiavl.Config @@ -59,11 +62,12 @@ type StateCommitConfig struct { // DefaultStateCommitConfig returns the default StateCommitConfig func DefaultStateCommitConfig() StateCommitConfig { return StateCommitConfig{ - Enable: true, - WriteMode: CosmosOnlyWrite, - ReadMode: CosmosOnlyRead, - MemIAVLConfig: memiavl.DefaultConfig(), - FlatKVConfig: flatkv.DefaultConfig(), + Enable: true, + WriteMode: CosmosOnlyWrite, + ReadMode: CosmosOnlyRead, + EnableLatticeHash: false, + MemIAVLConfig: memiavl.DefaultConfig(), + FlatKVConfig: flatkv.DefaultConfig(), HistoricalProofMaxInFlight: DefaultSCHistoricalProofMaxInFlight, HistoricalProofRateLimit: DefaultSCHistoricalProofRateLimit, @@ -74,10 +78,10 @@ func DefaultStateCommitConfig() StateCommitConfig { // Validate checks if the StateCommitConfig is valid func (c StateCommitConfig) Validate() error { if !c.WriteMode.IsValid() { - return fmt.Errorf("invalid write_mode: %s", c.WriteMode) + return fmt.Errorf("invalid write-mode: %s", c.WriteMode) } if !c.ReadMode.IsValid() { - return fmt.Errorf("invalid read_mode: %s", c.ReadMode) + return fmt.Errorf("invalid read-mode: %s", c.ReadMode) } return nil } diff --git a/sei-db/config/toml.go b/sei-db/config/toml.go index 7588e7b434..723832d0d6 100644 --- a/sei-db/config/toml.go +++ b/sei-db/config/toml.go @@ -23,6 +23,10 @@ sc-write-mode = "{{ .StateCommit.WriteMode }}" # defaults to cosmos_only sc-read-mode = "{{ .StateCommit.ReadMode }}" +# EnableLatticeHash controls whether the FlatKV lattice hash participates +# in the final app hash. Default: false. +sc-enable-lattice-hash = {{ .StateCommit.EnableLatticeHash }} + # Max concurrent historical proof queries (RPC /store path) sc-historical-proof-max-inflight = {{ .StateCommit.HistoricalProofMaxInFlight }} diff --git a/sei-db/state_db/sc/composite/store.go b/sei-db/state_db/sc/composite/store.go index 4d750d4437..d0e8b5a4c6 100644 --- a/sei-db/state_db/sc/composite/store.go +++ b/sei-db/state_db/sc/composite/store.go @@ -219,16 +219,44 @@ func (cs *CompositeCommitStore) GetEarliestVersion() (int64, error) { return cs.cosmosCommitter.GetEarliestVersion() } +// appendEvmLatticeHash returns a new CommitInfo with the EVM lattice hash +// appended, without mutating the original. Returns the original unchanged +// when lattice hashing is disabled. +func (cs *CompositeCommitStore) appendEvmLatticeHash(ci *proto.CommitInfo, evmHash []byte) *proto.CommitInfo { + if !cs.config.EnableLatticeHash { + return ci + } + combined := make([]proto.StoreInfo, len(ci.StoreInfos)+1) + copy(combined, ci.StoreInfos) + combined[len(combined)-1] = proto.StoreInfo{ + Name: "evm_lattice", + CommitId: proto.CommitID{ + Version: ci.Version, + Hash: evmHash, + }, + } + return &proto.CommitInfo{ + Version: ci.Version, + StoreInfos: combined, + } +} + // WorkingCommitInfo returns the working commit info func (cs *CompositeCommitStore) WorkingCommitInfo() *proto.CommitInfo { - // TODO: Need to combine hash for cosmos and evm - return cs.cosmosCommitter.WorkingCommitInfo() + ci := cs.cosmosCommitter.WorkingCommitInfo() + if cs.evmCommitter != nil { + return cs.appendEvmLatticeHash(ci, cs.evmCommitter.RootHash()) + } + return ci } // LastCommitInfo returns the last commit info func (cs *CompositeCommitStore) LastCommitInfo() *proto.CommitInfo { - // TODO: Need to combine hash for cosmos and evm - return cs.cosmosCommitter.LastCommitInfo() + ci := cs.cosmosCommitter.LastCommitInfo() + if cs.evmCommitter != nil { + return cs.appendEvmLatticeHash(ci, cs.evmCommitter.CommittedRootHash()) + } + return ci } // GetChildStoreByName returns the underlying child store by module name. diff --git a/sei-db/state_db/sc/composite/store_test.go b/sei-db/state_db/sc/composite/store_test.go index e4ed33ebe7..551b505a05 100644 --- a/sei-db/state_db/sc/composite/store_test.go +++ b/sei-db/state_db/sc/composite/store_test.go @@ -5,6 +5,7 @@ import ( "github.com/stretchr/testify/require" + "github.com/sei-protocol/sei-chain/sei-db/common/evm" "github.com/sei-protocol/sei-chain/sei-db/common/logger" "github.com/sei-protocol/sei-chain/sei-db/config" "github.com/sei-protocol/sei-chain/sei-db/proto" @@ -153,6 +154,149 @@ func TestWorkingAndLastCommitInfo(t *testing.T) { require.Equal(t, int64(1), lastInfo.Version) } +func TestLatticeHashCommitInfo(t *testing.T) { + addr := [20]byte{0xAA} + slot := [32]byte{0xBB} + evmStorageKey := evm.BuildMemIAVLEVMKey(evm.EVMKeyStorage, append(addr[:], slot[:]...)) + + makeChangesets := func(round byte) []*proto.NamedChangeSet { + return []*proto.NamedChangeSet{ + { + Name: "test", + Changeset: iavl.ChangeSet{ + Pairs: []*iavl.KVPair{ + {Key: []byte("key"), Value: []byte{round}}, + }, + }, + }, + { + Name: EVMStoreName, + Changeset: iavl.ChangeSet{ + Pairs: []*iavl.KVPair{ + {Key: evmStorageKey, Value: []byte{round}}, + }, + }, + }, + } + } + + tests := []struct { + name string + writeMode config.WriteMode + enableLattice bool + expectLattice bool + }{ + {"CosmosOnly/lattice_off", config.CosmosOnlyWrite, false, false}, + {"CosmosOnly/lattice_on", config.CosmosOnlyWrite, true, false}, + {"DualWrite/lattice_off", config.DualWrite, false, false}, + {"DualWrite/lattice_on", config.DualWrite, true, true}, + {"SplitWrite/lattice_off", config.SplitWrite, false, false}, + {"SplitWrite/lattice_on", config.SplitWrite, true, true}, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + dir := t.TempDir() + cfg := config.DefaultStateCommitConfig() + cfg.WriteMode = tt.writeMode + cfg.EnableLatticeHash = tt.enableLattice + + cs := NewCompositeCommitStore(t.Context(), dir, logger.NewNopLogger(), cfg) + cs.Initialize([]string{"test", EVMStoreName}) + _, err := cs.LoadVersion(0, false) + require.NoError(t, err) + defer cs.Close() + + var prevLastHash []byte + + for round := byte(1); round <= 3; round++ { + require.NoError(t, cs.ApplyChangeSets(makeChangesets(round))) + + // --- Working commit info --- + expectedCosmos := cs.cosmosCommitter.WorkingCommitInfo() + var expectedEvmHash []byte + if tt.expectLattice { + expectedEvmHash = cs.evmCommitter.RootHash() + } + + workingInfo := cs.WorkingCommitInfo() + cosmosCount := len(expectedCosmos.StoreInfos) + if tt.expectLattice { + require.Equal(t, cosmosCount+1, len(workingInfo.StoreInfos)) + } else { + require.Equal(t, cosmosCount, len(workingInfo.StoreInfos)) + } + for i, si := range expectedCosmos.StoreInfos { + require.Equal(t, si.Name, workingInfo.StoreInfos[i].Name) + require.Equal(t, si.CommitId.Hash, workingInfo.StoreInfos[i].CommitId.Hash) + } + if tt.expectLattice { + entry := workingInfo.StoreInfos[len(workingInfo.StoreInfos)-1] + require.Equal(t, "evm_lattice", entry.Name) + require.Equal(t, expectedEvmHash, entry.CommitId.Hash) + require.Equal(t, workingInfo.Version, entry.CommitId.Version) + + // Verify no duplicate names — important for app hash merkle tree + names := make(map[string]int) + for _, si := range workingInfo.StoreInfos { + names[si.Name]++ + } + for name, count := range names { + require.Equal(t, 1, count, "duplicate store name %q in WorkingCommitInfo", name) + } + } + + // --- Commit --- + _, err = cs.Commit() + require.NoError(t, err) + + // --- Last commit info --- + expectedCosmosLast := cs.cosmosCommitter.LastCommitInfo() + var expectedEvmCommitted []byte + if tt.expectLattice { + expectedEvmCommitted = cs.evmCommitter.CommittedRootHash() + require.Equal(t, expectedEvmHash, expectedEvmCommitted) + } + + lastInfo := cs.LastCommitInfo() + require.Equal(t, int64(round), lastInfo.Version) + cosmosLastCount := len(expectedCosmosLast.StoreInfos) + if tt.expectLattice { + require.Equal(t, cosmosLastCount+1, len(lastInfo.StoreInfos)) + } else { + require.Equal(t, cosmosLastCount, len(lastInfo.StoreInfos)) + } + for i, si := range expectedCosmosLast.StoreInfos { + require.Equal(t, si.Name, lastInfo.StoreInfos[i].Name) + require.Equal(t, si.CommitId.Hash, lastInfo.StoreInfos[i].CommitId.Hash) + } + if tt.expectLattice { + entry := lastInfo.StoreInfos[len(lastInfo.StoreInfos)-1] + require.Equal(t, "evm_lattice", entry.Name) + require.Equal(t, expectedEvmCommitted, entry.CommitId.Hash) + require.Equal(t, lastInfo.Version, entry.CommitId.Version) + + // Verify no duplicate names — important for app hash merkle tree + names := make(map[string]int) + for _, si := range lastInfo.StoreInfos { + names[si.Name]++ + } + for name, count := range names { + require.Equal(t, 1, count, "duplicate store name %q in LastCommitInfo", name) + } + + // Hash must change between rounds since data differs + if prevLastHash != nil { + require.NotEqual(t, prevLastHash, entry.CommitId.Hash, + "lattice hash should change across commits") + } + prevLastHash = entry.CommitId.Hash + } + } + }) + } +} + func TestRollback(t *testing.T) { dir := t.TempDir() cfg := config.DefaultStateCommitConfig() diff --git a/sei-db/state_db/sc/flatkv/api.go b/sei-db/state_db/sc/flatkv/api.go index dce5fc4c01..3bc76e9785 100644 --- a/sei-db/state_db/sc/flatkv/api.go +++ b/sei-db/state_db/sc/flatkv/api.go @@ -58,6 +58,9 @@ type Store interface { // raw LtHash vector. RootHash() []byte + // CommittedRootHash returns the 32-byte checksum of the last committed LtHash. + CommittedRootHash() []byte + // Version returns the latest committed version. Version() int64 diff --git a/sei-db/state_db/sc/flatkv/store.go b/sei-db/state_db/sc/flatkv/store.go index 5dabbbba1c..a833e60f18 100644 --- a/sei-db/state_db/sc/flatkv/store.go +++ b/sei-db/state_db/sc/flatkv/store.go @@ -437,6 +437,12 @@ func (s *CommitStore) RootHash() []byte { return checksum[:] } +// CommittedRootHash returns the Blake3-256 digest of the last committed LtHash. +func (s *CommitStore) CommittedRootHash() []byte { + checksum := s.committedLtHash.Checksum() + return checksum[:] +} + func (s *CommitStore) Importer(version int64) (types.Importer, error) { // rootmulti.Restore closes the store before creating an importer. // Reopen the DBs so the importer can write data.