diff --git a/sei-db/state_db/sc/migration/migration_manager.go b/sei-db/state_db/sc/migration/migration_manager.go index b98ec30407..3a93506225 100644 --- a/sei-db/state_db/sc/migration/migration_manager.go +++ b/sei-db/state_db/sc/migration/migration_manager.go @@ -329,7 +329,6 @@ func (m *MigrationManager) ApplyChangeSets(changesets []*proto.NamedChangeSet) e }) } - // Write to the old DB first, then the new DB. if err := m.oldDBWriter(oldDBChangeSet); err != nil { return fmt.Errorf("failed to apply changes to old database: %w", err) } diff --git a/sei-db/state_db/sc/migration/passthrough_router.go b/sei-db/state_db/sc/migration/passthrough_router.go new file mode 100644 index 0000000000..c59e153394 --- /dev/null +++ b/sei-db/state_db/sc/migration/passthrough_router.go @@ -0,0 +1,84 @@ +package migration + +import ( + "fmt" + + ics23 "github.com/confio/ics23/go" + "github.com/sei-protocol/sei-chain/sei-db/proto" + db "github.com/tendermint/tm-db" +) + +var _ Router = (*PassthroughRouter)(nil) + +// PassthroughRouter implements Router for single-backend modes where +// every operation goes to the same destination regardless of store +// name. Unlike ModuleRouter it holds no name -> backend map and +// performs no per-call name lookup: each method forwards its arguments +// straight to the supplied accessor. The wrapped accessors are +// themselves responsible for surfacing "unknown store" errors when the +// backend does not recognize the name. +// +// Used by MemiavlOnly mode where every store lives on memiavl and +// memiavl already reports unknown child stores from +// GetChildStoreByName. +type PassthroughRouter struct { + reader DBReader + writer DBWriter + iteratorBuilder DBIteratorBuilder + proofBuilder DBProofBuilder +} + +// NewPassthroughRouter builds a router that forwards every operation +// to the supplied accessors. The reader and writer are required. +// iteratorBuilder and proofBuilder are optional: when nil, the +// corresponding Router method returns an error describing the missing +// capability (e.g. flatkv has no proof builder). +func NewPassthroughRouter( + reader DBReader, + writer DBWriter, + iteratorBuilder DBIteratorBuilder, + proofBuilder DBProofBuilder, +) (*PassthroughRouter, error) { + if reader == nil { + return nil, fmt.Errorf("reader must not be nil") + } + if writer == nil { + return nil, fmt.Errorf("writer must not be nil") + } + return &PassthroughRouter{ + reader: reader, + writer: writer, + iteratorBuilder: iteratorBuilder, + proofBuilder: proofBuilder, + }, nil +} + +// Read forwards directly to the wrapped reader. +func (p *PassthroughRouter) Read(store string, key []byte) ([]byte, bool, error) { + return p.reader(store, key) +} + +// ApplyChangeSets forwards directly to the wrapped writer. The router +// performs no per-changeset name validation; the writer (and its +// backing store) is the sole authority on which names it accepts. +func (p *PassthroughRouter) ApplyChangeSets(changesets []*proto.NamedChangeSet) error { + return p.writer(changesets) +} + +// Iterator forwards to the wrapped iterator builder. If no iterator +// builder was supplied, returns an error describing the limitation. +func (p *PassthroughRouter) Iterator(store string, start []byte, end []byte, ascending bool) (db.Iterator, error) { + if p.iteratorBuilder == nil { + return nil, fmt.Errorf("iteration not supported by passthrough router (store=%q)", store) + } + return p.iteratorBuilder(store, start, end, ascending) +} + +// GetProof forwards to the wrapped proof builder. If no proof builder +// was supplied, returns an error describing the limitation. +func (p *PassthroughRouter) GetProof(store string, key []byte) (*ics23.CommitmentProof, error) { + if p.proofBuilder == nil { + return nil, fmt.Errorf("proofs not supported by passthrough router (store=%q)", store) + } + return p.proofBuilder(store, key) +} diff --git a/sei-db/state_db/sc/migration/passthrough_router_test.go b/sei-db/state_db/sc/migration/passthrough_router_test.go new file mode 100644 index 0000000000..f2bb71cd55 --- /dev/null +++ b/sei-db/state_db/sc/migration/passthrough_router_test.go @@ -0,0 +1,199 @@ +package migration + +import ( + "errors" + "testing" + + ics23 "github.com/confio/ics23/go" + "github.com/sei-protocol/sei-chain/sei-db/proto" + "github.com/stretchr/testify/require" + dbm "github.com/tendermint/tm-db" +) + +// newPassthroughRouterForTest wires the standard mockDB reader/writer +// into a PassthroughRouter with nil iterator/proof builders. Tests +// that need iteration or proofs construct the router directly. +func newPassthroughRouterForTest(t *testing.T) (*PassthroughRouter, *mockDB) { + t.Helper() + db := newMockDB() + r, err := NewPassthroughRouter(db.reader(), db.writer(), nil, nil) + require.NoError(t, err) + return r, db +} + +// TestPassthroughRouterRequiresReader verifies that NewPassthroughRouter +// rejects a nil reader. The router has no internal default and would +// nil-panic on the first Read call if we let it through. +func TestPassthroughRouterRequiresReader(t *testing.T) { + db := newMockDB() + r, err := NewPassthroughRouter(nil, db.writer(), nil, nil) + require.Error(t, err) + require.Nil(t, r) + require.Contains(t, err.Error(), "reader") +} + +// TestPassthroughRouterRequiresWriter verifies that NewPassthroughRouter +// rejects a nil writer. ApplyChangeSets has no fallback path. +func TestPassthroughRouterRequiresWriter(t *testing.T) { + db := newMockDB() + r, err := NewPassthroughRouter(db.reader(), nil, nil, nil) + require.Error(t, err) + require.Nil(t, r) + require.Contains(t, err.Error(), "writer") +} + +// TestPassthroughRouterReadForwardsAnyName is the core property test: +// the passthrough router never inspects the store name. Reads for +// names that are not in keys.MemIAVLStoreKeys (e.g. icahost) must +// still hit the backend. +func TestPassthroughRouterReadForwardsAnyName(t *testing.T) { + r, db := newPassthroughRouterForTest(t) + db.seed(map[string]map[string][]byte{ + "icahost": {"k": []byte("v")}, + }) + + got, ok, err := r.Read("icahost", []byte("k")) + require.NoError(t, err) + require.True(t, ok) + require.Equal(t, []byte("v"), got) +} + +// TestPassthroughRouterReadPropagatesReaderError verifies that a +// reader error is returned verbatim rather than masked by a routing +// error. ModuleRouter would have rejected unknown names before +// calling the reader; the passthrough router must not. +func TestPassthroughRouterReadPropagatesReaderError(t *testing.T) { + sentinel := errors.New("backend exploded") + r, err := NewPassthroughRouter(failReader(sentinel), newMockDB().writer(), nil, nil) + require.NoError(t, err) + + _, _, err = r.Read("anything", []byte("k")) + require.ErrorIs(t, err, sentinel) +} + +// TestPassthroughRouterApplyChangeSetsForwardsAnyName verifies that +// writes to names outside keys.MemIAVLStoreKeys are accepted and +// persisted. The mockDB writer records the raw batch so we can +// confirm the changesets reach it unmodified. +func TestPassthroughRouterApplyChangeSetsForwardsAnyName(t *testing.T) { + r, db := newPassthroughRouterForTest(t) + + batch := []*proto.NamedChangeSet{ + {Name: "icahost", Changeset: proto.ChangeSet{Pairs: []*proto.KVPair{ + {Key: []byte("k1"), Value: []byte("v1")}, + }}}, + {Name: "icacontroller", Changeset: proto.ChangeSet{Pairs: []*proto.KVPair{ + {Key: []byte("k2"), Value: []byte("v2")}, + }}}, + } + require.NoError(t, r.ApplyChangeSets(batch)) + + require.Len(t, db.writeLog, 1) + require.Equal(t, batch, db.writeLog[0]) + + v, ok := db.get("icahost", "k1") + require.True(t, ok) + require.Equal(t, []byte("v1"), v) + v, ok = db.get("icacontroller", "k2") + require.True(t, ok) + require.Equal(t, []byte("v2"), v) +} + +// TestPassthroughRouterApplyChangeSetsPropagatesWriterError verifies +// that the writer's error surfaces unwrapped. +func TestPassthroughRouterApplyChangeSetsPropagatesWriterError(t *testing.T) { + sentinel := errors.New("backend exploded") + r, err := NewPassthroughRouter(newMockDB().reader(), failWriter(sentinel), nil, nil) + require.NoError(t, err) + + err = r.ApplyChangeSets([]*proto.NamedChangeSet{{Name: "anything"}}) + require.ErrorIs(t, err, sentinel) +} + +// TestPassthroughRouterIteratorWithoutBuilder verifies that a router +// constructed without an iterator builder rejects Iterator() with a +// clean, descriptive error rather than nil-panicking. +func TestPassthroughRouterIteratorWithoutBuilder(t *testing.T) { + r, _ := newPassthroughRouterForTest(t) + + it, err := r.Iterator("icahost", nil, nil, true) + require.Error(t, err) + require.Nil(t, it) + require.Contains(t, err.Error(), "iteration not supported") + require.Contains(t, err.Error(), "icahost") +} + +// TestPassthroughRouterIteratorForwardsToBuilder verifies that when an +// iterator builder is supplied, calls forward with arguments intact +// and the builder's returned iterator/error are returned verbatim. +func TestPassthroughRouterIteratorForwardsToBuilder(t *testing.T) { + var captured struct { + store string + start []byte + end []byte + ascending bool + called bool + } + sentinelIter, err := dbm.NewMemDB().Iterator(nil, nil) + require.NoError(t, err) + t.Cleanup(func() { _ = sentinelIter.Close() }) + + builder := func(store string, start, end []byte, ascending bool) (dbm.Iterator, error) { + captured.store = store + captured.start = start + captured.end = end + captured.ascending = ascending + captured.called = true + return sentinelIter, nil + } + r, err2 := NewPassthroughRouter(newMockDB().reader(), newMockDB().writer(), builder, nil) + require.NoError(t, err2) + + got, err := r.Iterator("icahost", []byte("s"), []byte("e"), true) + require.NoError(t, err) + require.True(t, captured.called) + require.Equal(t, "icahost", captured.store) + require.Equal(t, []byte("s"), captured.start) + require.Equal(t, []byte("e"), captured.end) + require.True(t, captured.ascending) + require.Equal(t, sentinelIter, got) +} + +// TestPassthroughRouterGetProofWithoutBuilder verifies the proof path +// is symmetric with iterator: missing builder yields a clear error. +func TestPassthroughRouterGetProofWithoutBuilder(t *testing.T) { + r, _ := newPassthroughRouterForTest(t) + + p, err := r.GetProof("icahost", []byte("k")) + require.Error(t, err) + require.Nil(t, p) + require.Contains(t, err.Error(), "proofs not supported") + require.Contains(t, err.Error(), "icahost") +} + +// TestPassthroughRouterGetProofForwardsToBuilder verifies that when a +// proof builder is supplied, the call forwards with arguments intact +// and the builder's output is returned verbatim. +func TestPassthroughRouterGetProofForwardsToBuilder(t *testing.T) { + want := &ics23.CommitmentProof{} + var captured struct { + store string + key []byte + called bool + } + builder := func(store string, key []byte) (*ics23.CommitmentProof, error) { + captured.store = store + captured.key = key + captured.called = true + return want, nil + } + r, err := NewPassthroughRouter(newMockDB().reader(), newMockDB().writer(), nil, builder) + require.NoError(t, err) + + got, err := r.GetProof("icahost", []byte("k")) + require.NoError(t, err) + require.True(t, captured.called) + require.Equal(t, "icahost", captured.store) + require.Equal(t, []byte("k"), captured.key) + require.Same(t, want, got) +} diff --git a/sei-db/state_db/sc/migration/router_builder.go b/sei-db/state_db/sc/migration/router_builder.go index 7c06c30bfd..9143444d7c 100644 --- a/sei-db/state_db/sc/migration/router_builder.go +++ b/sei-db/state_db/sc/migration/router_builder.go @@ -19,7 +19,7 @@ func BuildRouter( ctx context.Context, writeMode WriteMode, memIAVL *memiavl.CommitStore, - flatKV *flatkv.CommitStore, + flatKV flatkv.Store, // If this router will be doing data migration, this is the number of keys to migrate in each batch. migrationBatchSize int, ) (Router, error) { @@ -104,9 +104,9 @@ func BuildRouter( /* Data flow: MemiavlOnly (0) - ┌──────────────┐ ┌─────────┐ -──all-modules────────▶ │ moduleRouter │ ──────────all-modules──────────▶ │ memIAVL │ - └──────────────┘ └─────────┘ + ┌─────────────┐ ┌─────────┐ +──all-modules────────▶ │ passthrough │ ──────────all-modules──────────▶ │ memIAVL │ + └─────────────┘ └─────────┘ */ // Build a router for handling write mode MemiavlOnly. Operates on a schema at migration version 0. @@ -117,14 +117,14 @@ func buildMemiavlOnlyRouter( return nil, fmt.Errorf("memIAVL is nil") } - route, err := routeToMemIAVL(memIAVL, keys.MemIAVLStoreKeys...) - if err != nil { - return nil, fmt.Errorf("routeToMemIAVL: %w", err) - } - - router, err := NewModuleRouter(route) + router, err := NewPassthroughRouter( + buildMemIAVLReader(memIAVL), + buildMemIAVLWriter(memIAVL), + buildMemIAVLIteratorBuilder(memIAVL), + buildMemIAVLProofBuilder(memIAVL), + ) if err != nil { - return nil, fmt.Errorf("NewModuleRouter: %w", err) + return nil, fmt.Errorf("NewPassthroughRouter: %w", err) } return router, nil @@ -149,7 +149,7 @@ func buildMemiavlOnlyRouter( func buildMigrateEVMRouter( ctx context.Context, memIAVL *memiavl.CommitStore, - flatKV *flatkv.CommitStore, + flatKV flatkv.Store, migrationBatchSize int, ) (Router, error) { @@ -219,7 +219,7 @@ func buildMigrateEVMRouter( // Build a router for handling write mode EVMMigrated. Operates on a schema at migration version 1. func buildEVMMigratedRouter( memIAVL *memiavl.CommitStore, - flatKV *flatkv.CommitStore, + flatKV flatkv.Store, ) (Router, error) { if memIAVL == nil { @@ -273,7 +273,7 @@ func buildEVMMigratedRouter( func buildMigrateAllButBankRouter( ctx context.Context, memIAVL *memiavl.CommitStore, - flatKV *flatkv.CommitStore, + flatKV flatkv.Store, migrationBatchSize int, ) (Router, error) { @@ -349,7 +349,7 @@ func buildMigrateAllButBankRouter( // Build a router for handling write mode AllMigratedButBank. Operates on a schema at migration version 2. func buildAllMigratedButBankRouter( memIAVL *memiavl.CommitStore, - flatKV *flatkv.CommitStore, + flatKV flatkv.Store, ) (Router, error) { if memIAVL == nil { @@ -402,7 +402,7 @@ func buildAllMigratedButBankRouter( func buildMigrateBankRouter( ctx context.Context, memIAVL *memiavl.CommitStore, - flatKV *flatkv.CommitStore, + flatKV flatkv.Store, migrationBatchSize int, ) (Router, error) { @@ -459,27 +459,27 @@ func buildMigrateBankRouter( /* Data flow: FlatKVOnly (3) - ┌──────────────┐ ┌────────┐ -──all-modules────────▶ │ moduleRouter │ ──────────all-modules──────────▶ │ flatKV │ - └──────────────┘ └────────┘ + ┌─────────────┐ ┌────────┐ +──all-modules────────▶ │ passthrough │ ──────────all-modules──────────▶ │ flatKV │ + └─────────────┘ └────────┘ */ // Build a router for handling write mode FlatKVOnly. Operates on a schema at migration version 3. func buildFlatKVOnlyRouter( - flatKV *flatkv.CommitStore, + flatKV flatkv.Store, ) (Router, error) { if flatKV == nil { return nil, fmt.Errorf("flatKV is nil") } - route, err := routeToFlatKV(flatKV, keys.MemIAVLStoreKeys...) - if err != nil { - return nil, fmt.Errorf("routeToFlatKV: %w", err) - } - - router, err := NewModuleRouter(route) + router, err := NewPassthroughRouter( + buildFlatKVReader(flatKV), + buildFlatKVWriter(flatKV), + nil, // iteration not supported by flatkv + nil, // proof building not supported by flatkv + ) if err != nil { - return nil, fmt.Errorf("NewModuleRouter: %w", err) + return nil, fmt.Errorf("NewPassthroughRouter: %w", err) } return router, nil @@ -505,7 +505,7 @@ func buildFlatKVOnlyRouter( // CRITICAL: this is a test-only router and should never be deployed to production machines. func buildTestOnlyDualWriteRouter( memIAVL *memiavl.CommitStore, - flatKV *flatkv.CommitStore, + flatKV flatkv.Store, ) (Router, error) { if memIAVL == nil { return nil, fmt.Errorf("memIAVL is nil") @@ -597,7 +597,7 @@ func buildMemIAVLProofBuilder(memIAVL *memiavl.CommitStore) DBProofBuilder { } // Build a function capable of reading data from flatkv. -func buildFlatKVReader(flatKV *flatkv.CommitStore) DBReader { +func buildFlatKVReader(flatKV flatkv.Store) DBReader { return func(store string, key []byte) ([]byte, bool, error) { value, found := flatKV.Get(store, key) return value, found, nil @@ -605,7 +605,7 @@ func buildFlatKVReader(flatKV *flatkv.CommitStore) DBReader { } // Build a function capable of writing data to flatkv. -func buildFlatKVWriter(flatKV *flatkv.CommitStore) DBWriter { +func buildFlatKVWriter(flatKV flatkv.Store) DBWriter { return func(changesets []*proto.NamedChangeSet) error { err := flatKV.ApplyChangeSets(changesets) if err != nil { @@ -627,7 +627,7 @@ func routeToMemIAVL(memIAVL *memiavl.CommitStore, moduleNames ...string) (*Route } // Build a route to a flatkv store for the given module names. -func routeToFlatKV(flatKV *flatkv.CommitStore, moduleNames ...string) (*Route, error) { +func routeToFlatKV(flatKV flatkv.Store, moduleNames ...string) (*Route, error) { return NewRoute( buildFlatKVReader(flatKV), buildFlatKVWriter(flatKV),