Skip to content
Draft
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
33 changes: 33 additions & 0 deletions app/seidb.go
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@ import (
"github.com/sei-protocol/sei-chain/sei-cosmos/baseapp"
servertypes "github.com/sei-protocol/sei-chain/sei-cosmos/server/types"
"github.com/sei-protocol/sei-chain/sei-cosmos/storev2/rootmulti"
"github.com/sei-protocol/sei-chain/sei-cosmos/version"
"github.com/sei-protocol/sei-chain/sei-db/config"
seidb "github.com/sei-protocol/sei-chain/sei-db/db_engine/types"
sctypes "github.com/sei-protocol/sei-chain/sei-db/state_db/sc/types"
Expand Down Expand Up @@ -39,6 +40,13 @@ const (
FlagSCKeysToMigratePerBlock = "state-commit.sc-keys-to-migrate-per-block"
FlagSCFlatKVReadWriteMetrics = "state-commit.flatkv.enable-read-write-metrics"

// Hash logger configs (per-block hash logging; enabled by default)
FlagSCHashLoggerEnable = "state-commit.sc-hash-logger-enable"
FlagSCHashLoggerDirectory = "state-commit.sc-hash-logger-directory"
FlagSCHashLoggerBlocksToRetain = "state-commit.sc-hash-logger-blocks-to-retain"
FlagSCHashLoggerTargetFileSize = "state-commit.sc-hash-logger-target-file-size"
FlagSCHashLoggerMaxDiskSize = "state-commit.sc-hash-logger-max-disk-size"

// SS Store configs
FlagSSEnable = "state-store.ss-enable"
FlagSSDirectory = "state-store.ss-db-directory"
Expand Down Expand Up @@ -142,6 +150,31 @@ func parseSCConfigs(appOpts servertypes.AppOptions) config.StateCommitConfig {
}
}

// Hash logger. Guard each read with v != nil so an absent app.toml entry preserves the default
// (notably Enable, which defaults to true) instead of clobbering it to the zero value.
if v := appOpts.Get(FlagSCHashLoggerEnable); v != nil {
scConfig.HashLogger.Enable = cast.ToBool(v)
}
if v := appOpts.Get(FlagSCHashLoggerDirectory); v != nil {
scConfig.HashLogger.Directory = cast.ToString(v)
}
// BlocksToRetain and MaxDiskSize take a configured value verbatim, including 0 (which disables that
// retention dimension). TargetFileSize must stay > 0, so a 0/absent value preserves the default.
if v := appOpts.Get(FlagSCHashLoggerBlocksToRetain); v != nil {
scConfig.HashLogger.BlocksToRetain = cast.ToUint(v)
}
if v := appOpts.Get(FlagSCHashLoggerTargetFileSize); v != nil {
if n := cast.ToUint(v); n > 0 {
scConfig.HashLogger.TargetFileSize = n
}
}
if v := appOpts.Get(FlagSCHashLoggerMaxDiskSize); v != nil {
scConfig.HashLogger.MaxDiskSize = cast.ToUint(v)
}
// The software version is embedded in hash log file names so archives from different builds are
// distinguishable. Sourced from the node build version, not from app.toml.
scConfig.HashLogger.Version = version.Version

return scConfig
}

