package root import ( "crypto/sha256" "errors" "fmt" "io" "time" corelog "cosmossdk.io/core/log" corestore "cosmossdk.io/core/store" "cosmossdk.io/store/v2" "cosmossdk.io/store/v2/metrics" "cosmossdk.io/store/v2/proof" "cosmossdk.io/store/v2/pruning" ) var ( _ store.RootStore = (*Store)(nil) _ store.UpgradeableStore = (*Store)(nil) ) // Store defines the SDK's default RootStore implementation. It contains a single // State Storage (SS) backend and a single State Commitment (SC) backend. The SC // backend may or may not support multiple store keys and is implementation // dependent. type Store struct { logger corelog.Logger // holds the db instance for closing it dbCloser io.Closer // stateCommitment reflects the state commitment (SC) backend stateCommitment store.Committer // lastCommitInfo reflects the last version/hash that has been committed lastCommitInfo *proof.CommitInfo // telemetry reflects a telemetry agent responsible for emitting metrics (if any) telemetry metrics.StoreMetrics // pruningManager reflects the pruning manager used to prune state of the SS and SC backends pruningManager *pruning.Manager } // New creates a new root Store instance. // // NOTE: The migration manager is optional and can be nil if no migration is required. func New( dbCloser io.Closer, logger corelog.Logger, sc store.Committer, pm *pruning.Manager, m metrics.StoreMetrics, ) (store.RootStore, error) { return &Store{ dbCloser: dbCloser, logger: logger, stateCommitment: sc, pruningManager: pm, telemetry: m, }, nil } // Close closes the store and resets all internal fields. Note, Close() is NOT // idempotent and should only be called once. func (s *Store) Close() (err error) { err = errors.Join(err, s.stateCommitment.Close()) err = errors.Join(err, s.dbCloser.Close()) s.stateCommitment = nil s.lastCommitInfo = nil return err } func (s *Store) SetMetrics(m metrics.Metrics) { s.telemetry = m } func (s *Store) SetInitialVersion(v uint64) error { return s.stateCommitment.SetInitialVersion(v) } // getVersionedReader returns a VersionedReader based on the given version. If the // version exists in the state storage, it returns the state storage. // If not, it checks if the state commitment implements the VersionedReader interface // and the version exists in the state commitment, since the state storage will be // synced during migration. func (s *Store) getVersionedReader(version uint64) (store.VersionedReader, error) { isExist, err := s.stateCommitment.VersionExists(version) if err != nil { return nil, err } if isExist { return s.stateCommitment, nil } return nil, fmt.Errorf("version %d does not exist", version) } // StateAt returns a read-only view of the state at a given version. func (s *Store) StateAt(v uint64) (corestore.ReaderMap, error) { vReader, err := s.getVersionedReader(v) return NewReaderMap(v, vReader), err } func (s *Store) StateLatest() (uint64, corestore.ReaderMap, error) { v, err := s.GetLatestVersion() if err != nil { return 0, nil, err } vReader, err := s.getVersionedReader(v) if err != nil { return 0, nil, err } return v, NewReaderMap(v, vReader), nil } func (s *Store) GetStateCommitment() store.Committer { return s.stateCommitment } // LastCommitID returns a CommitID based off of the latest internal CommitInfo. // If an internal CommitInfo is not set, a new one will be returned with only the // latest version set, which is based off of the SC view. func (s *Store) LastCommitID() (proof.CommitID, error) { if s.lastCommitInfo != nil { return *s.lastCommitInfo.CommitID(), nil } latestVersion, err := s.stateCommitment.GetLatestVersion() if err != nil { return proof.CommitID{}, err } // if the latest version is 0, we return a CommitID with version 0 and a hash of an empty byte slice bz := sha256.Sum256([]byte{}) return proof.CommitID{Version: int64(latestVersion), Hash: bz[:]}, nil } // GetLatestVersion returns the latest version based on the latest internal // CommitInfo. An error is returned if the latest CommitInfo or version cannot // be retrieved. func (s *Store) GetLatestVersion() (uint64, error) { lastCommitID, err := s.LastCommitID() if err != nil { return 0, err } return uint64(lastCommitID.Version), nil } func (s *Store) Query(storeKey []byte, version uint64, key []byte, prove bool) (store.QueryResult, error) { if s.telemetry != nil { defer s.telemetry.MeasureSince(time.Now(), "root_store", "query") } val, err := s.stateCommitment.Get(storeKey, version, key) if err != nil { return store.QueryResult{}, fmt.Errorf("failed to query SC store: %w", err) } result := store.QueryResult{ Key: key, Value: val, Version: version, } if prove { result.ProofOps, err = s.stateCommitment.GetProof(storeKey, version, key) if err != nil { return store.QueryResult{}, fmt.Errorf("failed to get SC store proof: %w", err) } } return result, nil } func (s *Store) LoadLatestVersion() error { if s.telemetry != nil { defer s.telemetry.MeasureSince(time.Now(), "root_store", "load_latest_version") } lv, err := s.GetLatestVersion() if err != nil { return err } return s.loadVersion(lv, nil, false) } func (s *Store) LoadVersion(version uint64) error { if s.telemetry != nil { defer s.telemetry.MeasureSince(time.Now(), "root_store", "load_version") } return s.loadVersion(version, nil, false) } func (s *Store) LoadVersionForOverwriting(version uint64) error { if s.telemetry != nil { defer s.telemetry.MeasureSince(time.Now(), "root_store", "load_version_for_overwriting") } return s.loadVersion(version, nil, true) } // LoadVersionAndUpgrade implements the UpgradeableStore interface. // // NOTE: It cannot be called while the store is migrating. func (s *Store) LoadVersionAndUpgrade(version uint64, upgrades *corestore.StoreUpgrades) error { if upgrades == nil { return errors.New("upgrades cannot be nil") } if s.telemetry != nil { defer s.telemetry.MeasureSince(time.Now(), "root_store", "load_version_and_upgrade") } if err := s.loadVersion(version, upgrades, true); err != nil { return err } return nil } func (s *Store) loadVersion(v uint64, upgrades *corestore.StoreUpgrades, overrideAfter bool) error { s.logger.Debug("loading version", "version", v) if upgrades == nil { if !overrideAfter { if err := s.stateCommitment.LoadVersion(v); err != nil { return fmt.Errorf("failed to load SC version %d: %w", v, err) } } else { if err := s.stateCommitment.LoadVersionForOverwriting(v); err != nil { return fmt.Errorf("failed to load SC version %d: %w", v, err) } } } else { // if upgrades are provided, we need to load the version and apply the upgrades if err := s.stateCommitment.LoadVersionAndUpgrade(v, upgrades); err != nil { return fmt.Errorf("failed to load SS version with upgrades %d: %w", v, err) } } // set lastCommitInfo explicitly s.t. Commit commits the correct version, i.e. v+1 var err error s.lastCommitInfo, err = s.stateCommitment.GetCommitInfo(v) if err != nil { return fmt.Errorf("failed to get commit info for version %d: %w", v, err) } return nil } // Commit commits all state changes to the underlying SS and SC backends. It // writes a batch of the changeset to the SC tree, and retrieves the CommitInfo // from the SC tree. Finally, it commits the SC tree and returns the hash of // the CommitInfo. func (s *Store) Commit(cs *corestore.Changeset) ([]byte, error) { if s.telemetry != nil { now := time.Now() defer func() { s.telemetry.MeasureSince(now, "root_store", "commit") }() } // signal to the pruning manager that a new version is about to be committed // this may be required if the SS and SC backends implementation have the // background pruning process (iavl v1 for example) which must be paused during the commit s.pruningManager.PausePruning() st := time.Now() if err := s.stateCommitment.WriteChangeset(cs); err != nil { return nil, fmt.Errorf("failed to write batch to SC store: %w", err) } writeDur := time.Since(st) st = time.Now() cInfo, err := s.stateCommitment.Commit(cs.Version) if err != nil { return nil, fmt.Errorf("failed to commit SC store: %w", err) } s.logger.Warn(fmt.Sprintf("commit version %d write=%s commit=%s", cs.Version, writeDur, time.Since(st))) if cInfo.Version != int64(cs.Version) { return nil, fmt.Errorf("commit version mismatch: got %d, expected %d", cInfo.Version, cs.Version) } s.lastCommitInfo = cInfo // signal to the pruning manager that the commit is done if err := s.pruningManager.ResumePruning(uint64(s.lastCommitInfo.Version)); err != nil { s.logger.Error("failed to signal commit done to pruning manager", "err", err) } return s.lastCommitInfo.Hash(), nil } func (s *Store) Prune(version uint64) error { return s.pruningManager.Prune(version) }