Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 0 additions & 1 deletion sei-db/state_db/sc/migration/migration_manager.go
Original file line number Diff line number Diff line change
Expand Up @@ -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)
}
Expand Down
84 changes: 84 additions & 0 deletions sei-db/state_db/sc/migration/passthrough_router.go
Original file line number Diff line number Diff line change
@@ -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)
}
199 changes: 199 additions & 0 deletions sei-db/state_db/sc/migration/passthrough_router_test.go
Original file line number Diff line number Diff line change
@@ -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)
}
Loading
Loading