Expand Down
26 changes: 26 additions & 0 deletions sei-cosmos/baseapp/abci.go
Original file line number Diff line number Diff line change
Expand Up @@ -22,6 +22,7 @@ import (
sdkerrors "github.com/sei-protocol/sei-chain/sei-cosmos/types/errors"
"github.com/sei-protocol/sei-chain/sei-cosmos/types/legacytm"
abci "github.com/sei-protocol/sei-chain/sei-tendermint/abci/types"
"github.com/sei-protocol/sei-chain/sei-tendermint/crypto/merkle"
tmproto "github.com/sei-protocol/sei-chain/sei-tendermint/proto/tendermint/types"
"go.opentelemetry.io/otel/attribute"
otelmetric "go.opentelemetry.io/otel/metric"
Expand Down Expand Up @@ -293,6 +294,17 @@ func (app *BaseApp) Commit(ctx context.Context) (res *abci.ResponseCommit, err e

app.WriteState()
app.GetWorkingHash()
// Hand the block hash to the commit store so it can record it in the per-block hash log under the
// same block number as the state hashes (the commit store cannot see the block hash on its own).
if reporter, ok := app.cms.(interface{ SetNextBlockHash([]byte) }); ok {
reporter.SetNextBlockHash(app.stateToCommit.ctx.HeaderHash())
}
// Hand the result hash (computed in FinalizeBlock from the block's tx results) to the commit
// store so it lands in the same per-block hash log row as the block and state hashes.
if reporter, ok := app.cms.(interface{ SetNextResultHash([]byte) }); ok {
reporter.SetNextResultHash(app.nextResultHash)
}
app.nextResultHash = nil
app.cms.Commit(true)

// Reset the Check state to the latest committed.
Expand Down Expand Up @@ -1073,6 +1085,20 @@ func (app *BaseApp) FinalizeBlock(ctx context.Context, req *abci.RequestFinalize
if err != nil {
return nil, err
}
// Compute the result hash (merkle root over the block's deterministic tx results: Code, Data,
// GasWanted, GasUsed) so Commit can record it in the hash log. This is the same value Tendermint
// stores as the next block's header.LastResultsHash; logging it per block surfaces gas/result
// divergence (e.g. between executors) independently of the state AppHash. Only computed when the
// commit store records hashes, and never fails the block on a marshal error.
if _, ok := app.cms.(interface{ SetNextResultHash([]byte) }); ok {

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

even sc-hash-logger-enable = false, interface{ SetNextResultHash([]byte) } is always true?

marshaled, mErr := abci.MarshalTxResults(res.TxResults)
if mErr != nil {
logger.Error("failed to marshal tx results for result hash", "err", mErr)
app.nextResultHash = nil
} else {
app.nextResultHash = merkle.HashFromByteSlices(marshaled)
}
}
res.Events = sdk.MarkEventsToIndex(res.Events, app.IndexEvents)
return res, nil
} else {
Expand Down
4 changes: 4 additions & 0 deletions sei-cosmos/baseapp/baseapp.go
Original file line number Diff line number Diff line change
Expand Up @@ -122,6 +122,10 @@ type BaseApp struct {
processProposalCleanCtx sdk.Context // snapshot before optimistic processing
stateToCommit *state

// nextResultHash is the result hash (merkle root over the block's deterministic tx results)
// computed in FinalizeBlock and handed to the commit store in Commit, mirroring nextBlockHash.
nextResultHash []byte

// paramStore is used to query for ABCI consensus parameters from an
// application parameter store.
paramStore ParamStore
Expand Down
199 changes: 199 additions & 0 deletions sei-cosmos/storev2/rootmulti/hashlog.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,199 @@
package rootmulti

import (
"fmt"
"path/filepath"

"github.com/sei-protocol/sei-chain/sei-db/proto"
"github.com/sei-protocol/sei-chain/sei-db/state_db/sc/hashlog"
)

// App-level hash logger categories owned by rootmulti. No backend can compute these:
// - appHashType: the root application hash returned to consensus.
// - blockHashType: the Tendermint block hash, supplied by baseapp via SetNextBlockHash.
// - resultHashType: the result hash (merkle root over the block's deterministic tx results),
// supplied by baseapp via SetNextResultHash. Equals the next block's header.LastResultsHash.
// - memIAVLRootHashType: the simple-merkle root over memIAVL's per-module hashes (requires the cosmos
// hashing utilities, which sei-db cannot reach), computed here via convertCommitInfo(...).Hash().
const (
appHashType = "appHash"
blockHashType = "blockHash"
resultHashType = "resultHash"
memIAVLRootHashType = "memIAVL/root"
)

// hashReportingStore is the subset of the SC store that owns and reports its own hash categories. The
// composite commit store implements it. Type-asserting keeps these methods out of the broad
// sctypes.Committer interface. The caller registers the categories returned by HashCategories.
type hashReportingStore interface {
HashCategories() []string
RecordHashes(hashlog.HashLogger, uint64) error
MemIAVLCommitInfo() *proto.CommitInfo
}

// SetNextBlockHash stashes the Tendermint block hash for the block currently being committed. baseapp
// calls it just before Commit; rootmulti records it (under the committed version) inside Commit so every
// hash for a block shares one block number. No-op when hash logging is disabled.
func (rs *Store) SetNextBlockHash(blockHash []byte) {
if rs.hashLoggerDisabled {
return
}
rs.nextBlockHash = append([]byte(nil), blockHash...)
}

// SetNextResultHash stashes the result hash (merkle root over the block's deterministic tx results)
// for the block currently being committed. baseapp calls it just before Commit; rootmulti records it
// (under the committed version) inside Commit so every hash for a block shares one block number.
// No-op when hash logging is disabled.
func (rs *Store) SetNextResultHash(resultHash []byte) {
if rs.hashLoggerDisabled {
return
}
rs.nextResultHash = append([]byte(nil), resultHash...)
}

// hashLogDir returns the directory hash log files are written to, defaulting to a "hash.log" directory
// under the state-commit store's data directory (sibling of committer.db / receipt.db). The ".log"
// suffix mirrors the data/ naming convention (.db, .wal); the files inside keep the .hlog format.
func (rs *Store) hashLogDir() string {
if rs.hashLoggerConfig.Directory != "" {
return rs.hashLoggerConfig.Directory
}
return filepath.Join(rs.scDir, "data", "hash.log")
}

// desiredHashCategories computes the full caller-reported category set for the current backend state:
// the app-level categories plus whatever the live backends report (and memIAVL/root when memIAVL is
// present). Used both to register categories and to detect when the set has changed.
func (rs *Store) desiredHashCategories() []string {
categories := []string{appHashType, blockHashType, resultHashType}
if h, ok := rs.scStore.(hashReportingStore); ok {
categories = append(categories, h.HashCategories()...)
if h.MemIAVLCommitInfo() != nil {
categories = append(categories, memIAVLRootHashType)
}
}
return categories
}

// openHashLogger constructs the logger once. It starts with no caller columns (just the changeset
// column); syncHashCategories then registers the live categories, which the logger handles as runtime
// column changes (each new column rotates to a fresh file, but the empty initial files are dropped and
// their indexes reused, so the first file with data starts at index 0).
func (rs *Store) openHashLogger() error {
loggerVersion := rs.hashLoggerConfig.Version
if loggerVersion == "" {
loggerVersion = "unknown"
}
cfg := hashlog.DefaultHashLoggerConfig(rs.hashLogDir(), loggerVersion)
// Propagate the operator-configured retention tunables verbatim. A configured 0 must reach the logger
// (where it disables that dimension); the old `if > 0` guards swallowed it. Defaults are applied at
// config construction (config.DefaultHashLoggerConfig), so these always carry a meaningful value.
cfg.BlocksToRetain = rs.hashLoggerConfig.BlocksToRetain
cfg.TargetFileSize = rs.hashLoggerConfig.TargetFileSize
cfg.MaxDiskSize = rs.hashLoggerConfig.MaxDiskSize

hl, err := hashlog.NewHashLogger(cfg)
if err != nil {
return fmt.Errorf("failed to create hash logger: %w", err)
}
rs.hashLogger = hl
return nil
}

// syncHashCategories brings the logger's column set in line with the desired set for the current backend
// state, registering newly-present categories and unregistering departed ones. The logger rotates files
// as needed on each change; a no-op when the set is unchanged (the common case after the first block).
func (rs *Store) syncHashCategories() {
desired := rs.desiredHashCategories()
for _, category := range rs.hashCategories {
if !containsString(desired, category) {
if err := rs.hashLogger.UnregisterHashType(category); err != nil {
logger.Error("failed to unregister hash category", "category", category, "err", err)
}
}
}
for _, category := range desired {

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

A set-based approach would be clearer?

desiredSet := make(map[string]struct{}, len(desired))
for _, c := range desired { desiredSet[c] = struct{}{} }

if !containsString(rs.hashCategories, category) {
if err := rs.hashLogger.RegisterHashType(category); err != nil {
logger.Error("failed to register hash category", "category", category, "err", err)
}
}
}
rs.hashCategories = desired
}

// disableHashLogger turns hash logging off after a fatal error, closing the logger if it is open.
func (rs *Store) disableHashLogger() {
rs.hashLoggerDisabled = true
if rs.hashLogger != nil {
_ = rs.hashLogger.Close()
rs.hashLogger = nil
}
}

// recordBlockHashes reports every hash for the just-committed block at the given version. It opens the
// logger on first use and keeps its column set in sync with the live backends. On open failure it
// disables hash logging rather than disrupting commit. Must be called with rs.mtx held (from Commit).
func (rs *Store) recordBlockHashes(version int64) {
if rs.hashLoggerDisabled {
return
}

if rs.hashLogger == nil {
if err := rs.openHashLogger(); err != nil {
logger.Error("failed to open hash logger; disabling hash logging", "err", err)
rs.disableHashLogger()
return
}
}
rs.syncHashCategories()

blockNumber := uint64(version) //nolint:gosec // commit versions are non-negative

// Changeset: the aggregate of all modules' changes for this block, captured (sorted) in flush.
rs.hashLogger.ReportChangeset(blockNumber, rs.blockChangeSets)
rs.blockChangeSets = nil

// appHash: the root application hash returned to consensus.
if rs.lastCommitInfo != nil {
appHash := append([]byte(nil), rs.lastCommitInfo.CommitID().Hash...)
if err := rs.hashLogger.ReportHash(blockNumber, appHashType, appHash); err != nil {
logger.Error("failed to report app hash", "err", err)
}
}

// blockHash: supplied by baseapp before Commit. nil if it was not provided for this block.
if err := rs.hashLogger.ReportHash(blockNumber, blockHashType, rs.nextBlockHash); err != nil {
logger.Error("failed to report block hash", "err", err)
}
rs.nextBlockHash = nil

// resultHash: supplied by baseapp before Commit. nil if it was not provided for this block.
if err := rs.hashLogger.ReportHash(blockNumber, resultHashType, rs.nextResultHash); err != nil {
logger.Error("failed to report result hash", "err", err)
}
rs.nextResultHash = nil

if h, ok := rs.scStore.(hashReportingStore); ok {
if memInfo := h.MemIAVLCommitInfo(); memInfo != nil {
root := convertCommitInfo(memInfo).Hash()
if err := rs.hashLogger.ReportHash(blockNumber, memIAVLRootHashType, root); err != nil {
logger.Error("failed to report memIAVL root hash", "err", err)
}
}
if err := h.RecordHashes(rs.hashLogger, blockNumber); err != nil {
logger.Error("failed to record backend hashes", "err", err)
}
}
}

// containsString reports whether s is present in xs.
func containsString(xs []string, s string) bool {

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Nit: The containsString function can be replaced with slices.Contains

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

done

for _, x := range xs {
if x == s {
return true
}
}
return false
}
Loading
Loading