feat(store/v2): add version exists (#22235)

Co-authored-by: Cool Developer <cool199966@outlook.com>
This commit is contained in:
Marko 2024-10-16 17:55:23 +02:00 committed by GitHub
parent abaccb4d4b
commit 76529d7680
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
17 changed files with 237 additions and 32 deletions

View File

@ -43,7 +43,7 @@ type Store interface {
Query(storeKey []byte, version uint64, key []byte, prove bool) (storev2.QueryResult, error)
// GetStateStorage returns the SS backend.
GetStateStorage() storev2.VersionedDatabase
GetStateStorage() storev2.VersionedWriter
// GetStateCommitment returns the SC backend.
GetStateCommitment() storev2.Committer

View File

@ -275,6 +275,7 @@ func (c *CommitStore) GetProof(storeKey []byte, version uint64, key []byte) ([]p
return []proof.CommitmentOp{commitOp, *storeCommitmentOp}, nil
}
// Get implements store.VersionedReader.
func (c *CommitStore) Get(storeKey []byte, version uint64, key []byte) ([]byte, error) {
tree, ok := c.multiTrees[conv.UnsafeBytesToStr(storeKey)]
if !ok {

View File

@ -7,19 +7,29 @@ import (
"cosmossdk.io/store/v2/proof"
)
// VersionedDatabase defines an API for a versioned database that allows reads,
// VersionedWriter defines an API for a versioned database that allows reads,
// writes, iteration and commitment over a series of versions.
type VersionedDatabase interface {
type VersionedWriter interface {
VersionedReader
SetLatestVersion(version uint64) error
ApplyChangeset(version uint64, cs *corestore.Changeset) error
// Close releases associated resources. It should NOT be idempotent. It must
// only be called once and any call after may panic.
io.Closer
}
type VersionedReader interface {
Has(storeKey []byte, version uint64, key []byte) (bool, error)
Get(storeKey []byte, version uint64, key []byte) ([]byte, error)
GetLatestVersion() (uint64, error)
SetLatestVersion(version uint64) error
VersionExists(v uint64) (bool, error)
Iterator(storeKey []byte, version uint64, start, end []byte) (corestore.Iterator, error)
ReverseIterator(storeKey []byte, version uint64, start, end []byte) (corestore.Iterator, error)
ApplyChangeset(version uint64, cs *corestore.Changeset) error
// Close releases associated resources. It should NOT be idempotent. It must
// only be called once and any call after may panic.
io.Closer
@ -53,18 +63,14 @@ type Committer interface {
// GetProof returns the proof of existence or non-existence for the given key.
GetProof(storeKey []byte, version uint64, key []byte) ([]proof.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 []byte, version uint64, key []byte) ([]byte, error)
// SetInitialVersion sets the initial version of the committer.
SetInitialVersion(version uint64) error
// GetCommitInfo returns the CommitInfo for the given version.
GetCommitInfo(version uint64) (*proof.CommitInfo, error)
Get(storeKey []byte, version uint64, key []byte) ([]byte, error)
// Close releases associated resources. It should NOT be idempotent. It must
// only be called once and any call after may panic.
io.Closer

View File

@ -404,3 +404,18 @@ func (mr *MockStateStorageMockRecorder) SetLatestVersion(version any) *gomock.Ca
mr.mock.ctrl.T.Helper()
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "SetLatestVersion", reflect.TypeOf((*MockStateStorage)(nil).SetLatestVersion), version)
}
// VersionExists mocks base method.
func (m *MockStateStorage) VersionExists(v uint64) (bool, error) {
m.ctrl.T.Helper()
ret := m.ctrl.Call(m, "VersionExists", v)
ret0, _ := ret[0].(bool)
ret1, _ := ret[1].(error)
return ret0, ret1
}
// VersionExists indicates an expected call of VersionExists.
func (mr *MockStateStorageMockRecorder) VersionExists(v any) *gomock.Call {
mr.mock.ctrl.T.Helper()
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "VersionExists", reflect.TypeOf((*MockStateStorage)(nil).VersionExists), v)
}

View File

@ -12,7 +12,7 @@ type StateCommitter interface {
// StateStorage is a mock of store.VersionedDatabase
type StateStorage interface {
store.VersionedDatabase
store.VersionedWriter
store.UpgradableDatabase
store.Pruner
store.PausablePruner

View File

@ -38,7 +38,7 @@ type Store struct {
dbCloser io.Closer
// stateStorage reflects the state storage backend
stateStorage store.VersionedDatabase
stateStorage store.VersionedWriter
// stateCommitment reflects the state commitment (SC) backend
stateCommitment store.Committer
@ -74,7 +74,7 @@ type Store struct {
func New(
dbCloser io.Closer,
logger corelog.Logger,
ss store.VersionedDatabase,
ss store.VersionedWriter,
sc store.Committer,
pm *pruning.Manager,
mm *migration.Manager,
@ -127,19 +127,21 @@ func (s *Store) StateLatest() (uint64, corestore.ReaderMap, error) {
return v, NewReaderMap(v, s), nil
}
// StateAt checks if the requested version is present in ss.
func (s *Store) StateAt(v uint64) (corestore.ReaderMap, error) {
// 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/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)
// check if version is present in state storage
isExist, err := s.stateStorage.VersionExists(v)
if err != nil {
return nil, err
}
if !isExist {
return nil, fmt.Errorf("version %d does not exist", v)
}
return NewReaderMap(v, s), nil
}
func (s *Store) GetStateStorage() store.VersionedDatabase {
func (s *Store) GetStateStorage() store.VersionedWriter {
return s.stateStorage
}

View File

@ -16,7 +16,7 @@ import (
"cosmossdk.io/store/v2/pruning"
)
func newTestRootStore(ss store.VersionedDatabase, sc store.Committer) *Store {
func newTestRootStore(ss store.VersionedWriter, sc store.Committer) *Store {
noopLog := coretesting.NewNopLogger()
pm := pruning.NewManager(sc.(store.Pruner), ss.(store.Pruner), nil, nil)
return &Store{

View File

@ -90,7 +90,7 @@ func (s *RootStoreTestSuite) newStoreWithPruneConfig(config *store.PruningOption
s.rootStore = rs
}
func (s *RootStoreTestSuite) newStoreWithBackendMount(ss store.VersionedDatabase, sc store.Committer, pm *pruning.Manager) {
func (s *RootStoreTestSuite) newStoreWithBackendMount(ss store.VersionedWriter, sc store.Committer, pm *pruning.Manager) {
noopLog := coretesting.NewNopLogger()
rs, err := New(dbm.NewMemDB(), noopLog, ss, sc, pm, nil, nil)

View File

@ -16,6 +16,7 @@ type Database interface {
Get(storeKey []byte, version uint64, key []byte) ([]byte, error)
GetLatestVersion() (uint64, error)
SetLatestVersion(version uint64) error
VersionExists(version uint64) (bool, error)
Iterator(storeKey []byte, version uint64, start, end []byte) (corestore.Iterator, error)
ReverseIterator(storeKey []byte, version uint64, start, end []byte) (corestore.Iterator, error)

View File

@ -137,6 +137,15 @@ func (db *Database) GetLatestVersion() (uint64, error) {
return binary.LittleEndian.Uint64(bz), closer.Close()
}
func (db *Database) VersionExists(version uint64) (bool, error) {
latestVersion, err := db.GetLatestVersion()
if err != nil {
return false, err
}
return latestVersion >= version && version >= db.earliestVersion, nil
}
func (db *Database) setPruneHeight(pruneVersion uint64) error {
db.earliestVersion = pruneVersion + 1

View File

@ -131,6 +131,15 @@ func (db *Database) GetLatestVersion() (uint64, error) {
return binary.LittleEndian.Uint64(bz), nil
}
func (db *Database) VersionExists(version uint64) (bool, error) {
latestVersion, err := db.GetLatestVersion()
if err != nil {
return false, err
}
return latestVersion >= version && version >= db.tsLow, nil
}
func (db *Database) Has(storeKey []byte, version uint64, key []byte) (bool, error) {
slice, err := db.getSlice(storeKey, version, key)
if err != nil {

View File

@ -36,6 +36,10 @@ func (db *Database) GetLatestVersion() (uint64, error) {
panic("rocksdb requires a build flag")
}
func (db *Database) VersionExists(version uint64) (bool, error) {
panic("rocksdb requires a build flag")
}
func (db *Database) Has(storeKey []byte, version uint64, key []byte) (bool, error) {
panic("rocksdb requires a build flag")
}

View File

@ -103,7 +103,11 @@ func (db *Database) NewBatch(version uint64) (store.Batch, error) {
}
func (db *Database) GetLatestVersion() (uint64, error) {
stmt, err := db.storage.Prepare("SELECT value FROM state_storage WHERE store_key = ? AND key = ?")
stmt, err := db.storage.Prepare(`
SELECT value
FROM state_storage
WHERE store_key = ? AND key = ?
`)
if err != nil {
return 0, fmt.Errorf("failed to prepare SQL statement: %w", err)
}
@ -123,6 +127,15 @@ func (db *Database) GetLatestVersion() (uint64, error) {
return latestHeight, nil
}
func (db *Database) VersionExists(v uint64) (bool, error) {
latestVersion, err := db.GetLatestVersion()
if err != nil {
return false, err
}
return latestVersion >= v && v >= db.earliestVersion, nil
}
func (db *Database) SetLatestVersion(version uint64) error {
_, err := db.storage.Exec(reservedUpsertStmt, reservedStoreKey, keyLatestHeight, version, 0, version)
if err != nil {

View File

@ -24,12 +24,12 @@ import (
var storeKey1 = []byte("store1")
var (
backends = map[string]func(dataDir string) (store.VersionedDatabase, error){
"rocksdb_versiondb_opts": func(dataDir string) (store.VersionedDatabase, error) {
backends = map[string]func(dataDir string) (store.VersionedWriter, error){
"rocksdb_versiondb_opts": func(dataDir string) (store.VersionedWriter, error) {
db, err := rocksdb.New(dataDir)
return storage.NewStorageStore(db, coretesting.NewNopLogger()), err
},
"pebbledb_default_opts": func(dataDir string) (store.VersionedDatabase, error) {
"pebbledb_default_opts": func(dataDir string) (store.VersionedWriter, error) {
db, err := pebbledb.New(dataDir)
if err == nil && db != nil {
db.SetSync(false)
@ -37,7 +37,7 @@ var (
return storage.NewStorageStore(db, coretesting.NewNopLogger()), err
},
"btree_sqlite": func(dataDir string) (store.VersionedDatabase, error) {
"btree_sqlite": func(dataDir string) (store.VersionedWriter, error) {
db, err := sqlite.New(dataDir)
return storage.NewStorageStore(db, coretesting.NewNopLogger()), err
},

View File

@ -880,9 +880,149 @@ func (s *StorageTestSuite) TestRemovingOldStoreKey() {
}
}
// TestVersionExists tests the VersionExists method of the Database struct.
func (s *StorageTestSuite) TestVersionExists() {
// Define test cases
testCases := []struct {
name string
setup func(t *testing.T, db *StorageStore)
version uint64
expectedExists bool
expectError bool
}{
{
name: "Fresh database: version 0 exists",
setup: func(t *testing.T, db *StorageStore) {
t.Helper()
// No setup needed for fresh database
},
version: 0,
expectedExists: true,
expectError: false,
},
{
name: "Fresh database: version 1 exists",
setup: func(t *testing.T, db *StorageStore) {
t.Helper()
// No setup needed for fresh database
},
version: 1,
expectedExists: false,
expectError: false,
},
{
name: "After setting latest version to 10, version 5 exists",
setup: func(t *testing.T, db *StorageStore) {
t.Helper()
err := db.SetLatestVersion(10)
if err != nil {
t.Fatalf("Setting latest version should not error: %v", err)
}
},
version: 5,
expectedExists: true, // Since pruning hasn't occurred, earliestVersion is still 0
expectError: false,
},
{
name: "After setting latest version to 10 and pruning to 5, version 4 does not exist",
setup: func(t *testing.T, db *StorageStore) {
t.Helper()
err := db.SetLatestVersion(10)
if err != nil {
t.Fatalf("Setting latest version should not error: %v", err)
}
err = db.Prune(5)
if err != nil {
t.Fatalf("Pruning to version 5 should not error: %v", err)
}
},
version: 4,
expectedExists: false,
expectError: false,
},
{
name: "After setting latest version to 10 and pruning to 5, version 5 does not exist",
setup: func(t *testing.T, db *StorageStore) {
t.Helper()
err := db.SetLatestVersion(10)
if err != nil {
t.Fatalf("Setting latest version should not error: %v", err)
}
err = db.Prune(5)
if err != nil {
t.Fatalf("Pruning to version 5 should not error: %v", err)
}
},
version: 5,
expectedExists: false,
expectError: false,
},
{
name: "After setting latest version to 10 and pruning to 5, version 6 exists",
setup: func(t *testing.T, db *StorageStore) {
t.Helper()
err := db.SetLatestVersion(10)
if err != nil {
t.Fatalf("Setting latest version should not error: %v", err)
}
err = db.Prune(5)
if err != nil {
t.Fatalf("Pruning to version 5 should not error: %v", err)
}
},
version: 6,
expectedExists: true,
expectError: false,
},
{
name: "After pruning to 0, all versions >=1 exist",
setup: func(t *testing.T, db *StorageStore) {
t.Helper()
err := db.SetLatestVersion(10)
if err != nil {
t.Fatalf("Setting latest version should not error: %v", err)
}
// Prune to version 0
err = db.Prune(0)
if err != nil {
t.Fatalf("Pruning to version 0 should not error: %v", err)
}
},
version: 1,
expectedExists: true,
expectError: false,
},
}
// Iterate over each test case
for _, tc := range testCases {
s.T().Run(tc.name, func(t *testing.T) {
// Initialize the database for each test
db, err := s.NewDB(t.TempDir())
require.NoError(t, err, "Failed to initialize the database")
defer db.Close()
// Setup test environment
tc.setup(t, db)
// Call VersionExists and check the result
exists, err := db.VersionExists(tc.version)
if tc.expectError {
require.Error(t, err, "Expected error but got none")
} else {
require.NoError(t, err, "Did not expect an error but got one")
require.Equal(t, tc.expectedExists, exists, "Version existence mismatch")
}
})
}
}
func dbApplyChangeset(
t *testing.T,
db store.VersionedDatabase,
db store.VersionedWriter,
version uint64,
storeKey string,
keys, vals [][]byte,

View File

@ -16,7 +16,7 @@ const (
)
var (
_ store.VersionedDatabase = (*StorageStore)(nil)
_ store.VersionedWriter = (*StorageStore)(nil)
_ snapshots.StorageSnapshotter = (*StorageStore)(nil)
_ store.Pruner = (*StorageStore)(nil)
_ store.UpgradableDatabase = (*StorageStore)(nil)
@ -84,6 +84,11 @@ func (ss *StorageStore) SetLatestVersion(version uint64) error {
return ss.db.SetLatestVersion(version)
}
// VersionExists returns true if the given version exists in the store.
func (ss *StorageStore) VersionExists(version uint64) (bool, error) {
return ss.db.VersionExists(version)
}
// Iterator returns an iterator over the specified domain and prefix.
func (ss *StorageStore) Iterator(storeKey []byte, version uint64, start, end []byte) (corestore.Iterator, error) {
return ss.db.Iterator(storeKey, version, start, end)

View File

@ -70,7 +70,7 @@ type RootStore interface {
// Backend defines the interface for the RootStore backends.
type Backend interface {
// GetStateStorage returns the SS backend.
GetStateStorage() VersionedDatabase
GetStateStorage() VersionedWriter
// GetStateCommitment returns the SC backend.
GetStateCommitment() Committer