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
56 changes: 29 additions & 27 deletions cmd/seid/cmd/legacy_config_fuzz_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -176,24 +176,20 @@ var tmKeys = []tmKey{
},
}

// FuzzHashVaultDisabledUnsafeResolution pins the root-scope kill switch for the
// app-hash equivocation guard.
// FuzzHashVaultHaltOnMismatchResolution pins the root-scope switch that selects whether a hash vault
// mismatch halts the node.
//
// Two things make it worth its own target. It is a bool whose safe value is the
// default, so an absent key must resolve false — setting it true removes
// equivocation protection with only a log banner. And it lives at TOML root scope,
// before any [section] header: nested under a section it parses as a different key
// and is silently ignored, which reads as "I disabled the guard" while the guard
// stays on, and would read the other way round if the scope were ever mishandled.
// The document is built from the fuzzer's choices rather than taken as free text,
// so the expected outcome follows from construction instead of being a second
// input the fuzzer can mutate out of agreement with the first.
func FuzzHashVaultDisabledUnsafeResolution(f *testing.F) {
// An absent key resolves false, so a mismatch replaces the recorded state hash with only an error log.
// The key lives at TOML root scope, before any [section] header: nested under a section it parses as a
// different key and is silently ignored. The document is built from the fuzzer's choices rather than
// taken as free text, so the expected outcome follows from construction instead of being a second input
// the fuzzer can mutate out of agreement with the first.
func FuzzHashVaultHaltOnMismatchResolution(f *testing.F) {
f.Add(false, false, false)
f.Add(true, true, false) // root scope, true: the guard is off
f.Add(true, false, false) // root scope, false
f.Add(true, true, true) // nested under a section: silently ignored
f.Add(true, false, true)
f.Add(true, false, false) // root scope, false: a mismatch only logs
f.Add(true, true, false) // root scope, true
f.Add(true, false, true) // nested under a section: silently ignored
f.Add(true, true, true)

f.Fuzz(func(t *testing.T, present, value, underSection bool) {
configtest.Isolate(t)
Expand All @@ -204,37 +200,43 @@ func FuzzHashVaultDisabledUnsafeResolution(f *testing.F) {
if underSection {
doc.WriteString("[p2p]\n")
}
fmt.Fprintf(&doc, "hash-vault-disabled-unsafe = %t\n", value)
fmt.Fprintf(&doc, "hash-vault-halt-on-mismatch = %t\n", value)
}
if doc.Len() > 0 {
home.WriteConfigTOML(t, []byte(doc.String()))
}

// Root scope is the only placement that resolves. Nested under a section the
// key becomes p2p.hash-vault-disabled-unsafe, which nothing reads.
wantDisabled := present && value && !underSection
// key becomes p2p.hash-vault-halt-on-mismatch, which nothing reads.
wantHalt := false
if present && !underSection {
wantHalt = value
}

got := applyLegacy(t, home, nil)
if got.err != nil {
t.Fatalf("Apply must succeed on a well-formed config.toml, got %v", got.err)
}
if got.ctx.Config.HashVaultDisabledUnsafe != wantDisabled {
t.Fatalf("hash-vault-disabled-unsafe resolved to %v, want %v, from:\n%s",
got.ctx.Config.HashVaultDisabledUnsafe, wantDisabled, doc.String())
if got.ctx.Config.HashVaultHaltOnMismatch != wantHalt {
t.Fatalf("hash-vault-halt-on-mismatch resolved to %v, want %v, from:\n%s",
got.ctx.Config.HashVaultHaltOnMismatch, wantHalt, doc.String())
}
})
}

// TestHashVaultDisabledUnsafeDefaultsToEnabledGuard states the default on its own,
// so the guard's safe value is pinned even if every seed above were removed.
func TestHashVaultDisabledUnsafeDefaultsToEnabledGuard(t *testing.T) {
// TestHashVaultDefaults pins the hash vault defaults an empty home resolves to.
func TestHashVaultDefaults(t *testing.T) {
configtest.Isolate(t)
got := applyLegacy(t, configtest.NewHome(t), nil)
if got.err != nil {
t.Fatalf("Apply: %v", got.err)
}
if got.ctx.Config.HashVaultDisabledUnsafe {
t.Fatal("an empty home must leave the app-hash equivocation guard enabled")
if got.ctx.Config.HashVaultHaltOnMismatch {
t.Fatal("an empty home must leave a hash vault mismatch replacing the recorded hash")
}
if got.ctx.Config.HashVaultEmptyRollbackBlocks != 1000 {
t.Fatalf("an empty home must rewind 1000 blocks over an empty hash vault, got %d",
got.ctx.Config.HashVaultEmptyRollbackBlocks)
}
}

Expand Down
12 changes: 7 additions & 5 deletions config/tendermintbase/tendermintbase.go
Original file line number Diff line number Diff line change
Expand Up @@ -57,8 +57,9 @@ var removedFromTheNode = []string{"proxy-app", "abci", "filter-peers"}
type nodeRootSchema struct {
tmcfg.BaseConfig `mapstructure:",squash"`

AutobahnConfigFile string `mapstructure:"autobahn-config-file"`
HashVaultDisabledUnsafe bool `mapstructure:"hash-vault-disabled-unsafe"`
AutobahnConfigFile string `mapstructure:"autobahn-config-file"`
HashVaultHaltOnMismatch bool `mapstructure:"hash-vault-halt-on-mismatch"`
HashVaultEmptyRollbackBlocks uint64 `mapstructure:"hash-vault-empty-rollback-blocks"`
}

// removedSettings are the consensus paths this section does not declare. Each names a field the node's
Expand Down Expand Up @@ -289,9 +290,10 @@ func privValidatorDefaults(mode registry.Mode) any { return *forMode(mode).PrivV
func rootDefaults(mode registry.Mode) any {
live := forMode(mode)
return nodeRootSchema{
BaseConfig: live.BaseConfig,
AutobahnConfigFile: live.AutobahnConfigFile,
HashVaultDisabledUnsafe: live.HashVaultDisabledUnsafe,
BaseConfig: live.BaseConfig,
AutobahnConfigFile: live.AutobahnConfigFile,
HashVaultHaltOnMismatch: live.HashVaultHaltOnMismatch,
HashVaultEmptyRollbackBlocks: live.HashVaultEmptyRollbackBlocks,
}
}

Expand Down
6 changes: 6 additions & 0 deletions giga/evmonly/giga_store_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -42,6 +42,12 @@ func (s *recordingGigaStore) OpenViewAt(int64) (gigatypes.StateView, bool) {
return nil, false
}

func (s *recordingGigaStore) GetBlockHeight() uint64 { return 0 }

func (s *recordingGigaStore) GetBlockHash(uint64) ([32]byte, gigatypes.BlockHashStatus, error) {
return [32]byte{}, gigatypes.BlockHashStatusNotReady, nil
}

func (s *recordingGigaStore) Close() error { return nil }

type memoryGigaSnapshot struct {
Expand Down
15 changes: 15 additions & 0 deletions giga/evmonly/memory_store.go
Original file line number Diff line number Diff line change
Expand Up @@ -372,6 +372,21 @@ func (s *MemoryStore) RegisterHashListener(_ gigatypes.HashListener) (lthash.Blo
return lthash.BlockHash{}, fmt.Errorf("evmonly: an in-memory store computes no block hashes")
}

// GetBlockHeight returns the last block committed, or 0 when none has been.
func (s *MemoryStore) GetBlockHeight() uint64 {
s.mu.RLock()
defer s.mu.RUnlock()
if !s.hasCurrentHeight {
return 0
}
return uint64(s.currentHeight) //nolint:gosec // CommitStateChanges refuses a negative block number
}

// GetBlockHash reports every block's hash as not ready, since this store computes no block hashes.
func (s *MemoryStore) GetBlockHash(uint64) ([32]byte, gigatypes.BlockHashStatus, error) {
return [32]byte{}, gigatypes.BlockHashStatusNotReady, nil
}

// Close releases nothing. This store holds no handle outside its own maps, which go with it.
func (s *MemoryStore) Close() error { return nil }

Expand Down
4 changes: 0 additions & 4 deletions giga/metrics/autobahn_loop.go
Original file line number Diff line number Diff line change
Expand Up @@ -23,8 +23,6 @@ const (
// PhaseStorage is time spent persisting receipts, state, and the app commit.
PhaseStorage = "storage"

// StoragePhaseVaultCommit is the app hash's durable write to the hash vault.
StoragePhaseVaultCommit = "vault_commit"
// StoragePhaseAppCommit is the app's Commit call.
StoragePhaseAppCommit = "app_commit"
// StoragePhasePushAppHash is publishing the app hash to the data layer.
Expand All @@ -34,8 +32,6 @@ const (
StoragePhaseBookkeeping = "bookkeeping"
// StoragePhasePruneData is pruning the data layer below the app's retain height.
StoragePhasePruneData = "prune_data"
// StoragePhasePruneVault is pruning the hash vault to the same boundary.
StoragePhasePruneVault = "prune_vault"
)

var (
Expand Down
4 changes: 2 additions & 2 deletions sei-db/bench/cryptosim/block_hashes.go
Original file line number Diff line number Diff line change
Expand Up @@ -38,7 +38,7 @@ type blockHashWaiter struct {
committed int

// The block the next hash taken must describe, or 0 until the first one has been taken.
nextExpected int64
nextExpected uint64

// How long to wait for one hash before reporting a database that has stopped hashing.
waitTimeout time.Duration
Expand All @@ -62,7 +62,7 @@ func newBlockHashWaiter(lagBlocks int, metrics *CryptosimMetrics) *blockHashWait
// Blocking here is the backpressure: it stops a database finalizing blocks faster than the benchmark
// accepts their hashes. The context is the release, cancelled when the database shuts down, since a
// send with no taker left would otherwise never return.
func (w *blockHashWaiter) listen(ctx context.Context, _ int64, hash *lthash.BlockHash) error {
func (w *blockHashWaiter) listen(ctx context.Context, _ uint64, hash *lthash.BlockHash) error {
select {
case w.hashes <- hash:
return nil
Expand Down
6 changes: 3 additions & 3 deletions sei-db/bench/cryptosim/block_hashes_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -11,7 +11,7 @@ import (
)

// publish hands the waiter the hash of one block, as the database's dispatch would.
func publish(t *testing.T, w *blockHashWaiter, blockNumber int64) {
func publish(t *testing.T, w *blockHashWaiter, blockNumber uint64) {
t.Helper()
require.NoError(t, w.listen(t.Context(), blockNumber, &lthash.BlockHash{BlockNumber: blockNumber}))
}
Expand All @@ -31,7 +31,7 @@ func TestTheFirstBlocksRunAheadWithoutTakingAHash(t *testing.T) {
// would let the benchmark drift arbitrarily far ahead of hashing.
func TestOneHashIsTakenPerBlockAfterTheWindow(t *testing.T) {
waiter := newBlockHashWaiter(3, nil)
for block := int64(1); block <= 4; block++ {
for block := uint64(1); block <= 4; block++ {
publish(t, waiter, block)
}

Expand All @@ -40,7 +40,7 @@ func TestOneHashIsTakenPerBlockAfterTheWindow(t *testing.T) {
require.NoError(t, waiter.awaitBlock())
}
require.Len(t, waiter.hashes, 3, "exactly one hash may be taken per block committed")
require.Equal(t, int64(2), waiter.nextExpected)
require.Equal(t, uint64(2), waiter.nextExpected)
}

// The wait is the point: a database that cannot hash as fast as the benchmark commits has to slow the
Expand Down
6 changes: 5 additions & 1 deletion sei-db/bench/cryptosim/cryptosim.go
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,7 @@ import (
"github.com/sei-protocol/sei-chain/sei-db/common/utils"
"github.com/sei-protocol/sei-chain/sei-db/controller"
"github.com/sei-protocol/sei-chain/sei-db/state_db/giga"
"github.com/sei-protocol/sei-chain/sei-db/state_db/sc/hashvault"
)

const (
Expand Down Expand Up @@ -146,7 +147,10 @@ func NewCryptoSim(
// giga.NewStateDB is the node's own entry point, and the only one that leaves the state WAL
// outside the live state DB: it opens the WAL itself and writes each block to it ahead of the
// commit. A live state DB opened directly would own its WAL and write it inline instead.
db, err := giga.NewStateDB(ctx, config.FlatKVConfig, config.StateStoreConfig, config.CheckpointConfig)
hashVaultConfig := hashvault.DefaultHashVaultConfig()
hashVaultConfig.DataDir = filepath.Join(config.DataDir, "hashvault")
db, err := giga.NewStateDB(
ctx, config.FlatKVConfig, config.StateStoreConfig, config.CheckpointConfig, hashVaultConfig, 0)
if err != nil {
cancel()
return nil, fmt.Errorf("failed to open the state DB: %w", err)
Expand Down
4 changes: 2 additions & 2 deletions sei-db/bench/gigasim/block_hashes.go
Original file line number Diff line number Diff line change
Expand Up @@ -34,7 +34,7 @@ type blockHashWaiter struct {
committed int

// The block the next hash taken must describe, or 0 until the first one has been taken.
nextExpected int64
nextExpected uint64

// How long to wait for one hash before reporting a state DB that has stopped hashing.
waitTimeout time.Duration
Expand All @@ -55,7 +55,7 @@ func newBlockHashWaiter(lagBlocks int, metrics *GigasimMetrics) *blockHashWaiter
// listen takes one block's hash from the state DB, blocking while the benchmark is further ahead than
// its window allows. That block is the backpressure on hashing; the context releases it when the state
// DB shuts down, since a send with no taker left would never return.
func (w *blockHashWaiter) listen(ctx context.Context, _ int64, hash *lthash.BlockHash) error {
func (w *blockHashWaiter) listen(ctx context.Context, _ uint64, hash *lthash.BlockHash) error {
select {
case w.hashes <- hash:
return nil
Expand Down
33 changes: 20 additions & 13 deletions sei-db/bootstrap/recovery.go
Original file line number Diff line number Diff line change
Expand Up @@ -56,7 +56,7 @@ func (m *GigaStorageManager) OpenDBWithRecovery(ctx context.Context) error {
// State goes first because it is the rollback that refuses: a target its snapshots and WAL cannot span
// leaves the node down for an operator to retry at a higher one, and receipts cut to the lower target
// would no longer be there to reach.
func (m *GigaStorageManager) recoverStores(ctx context.Context, target int64) error {
func (m *GigaStorageManager) recoverStores(ctx context.Context, target uint64) error {
if target == 0 {
logger.Info("No height to converge on, opening the state DB where its files sit")
return m.openStateDB(ctx)
Expand Down Expand Up @@ -104,7 +104,7 @@ func (m *GigaStorageManager) openReceiptStore() error {
//
// The state and receipt heads are read from their directories, which takes the locks their open stores
// hold, so this must run before either of those stores opens.
func (m *GigaStorageManager) findTargetRecoveryHeight() (int64, error) {
func (m *GigaStorageManager) findTargetRecoveryHeight() (uint64, error) {
logger.Info("Reading a store head", "store", "block store")
blockHeight, err := m.blockStore.GetLatestBlock()
if err != nil {
Expand All @@ -129,7 +129,7 @@ func (m *GigaStorageManager) findTargetRecoveryHeight() (int64, error) {
"state_wal", stateHeight,
"receipt_store", receiptHeight,
"target", target)
return int64(target), nil //nolint:gosec // heights fit within int64
return target, nil
}

// ErrReceiptStoreDisabledWithHistory is returned when the receipt store is disabled while its
Expand Down Expand Up @@ -188,24 +188,32 @@ func recoveryTarget(blockHeight, stateHeight, receiptHeight uint64) uint64 {
return target
}

// openStateDB opens the state commit store, the EVM state store (when enabled) and the state WAL,
// leaving them on the height the WAL holds.
// openStateDB opens the state commit store, the EVM state store (when enabled), the state WAL and the
// hash vault, leaving the stores on the height the WAL holds.
func (m *GigaStorageManager) openStateDB(ctx context.Context) error {
stateDB, err := giga.NewStateDB(ctx, m.cfg.FlatKVConfig, m.cfg.SSConfig, m.cfg.CheckpointConfig)
stateDB, err := giga.NewStateDB(
ctx, m.cfg.FlatKVConfig, m.cfg.SSConfig, m.cfg.CheckpointConfig, m.cfg.HashVaultConfig, 0)
if err != nil {
return err
}
m.stateDB = stateDB
return nil
}

// openStateDBAt opens the same three stores on target, rolling them back to it first.
// openStateDBAt opens the same stores on target, rolling them back to it first. The hash vault is not
// rolled back.
//
// The rollback is part of the open because cutting the state WAL's tail needs the WAL closed, so an
// already-open state DB would have to close and reopen it.
func (m *GigaStorageManager) openStateDBAt(ctx context.Context, target int64) error {
stateDB, err := giga.NewStateDBWithRollback(
ctx, m.cfg.FlatKVConfig, m.cfg.SSConfig, m.cfg.CheckpointConfig, target)
func (m *GigaStorageManager) openStateDBAt(ctx context.Context, target uint64) error {
if target == 0 {
// The state DB reads a target of 0 as no rollback at all, so without this a caller asking for one
// would get a plain open instead.
return fmt.Errorf("rollback target %d is invalid: version 0 means no state, so there is "+
"nothing to roll back to", target)
}
stateDB, err := giga.NewStateDB(
ctx, m.cfg.FlatKVConfig, m.cfg.SSConfig, m.cfg.CheckpointConfig, m.cfg.HashVaultConfig, target)
if err != nil {
return err
}
Expand All @@ -217,12 +225,11 @@ func (m *GigaStorageManager) openStateDBAt(ctx context.Context, target int64) er
// open store. A store already at or below target is left alone.
//
// It takes the locks an open receipt store holds, so it must run before openReceiptStore.
func (m *GigaStorageManager) recoverReceipt(target int64) error {
func (m *GigaStorageManager) recoverReceipt(target uint64) error {
if !m.cfg.ReceiptDBConfig.Enable {
return nil
}
//nolint:gosec // recoverStores guards target > 0
if err := receipt.PruneAfter(m.cfg.ReceiptDBConfig, uint64(target)); err != nil {
if err := receipt.PruneAfter(m.cfg.ReceiptDBConfig, target); err != nil {
return fmt.Errorf("roll the receipt store back to %d: %w", target, err)
}
return nil
Expand Down
6 changes: 3 additions & 3 deletions sei-db/bootstrap/recovery_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -114,15 +114,15 @@ func commitBlocksWithSSSnapshots(t *testing.T, manager *GigaStorageManager, thro

// reconverge re-runs what a restart does: it closes every store recovery touches, recovers them onto
// target — which is what opens the state DB again — and reopens the receipt store on the far side.
func reconverge(t *testing.T, manager *GigaStorageManager, target int64) {
func reconverge(t *testing.T, manager *GigaStorageManager, target uint64) {
t.Helper()
require.NoError(t, reconvergeErr(t, manager, target))
require.NoError(t, manager.openReceiptStore())
}

// reconvergeErr is reconverge up to the point recovery can fail, for a test that expects it to. The
// receipt store is left closed, since a failed recovery leaves the manager with no state DB.
func reconvergeErr(t *testing.T, manager *GigaStorageManager, target int64) error {
func reconvergeErr(t *testing.T, manager *GigaStorageManager, target uint64) error {
t.Helper()
// Closing first is what a restart does, and it is also required: recovery takes file locks the
// open stores hold — the state WAL's directory lock for the reads and the tail cut that precede
Expand Down Expand Up @@ -252,7 +252,7 @@ func TestFindTargetRecoveryHeightIsZeroWithoutABlockLedger(t *testing.T) {

got, err := manager.findTargetRecoveryHeight()
require.NoError(t, err)
require.Equal(t, int64(0), got)
require.Equal(t, uint64(0), got)
}

// Recovering to a target below the WAL head drops every block above it, so the write head resumes at
Expand Down
2 changes: 1 addition & 1 deletion sei-db/bootstrap/storage_manager.go
Original file line number Diff line number Diff line change
Expand Up @@ -78,7 +78,7 @@ func (m *GigaStorageManager) startGarbageCollector(ctx context.Context, pruningC
// prunableStores returns the opened stores that can join the shared prune cycle. The state stores come
// from the StateDB that owns them.
func (m *GigaStorageManager) prunableStores() []controller.PrunableStore {
stores := make([]controller.PrunableStore, 0, 5)
stores := make([]controller.PrunableStore, 0, 6)
if m.stateDB != nil {
stores = append(stores, m.stateDB.PrunableStores()...)
}
Expand Down
Loading
Loading