From 8d0696326bc325072e00b338bbe03d02a3ee1560 Mon Sep 17 00:00:00 2001 From: Aleksandr Bezobchuk Date: Fri, 19 Jan 2024 10:11:15 -0500 Subject: [PATCH] feat(store/v2): Fallback to SC on queries (#19090) --- store/commitment/iavl/tree.go | 15 +++++++++++--- store/commitment/iavl/tree_test.go | 9 ++++++++ store/commitment/store.go | 14 +++++++++++++ store/commitment/tree.go | 10 +++++++++ store/database.go | 13 ++++++++++++ store/root/store.go | 33 ++++++++++++++++++++++-------- store/root/store_test.go | 25 ++++++++++++++++++++++ 7 files changed, 107 insertions(+), 12 deletions(-) diff --git a/store/commitment/iavl/tree.go b/store/commitment/iavl/tree.go index 5bc061932c..d3c126ad0e 100644 --- a/store/commitment/iavl/tree.go +++ b/store/commitment/iavl/tree.go @@ -67,12 +67,21 @@ func (t *IavlTree) Commit() ([]byte, uint64, error) { // GetProof returns a proof for the given key and version. func (t *IavlTree) GetProof(version uint64, key []byte) (*ics23.CommitmentProof, error) { - imutableTree, err := t.tree.GetImmutable(int64(version)) + immutableTree, err := t.tree.GetImmutable(int64(version)) if err != nil { - return nil, err + return nil, fmt.Errorf("failed to get immutable tree at version %d: %w", version, err) } - return imutableTree.GetProof(key) + return immutableTree.GetProof(key) +} + +func (t *IavlTree) Get(version uint64, key []byte) ([]byte, error) { + immutableTree, err := t.tree.GetImmutable(int64(version)) + if err != nil { + return nil, fmt.Errorf("failed to get immutable tree at version %d: %w", version, err) + } + + return immutableTree.Get(key) } // GetLatestVersion returns the latest version of the tree. diff --git a/store/commitment/iavl/tree_test.go b/store/commitment/iavl/tree_test.go index 88575702f3..6963c8aabe 100644 --- a/store/commitment/iavl/tree_test.go +++ b/store/commitment/iavl/tree_test.go @@ -57,6 +57,15 @@ func TestIavlTree(t *testing.T) { require.Equal(t, workingHash, commitHash) require.Equal(t, uint64(1), tree.GetLatestVersion()) + // ensure we can get expected values + bz, err := tree.Get(1, []byte("key1")) + require.NoError(t, err) + require.Equal(t, []byte("value1"), bz) + + bz, err = tree.Get(2, []byte("key1")) + require.Error(t, err) + require.Nil(t, bz) + // write a batch of version 2 require.NoError(t, tree.Set([]byte("key4"), []byte("value4"))) require.NoError(t, tree.Set([]byte("key5"), []byte("value5"))) diff --git a/store/commitment/store.go b/store/commitment/store.go index 297165f867..bfe319e0d0 100644 --- a/store/commitment/store.go +++ b/store/commitment/store.go @@ -255,6 +255,20 @@ func (c *CommitStore) GetProof(storeKey string, version uint64, key []byte) ([]s return []store.CommitmentOp{commitOp, *storeCommitmentOp}, nil } +func (c *CommitStore) Get(storeKey string, version uint64, key []byte) ([]byte, error) { + tree, ok := c.multiTrees[storeKey] + if !ok { + return nil, fmt.Errorf("store %s not found", storeKey) + } + + bz, err := tree.Get(version, key) + if err != nil { + return nil, fmt.Errorf("failed to get key %s from store %s: %w", key, storeKey, err) + } + + return bz, nil +} + func (c *CommitStore) Prune(version uint64) (ferr error) { // prune the metadata batch := c.db.NewBatch() diff --git a/store/commitment/tree.go b/store/commitment/tree.go index 07f7891874..54fe2d60f0 100644 --- a/store/commitment/tree.go +++ b/store/commitment/tree.go @@ -17,14 +17,24 @@ type Tree interface { Set(key, value []byte) error Remove(key []byte) error GetLatestVersion() uint64 + // Hash returns the hash of the latest saved version of the tree. Hash() []byte + // WorkingHash returns the working hash of the tree. WorkingHash() []byte + LoadVersion(version uint64) error Commit() ([]byte, uint64, error) SetInitialVersion(version uint64) error GetProof(version uint64, key []byte) (*ics23.CommitmentProof, error) + + // Get attempts to retrieve a value from the tree for a given version. + // + // NOTE: This method only exists to support migration from IAVL v0/v1 to v2. + // Once migration is complete, this method should be removed and/or not used. + Get(version uint64, key []byte) ([]byte, error) + Prune(version uint64) error Export(version uint64) (Exporter, error) Import(version uint64) (Importer, error) diff --git a/store/database.go b/store/database.go index 9ef7b4f348..e665a97278 100644 --- a/store/database.go +++ b/store/database.go @@ -69,18 +69,31 @@ type VersionedDatabase interface { type Committer interface { // WriteBatch writes a batch of key-value pairs to the tree. WriteBatch(cs *Changeset) error + // WorkingCommitInfo returns the CommitInfo for the working tree. WorkingCommitInfo(version uint64) *CommitInfo + // GetLatestVersion returns the latest version. GetLatestVersion() (uint64, error) + // LoadVersion loads the tree at the given version. LoadVersion(targetVersion uint64) error + // Commit commits the working tree to the database. Commit(version uint64) (*CommitInfo, error) + // GetProof returns the proof of existence or non-existence for the given key. GetProof(storeKey string, version uint64, key []byte) ([]CommitmentOp, error) + + // Get returns the value for the given key at the given version. + // + // NOTE: This method only exists to support migration from IAVL v0/v1 to v2. + // Once migration is complete, this method should be removed and/or not used. + Get(storeKey string, version uint64, key []byte) ([]byte, error) + // SetInitialVersion sets the initial version of the tree. SetInitialVersion(version uint64) error + // GetCommitInfo returns the CommitInfo for the given version. GetCommitInfo(version uint64) (*CommitInfo, error) diff --git a/store/root/store.go b/store/root/store.go index 2e55c1600c..4495dc3946 100644 --- a/store/root/store.go +++ b/store/root/store.go @@ -105,13 +105,13 @@ func (s *Store) StateLatest() (uint64, store.ReadOnlyRootStore, error) { } func (s *Store) StateAt(v uint64) (store.ReadOnlyRootStore, error) { - // TODO(bez): Ensure the version exists. We can utilize the GetCommitInfo() - // SC method once available. + // TODO(bez): We may want to avoid relying on the SC metadata here. Instead, + // we should add a VersionExists() method to the VersionedDatabase interface. // - // Ref: https://github.com/cosmos/cosmos-sdk/pull/18736 - // if err := s.stateCommitment.GetCommitInfo(v); err != nil { - // return nil, fmt.Errorf("failed to get commit info for version %d: %w", v, err) - // } + // Ref: https://github.com/cosmos/cosmos-sdk/issues/19091 + if cInfo, err := s.stateCommitment.GetCommitInfo(v); err != nil || cInfo == nil { + return nil, fmt.Errorf("failed to get commit info for version %d: %w", v, err) + } return NewReadOnlyAdapter(v, s), nil } @@ -174,8 +174,23 @@ func (s *Store) Query(storeKey string, version uint64, key []byte, prove bool) ( } val, err := s.stateStore.Get(storeKey, version, key) - if err != nil { - return store.QueryResult{}, err + if err != nil || val == nil { + // fallback to querying SC backend if not found in SS backend + // + // Note, this should only used during migration, i.e. while SS and IAVL v2 + // are being asynchronously synced. + if val == nil { + bz, scErr := s.stateCommitment.Get(storeKey, version, key) + if scErr != nil { + return store.QueryResult{}, fmt.Errorf("failed to query SC store: %w", scErr) + } + + val = bz + } + + if err != nil { + return store.QueryResult{}, fmt.Errorf("failed to query SS store: %w", err) + } } result := store.QueryResult{ @@ -187,7 +202,7 @@ func (s *Store) Query(storeKey string, version uint64, key []byte, prove bool) ( if prove { result.ProofOps, err = s.stateCommitment.GetProof(storeKey, version, key) if err != nil { - return store.QueryResult{}, err + return store.QueryResult{}, fmt.Errorf("failed to get SC store proof: %w", err) } } diff --git a/store/root/store_test.go b/store/root/store_test.go index 83a07edf2c..bbdaceabb8 100644 --- a/store/root/store_test.go +++ b/store/root/store_test.go @@ -104,6 +104,31 @@ func (s *RootStoreTestSuite) TestQuery() { s.Require().Equal([]byte("foo"), result.ProofOps[0].Key) } +func (s *RootStoreTestSuite) TestGetFallback() { + sc := s.rootStore.GetStateCommitment() + + // create a changeset and commit it to SC ONLY + cs := store.NewChangeset() + cs.Add(testStoreKey, []byte("foo"), []byte("bar")) + + err := sc.WriteBatch(cs) + s.Require().NoError(err) + + ci := sc.WorkingCommitInfo(1) + _, err = sc.Commit(ci.Version) + s.Require().NoError(err) + + // ensure we can query for the key, which should fallback to SC + qResult, err := s.rootStore.Query(testStoreKey, 1, []byte("foo"), false) + s.Require().NoError(err) + s.Require().Equal([]byte("bar"), qResult.Value) + + // non-existent key + qResult, err = s.rootStore.Query(testStoreKey, 1, []byte("non_existent_key"), false) + s.Require().NoError(err) + s.Require().Nil(qResult.Value) +} + func (s *RootStoreTestSuite) TestQueryProof() { cs := store.NewChangeset() // testStoreKey