Skip to content
Open
Show file tree
Hide file tree
Changes from 10 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
6 changes: 4 additions & 2 deletions app/seidb_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -252,7 +252,9 @@ func TestParseReceiptConfigs_UsesConfiguredBackend(t *testing.T) {
assert.NoError(t, err)
assert.Equal(t, "pebbledb", receiptConfig.Backend)
assert.Equal(t, config.DefaultReceiptStoreConfig().AsyncWriteBuffer, receiptConfig.AsyncWriteBuffer)
assert.Equal(t, 0, receiptConfig.KeepRecent)
// Left at the default: no [receipt-store] key reaches KeepRecent. What a node retains is
// set later, by readReceiptStoreConfig from min-retain-blocks.
assert.Equal(t, config.DefaultReceiptKeepRecent, receiptConfig.KeepRecent)
}

func TestParseReceiptConfigs_UsesConfiguredValues(t *testing.T) {
Expand All @@ -267,7 +269,7 @@ func TestParseReceiptConfigs_UsesConfiguredValues(t *testing.T) {
assert.Equal(t, "/tmp/custom-receipt-db", receiptConfig.DBDirectory)
assert.Equal(t, "pebbledb", receiptConfig.Backend)
assert.Equal(t, 7, receiptConfig.AsyncWriteBuffer)
assert.Equal(t, 0, receiptConfig.KeepRecent)
assert.Equal(t, config.DefaultReceiptKeepRecent, receiptConfig.KeepRecent)
assert.Equal(t, 9, receiptConfig.PruneIntervalSeconds)
assert.True(t, receiptConfig.EnableReadWriteMetrics)
}
Expand Down
1 change: 1 addition & 0 deletions app/testdata/state-commit.golden
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,7 @@ FlatKVConfig.Fsync = bool(false)
FlatKVConfig.AsyncWriteBuffer = int(0)
FlatKVConfig.SnapshotInterval = uint32(10000)
FlatKVConfig.SnapshotKeepRecent = uint32(1)
FlatKVConfig.ExternalPruning = bool(false)
FlatKVConfig.EnablePebbleMetrics = bool(true)
FlatKVConfig.EnableReadWriteMetrics = bool(false)
FlatKVConfig.AccountDBConfig.DataDir = string("")
Expand Down
1 change: 1 addition & 0 deletions sei-cosmos/server/config/testdata/server_config.golden
Original file line number Diff line number Diff line change
Expand Up @@ -67,6 +67,7 @@ StateCommit.FlatKVConfig.Fsync = bool(false)
StateCommit.FlatKVConfig.AsyncWriteBuffer = int(0)
StateCommit.FlatKVConfig.SnapshotInterval = uint32(10000)
StateCommit.FlatKVConfig.SnapshotKeepRecent = uint32(1)
StateCommit.FlatKVConfig.ExternalPruning = bool(false)
StateCommit.FlatKVConfig.EnablePebbleMetrics = bool(true)
StateCommit.FlatKVConfig.EnableReadWriteMetrics = bool(false)
StateCommit.FlatKVConfig.AccountDBConfig.DataDir = string("")
Expand Down
2 changes: 1 addition & 1 deletion sei-db/config/giga_config.go
Original file line number Diff line number Diff line change
Expand Up @@ -22,7 +22,7 @@ type GigaStorageConfig struct {
FlatKVConfig *flatkvConfig.Config
SSConfig StateStoreConfig
ReceiptDBConfig ReceiptStoreConfig
BlockDBConfig *littblock.LittBlockConfig
BlockDBConfig *littblock.BlockDBConfig
PruningConfig *gc.StorageGarbageCollectorConfig
}

Expand Down
41 changes: 36 additions & 5 deletions sei-db/config/receipt_config.go
Original file line number Diff line number Diff line change
Expand Up @@ -27,6 +27,12 @@ const (
// littidx eth_getLogs (see ReceiptStoreConfig.LogFilterParallelism).
const DefaultReceiptLogFilterParallelism = 16

// DefaultReceiptKeepRecent is the default retention window in blocks for callers that
// build a ReceiptStoreConfig directly. A seid node never uses it — see
// ReceiptStoreConfig.KeepRecent for why — so it is sized for a tool or test that wants
// bounded growth, not to express what a node should retain.
const DefaultReceiptKeepRecent = 10000

// ReceiptStoreConfig defines configuration for the receipt store database.
type ReceiptStoreConfig struct {
// DBDirectory defines the directory to store the receipt store db files
Expand All @@ -47,14 +53,37 @@ type ReceiptStoreConfig struct {

// KeepRecent defines the number of versions to keep in receipt store.
// Setting it to 0 means keep everything (no pruning).
// This is NOT read from receipt-store config; it is always derived from
// the global min-retain-blocks flag at the app layer.
//
// This is NOT read from receipt-store config, and on a seid node it is not read
// from DefaultReceiptStoreConfig either: readReceiptStoreConfig overwrites it
// unconditionally from the global min-retain-blocks flag, including with 0 when
// that flag is unset. The default below is therefore only reachable by callers
// that build this config directly — tools and tests — and changing it does not
// change what any node retains.
KeepRecent int `mapstructure:"-"`

// PruneIntervalSeconds defines the interval in seconds to trigger pruning
// default to every 600 seconds
PruneIntervalSeconds int `mapstructure:"prune-interval-seconds"`

// ExternalPruning hands retention to the StorageGarbageCollector: the littidx backend
// stops running its own KeepRecent pruner and answers gc.PrunableStore.ExternalPruning
// with this value, so the collector prunes it instead.
//
// Like KeepRecent this is not read from the receipt-store config. It is set by whatever
// constructs the collector, because it is only correct when this store is actually
// registered with a running one — nothing here can check that, and the failure is silent
// and unbounded: the retention floor simply stops advancing.
//
// It is one field rather than two so the KeepRecent pruner and the collector can never
// both be enforcing retention. They would disagree, and the local pruner would win by
// deleting the rollback headroom the collector exists to preserve.
//
// Only the littidx backend honors this. The pebbledb backend is not a gc.PrunableStore,
// so the collector would not prune it and setting this would leave it with no pruner at
// all; newReceiptBackend rejects that combination rather than growing without bound.
ExternalPruning bool `mapstructure:"-"`

// EnableReadWriteMetrics emits simple estimated read/write counters for Pebble-backed receipt storage.
// defaults to false
EnableReadWriteMetrics bool `mapstructure:"enable-read-write-metrics"`
Expand All @@ -68,13 +97,15 @@ type ReceiptStoreConfig struct {
}

// DefaultReceiptStoreConfig returns the default ReceiptStoreConfig.
// KeepRecent defaults to 0 (no pruning). The app layer is responsible
// for setting KeepRecent from the global min-retain-blocks flag.
//
// KeepRecent is a bounded default rather than 0 so that a caller building this config
// directly gets a store that prunes. It is not what a node retains: the app layer
// replaces it with min-retain-blocks before the store is opened (see KeepRecent).
func DefaultReceiptStoreConfig() ReceiptStoreConfig {
return ReceiptStoreConfig{
Backend: "pebbledb",
AsyncWriteBuffer: DefaultSSAsyncBuffer,
KeepRecent: 0,
KeepRecent: DefaultReceiptKeepRecent,

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

[suggestion] I confirmed the safety claim for seid: readReceiptStoreConfig (app/receipt_store_config.go:27) overwrites KeepRecent unconditionally from min-retain-blocks, and KeepRecent is mapstructure:"-" so the app.toml template is unaffected. So no node changes behavior here.

The one caller worth flagging is DefaultGigaStorageConfig (sei-db/config/giga_config.go:58), which now hands a receipt store KeepRecent = 10000 instead of "keep everything". It has no production caller today, but it is the Giga wiring path this PR is building toward — a node constructed from it would serve eth_getTransactionReceipt for only ~10k blocks, and nothing in that path re-derives KeepRecent from min-retain-blocks. Consider setting it explicitly in DefaultGigaStorageConfig (or pinning it in giga_config_test.go) so the wiring PR can't inherit this default by accident.

PruneIntervalSeconds: DefaultSSPruneInterval,
LogFilterParallelism: DefaultReceiptLogFilterParallelism,
}
Expand Down
6 changes: 6 additions & 0 deletions sei-db/config/receipt_config_fuzz_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -206,6 +206,12 @@ func TestManifestNamesEveryField(t *testing.T) {
// sitting in a config struct that configuration cannot address is exactly the kind of
// thing a replacement manager would otherwise try to map a key onto.
"KeepRecent",
// ExternalPruning is tagged mapstructure:"-" for a sharper reason than KeepRecent: it is

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

[suggestion] This comment states as fact that ExternalPruning is tagged mapstructure:"-", but receipt_config.go:74 tags it mapstructure:"external-pruning". Per testutil/configtest/AGENTS.md these manifest exclusions are the recorded contract a replacement implementation reads, so a comment that misdescribes the tag is the specific drift the suite is meant to prevent. Fix the tag (preferred) or the comment.

// only correct when this store is registered with a running StorageGarbageCollector, which
// is a property of how the process was wired and not something an operator can assert from
// app.toml. Exposing a key for it would let a node stand its pruner down with nothing to
// replace it, and the resulting unbounded growth is silent.
"ExternalPruning",
)
}

Expand Down
3 changes: 2 additions & 1 deletion sei-db/config/testdata/receipt-store.golden
Original file line number Diff line number Diff line change
@@ -1,7 +1,8 @@
DBDirectory = string("")
Backend = string("pebbledb")
AsyncWriteBuffer = int(100)
KeepRecent = int(0)
KeepRecent = int(10000)
PruneIntervalSeconds = int(600)
ExternalPruning = bool(false)
EnableReadWriteMetrics = bool(false)
LogFilterParallelism = int(16)
1 change: 1 addition & 0 deletions sei-db/config/toml_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -58,6 +58,7 @@ func TestStateCommitConfigTemplate(t *testing.T) {
require.NotContains(t, flatKVSection, "snapshot-interval", "FlatKV snapshot-interval should not be exposed in app.toml")
require.NotContains(t, flatKVSection, "snapshot-keep-recent", "FlatKV snapshot-keep-recent should not be exposed in app.toml")
require.NotContains(t, flatKVSection, "enable-read-write-metrics", "FlatKV read/write metrics flag should not be exposed in app.toml")
require.NotContains(t, flatKVSection, "external-pruning", "FlatKV external-pruning should not be exposed in app.toml")

// sc-snapshot-writer-limit is intentionally removed from template (hardcoded to 4)
// but old configs with this field still parse fine via mapstructure
Expand Down
4 changes: 2 additions & 2 deletions sei-db/ledger_db/block/block_db_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -1402,10 +1402,10 @@ func TestMemblockPruneIntoCohortRoundsDown(t *testing.T) {

// littConfig builds a littblock config rooted at dir with a tiny retention so
// the prune watermark is the sole observable reclamation gate in tests.
func littConfig(t *testing.T, dir string) *littblock.LittBlockConfig {
func littConfig(t *testing.T, dir string) *littblock.BlockDBConfig {
cfg, err := littblock.DefaultConfig(dir)
require.NoError(t, err)
cfg.Retention = time.Nanosecond
cfg.RetentionTime = time.Nanosecond
return cfg
}

Expand Down
2 changes: 1 addition & 1 deletion sei-db/ledger_db/block/blocksim/blocksim.go
Original file line number Diff line number Diff line change
Expand Up @@ -506,7 +506,7 @@ func openBlockDB(config *BlocksimConfig) (types.BlockDB, error) {
if err != nil {
return nil, fmt.Errorf("failed to build litt block db config: %w", err)
}
littConfig.Retention = time.Duration(config.LittRetentionSeconds) * time.Second
littConfig.RetentionTime = time.Duration(config.LittRetentionSeconds) * time.Second
// Record litt_* metrics into blocksim's already-configured global OTel MeterProvider (set up in
// main before the DB is opened). MetricsServeEndpoint stays false so LittDB does not stand up its
// own registry/server; the metrics surface on blocksim's single /metrics endpoint.
Expand Down
65 changes: 49 additions & 16 deletions sei-db/ledger_db/block/littblock/litt_block_config.go
Original file line number Diff line number Diff line change
Expand Up @@ -5,46 +5,79 @@ import (
"time"

littdb "github.com/sei-protocol/sei-chain/sei-db/db_engine/litt"
"github.com/sei-protocol/sei-chain/sei-db/management/gc"
)

// LittBlockConfig configures a LittDB-backed types.BlockDB.
type LittBlockConfig struct {
// BlockDBConfig configures a LittDB-backed types.BlockDB.
type BlockDBConfig struct {
// Litt is the underlying LittDB configuration, including the data directory
// paths. The block store builds its two tables (blocks, qcs) on top of this
// DB. Required; use DefaultConfig to obtain one with sane defaults, then
// override fields as needed (e.g. Litt.Fsync, Litt.GCPeriod).
// paths. The block store builds its single table (see tableName, which holds
// blocks and QCs both) on top of this DB. Required; use DefaultConfig to obtain
// one with sane defaults, then override fields as needed (e.g. Litt.Fsync,
// Litt.GCPeriod).
Litt *littdb.Config

// Retention is the failsafe minimum age before any pruned record may be
// RetentionTime is the failsafe minimum age before any pruned record may be
// reclaimed. Reclamation requires BOTH this age to elapse AND the prune
// watermark to advance past the record, so even an over-eager watermark
// cannot delete data younger than Retention. Must be positive.
Retention time.Duration
// cannot delete data younger than RetentionTime. Must be positive.
//
// It is an age floor, not a retention policy: how much history this store
// keeps is RetentionWindow below. Raising this only delays reclaiming what
// the watermark has already released, which costs disk and buys nothing the
// window does not already express.
RetentionTime time.Duration

// RetentionWindow is how much history this store keeps beyond the shared rollback
// window of the StorageGarbageCollector that manages it, in blocks. It is what
// gc.PrunableStore.GetRetentionWindow answers:
//
// > 0 → that many blocks of history beyond the rollback window
// 0 → keep history to serve rollback window only
// -1 → never prune this store (gc.InfiniteRetentionWindow)
//
// Zero does NOT mean "keep everything" here, unlike the KeepRecent fields on
// StateStoreConfig and ReceiptStoreConfig, where 0 disables pruning. It is the most
// aggressive setting this field has; "keep everything" is -1. Assigning a KeepRecent
// value to this field inverts the retention it asks for.
//
// This is an input to a minimum shared across every managed store, not a policy applied
// to this store alone: a deep window here also holds back receiptDB and the SC/SS
// snapshots. Must be >= gc.InfiniteRetentionWindow.
//
// Independent of Retention, which is a wall-clock TTL failsafe underneath the watermark.
// Both must permit reclamation before any record is dropped.
RetentionWindow int64

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.

Might be useful to call out the following invariants:

Storage garbage collection guarantees the following invariants:

1. Garbage collection will not delete any data that is necessary to roll back to any block 
   between LatestBlock and (LatestBlock - RollbackWindow), inclusive.
2. Garbage collection will not delete block DB data that is before 
   (LatestBlock - RollbackWindow - RetentionWindow). This ensures that even if the 
   system rolls back to block (LatestBlock - RollbackWindow), it is still possible to read any
   block from the last RetentionWindow blocks.
3. Garbage collection will eventually delete block data older than 
   (LatestBlock - RollbackWindow - RetentionWindow).

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.

I suggest we paste this block of invariants at each RetentionWindow or RollbackWindow config (with wording adjusted a little, the above is specific to block storage).

}

// DefaultConfig returns a LittBlockConfig preloaded with all defaults, rooted at
// DefaultConfig returns a BlockDBConfig preloaded with all defaults, rooted at
// dir. Override fields as needed, then pass it to NewBlockDB (which validates).
func DefaultConfig(dir string) (*LittBlockConfig, error) {
func DefaultConfig(dir string) (*BlockDBConfig, error) {
littConfig, err := littdb.DefaultConfig(dir)
if err != nil {
return nil, fmt.Errorf("failed to build litt config: %w", err)
}
return &LittBlockConfig{
Litt: littConfig,
Retention: 24 * time.Hour,
return &BlockDBConfig{
Litt: littConfig,
RetentionTime: time.Hour,

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

[suggestion] This silently drops the TTL failsafe from 24h to 1h (the setup_test.go assertion is updated to match, so it is deliberate), but the PR description doesn't mention it among the BlockDB changes.

RetentionTime is documented right above as the guard that "even an over-eager watermark cannot delete data younger than" — shrinking it 24× shrinks exactly that safety margin, and it applies today to autobahn/devnet block DBs that have no collector at all. Please call it out in the description, or keep 24h until the collector actually owns the watermark.

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

[nit] Worth calling out that this is a live default change, not just a rename: AutobahnBlockDBConfig.Retention is optional, so any autobahn node without an explicit retention in its config file goes from a 24h TTL failsafe to 1h. Benign as far as I can tell (the watermark is the visible-retention gate and reads below it are already refused, so this only makes reclamation of already-released data 24× more prompt), but the PR description presents the TTL rework under the ReceiptDB heading and this one reads as incidental to the rename.

Separately: RetentionWindow: 10000 on the next line has no path from AutobahnBlockDBConfig — the only two overrides are retention and gc_period — so whenever BlockDB is registered with a collector, autobahn nodes will be stuck on the hardcoded window until that config grows a key for it.

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

[nit] The BlockDB age failsafe drops 24h → 1h here (the receipt side goes the other way, from KeepRecent × 2s up to a flat 1h). The PR body states the new default, so this is disclosed — just confirming intent, since this value is the backstop against a watermark bug and it shrinks 24×. Autobahn devnets pick it up via DefaultConfig; AutobahnBlockDBConfig.Retention still overrides it, and the assertion in sei-tendermint/node/setup_test.go was updated to match.

RetentionWindow: 10000,
Comment thread
yzang2019 marked this conversation as resolved.
Outdated

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

[suggestion] This default is a fleet-wide policy in a per-store field. As the RetentionWindow doc two dozen lines up says, the value is an input to a shared minimum — so 10000 here pushes pruneHeight 10k blocks deeper for ReceiptDB, the state WAL and the SC snapshots too, not just for BlockDB. Two things worth reconsidering: (a) whether the default should be 0 and the extra depth expressed once as RollbackWindow in StorageGarbageCollectorConfig, and (b) that AutobahnBlockDBConfig.LittBlockConfig exposes Retention but not RetentionWindow, so once wired there is no way to tune this from tendermint config.

}, nil
}

// Validate performs a sanity check on the configuration.
func (c *LittBlockConfig) Validate() error {
func (c *BlockDBConfig) Validate() error {
if c == nil {
return fmt.Errorf("config is required")
}
if c.Litt == nil {
return fmt.Errorf("config.Litt is required")
}
if c.Retention <= 0 {
return fmt.Errorf("config.Retention must be positive (got %s)", c.Retention)
if c.RetentionTime <= 0 {
return fmt.Errorf("config.RetentionTime must be positive (got %s)", c.RetentionTime)
}
if c.RetentionWindow < gc.InfiniteRetentionWindow {
return fmt.Errorf("config.RetentionWindow must be >= %d (got %d)",
gc.InfiniteRetentionWindow, c.RetentionWindow)
}
return nil
}
74 changes: 61 additions & 13 deletions sei-db/ledger_db/block/littblock/litt_block_db.go
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,8 @@ package littblock

import (
"fmt"
"os"
"path/filepath"
"sync"
"sync/atomic"

Expand All @@ -12,18 +14,27 @@ import (
"github.com/sei-protocol/sei-chain/sei-tendermint/libs/utils"
)

// ledgerTableName is the single table holding both blocks and QCs. They share
// one table so a crash leaves a contiguous write-order prefix spanning both
// record kinds (see NewBlockDB), which is what guarantees a persisted block is
// always covered by a persisted QC.
const ledgerTableName = "ledger"
// tableName is the single table holding blocks and QCs both, despite the name. They share one
// table so a crash leaves a contiguous write-order prefix spanning both record kinds (see
// NewBlockDB), which is what guarantees a persisted block is always covered by a persisted QC.
//
// This value is persisted layout, not just an identifier: littdb puts a table's data at
// <root>/<tableName>/segments, so changing it makes NewBlockDB open a fresh empty table while the
// old data sits untouched under the previous name — neither served nor reclaimed. refuseLegacyTable
// turns that into a startup error rather than a store that looks healthy and empty.
const tableName = "blocks"

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

[suggestion] The comment you added here states the hazard precisely — a rename makes NewBlockDB open a fresh empty table while the old data sits under <root>/ledger/ "neither served nor reclaimed" — and then the rename is performed anyway, relying on "BlockDB is not deployed on any network yet" from the PR description. That holds for mainnet, but any dev/CI/devnet home directory carrying a ledger/ table silently comes up empty rather than failing, which is the worst shape for the one class of environment where it can happen.

Since the check is cheap and the comment already argues for it: os.Stat(<root>/ledger) at open and refuse to start (or log loudly) if it exists. That turns a silent empty store into a one-line operator action, and can be deleted once no such directories remain.

Comment thread
yzang2019 marked this conversation as resolved.

// legacyTableName is what tableName was called before the rename. Nothing opens it; it exists only
// so refuseLegacyTable can recognize a directory written before the rename.
const legacyTableName = "ledger"

var _ types.BlockDB = (*blockDB)(nil)

// blockDB is a durable types.BlockDB backed by LittDB
type blockDB struct {
db littdb.DB
table littdb.Table
db littdb.DB
table littdb.Table
config *BlockDBConfig

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

[suggestion] This field is assigned once (s.config = config, line 126) and never read anywhere in the package — grep '\.config' across littblock/*.go (non-test) returns only the assignment. The doc comments in litt_block_gc.go refer to config.RetentionTime, but the value that actually gates reclamation is the TTL already handed to littdb.DefaultTableConfig. unused isn't enabled in .golangci.yml, so nothing catches it. Drop the field and the assignment, or read it where the docs imply it is read.

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

[suggestion] config is stored (s.config = config at line 126) but never read anywhere in the package — grep -rn '\.config' sei-db/ledger_db/block/littblock/ returns only the assignment. config.RetentionTime is consumed at construction time via tableConfig.TTL, so the retained pointer does no work.

Struct fields aren't caught by the unused linter, so this will sit here indefinitely. Either drop the field, or if it's groundwork for a follow-up, say so in a comment. Note the doc comments in litt_block_gc.go refer to "config.RetentionTime" as though this field were the source (PruneHistory, ExternalPruning), which reads as if it's live.


// watermark is a retention floor, always a QC boundary (a GlobalRange().First):
// PruneBefore rounds a requested prune point down to the start of the cohort
Expand Down Expand Up @@ -74,13 +85,18 @@ type blockDB struct {
}

// NewBlockDB opens (or creates) a LittDB-backed types.BlockDB from config. The
// underlying LittDB is built from config.Litt, and the two tables apply
// config.Retention as a TTL failsafe (pruning never reclaims data younger than
// that even once the watermark has advanced past it).
func NewBlockDB(config *LittBlockConfig) (types.BlockDB, error) {
// underlying LittDB is built from config.Litt, and the table applies
// config.RetentionTime as a TTL failsafe (pruning never reclaims data younger
// than that even once the watermark has advanced past it).
func NewBlockDB(config *BlockDBConfig) (types.BlockDB, error) {
if err := config.Validate(); err != nil {
return nil, fmt.Errorf("invalid block db config: %w", err)
}
// Before littbuilder.NewDB, so a refused open leaves the directory exactly as it found it
// rather than adding an empty table beside the one it is complaining about.
if err := refuseLegacyTable(config.Litt.Paths); err != nil {
return nil, err
}
db, err := littbuilder.NewDB(config.Litt)
if err != nil {
return nil, fmt.Errorf("failed to open litt db: %w", err)
Expand All @@ -96,8 +112,8 @@ func NewBlockDB(config *LittBlockConfig) (types.BlockDB, error) {
// guarantees a persisted block is always covered by a persisted QC. It also
// backs the write-order cursors and contiguous-QC recovery. ShardingFactor
// > 1, or splitting blocks and QCs across two tables, would void this.
tableConfig := littdb.DefaultTableConfig(ledgerTableName)
tableConfig.TTL = config.Retention
tableConfig := littdb.DefaultTableConfig(tableName)
tableConfig.TTL = config.RetentionTime
tableConfig.GCFilter = s.gcFilter
tableConfig.ShardingFactor = 1 // DO NOT CHANGE!!
table, err := db.BuildTable(tableConfig)
Expand All @@ -107,6 +123,7 @@ func NewBlockDB(config *LittBlockConfig) (types.BlockDB, error) {
}

s.table = table
s.config = config

if err := s.recoverCursors(); err != nil {
_ = db.Close()
Expand All @@ -119,6 +136,37 @@ func NewBlockDB(config *LittBlockConfig) (types.BlockDB, error) {
return s, nil
}

// refuseLegacyTable fails the open when any root path holds a table directory under
// legacyTableName. Such a directory is blocks and QCs that this process cannot reach: littdb
// resolves a table to <root>/<tableName>/segments, so the data is neither served nor reclaimed, and
// the store would otherwise present itself as healthy and empty. An empty store is indistinguishable
// from a correct one until something asks for history that is no longer there, which makes it the
// worst shape this failure could take.
//
// No deployment can reach this — nothing has ever run littblock with the old name persisted — so it
// exists for dev, CI, and devnet homes written before the rename. The operator action is to delete
// the directory (or move it aside); this check can be deleted once no such directory remains.
//
// A root that cannot be stat'd is refused for the same reason it is refused when the directory is
// present: an unreadable root cannot rule out data hiding under the old name.
func refuseLegacyTable(paths []string) error {
for _, root := range paths {
legacy := filepath.Join(root, legacyTableName)
switch _, err := os.Stat(legacy); {
case err == nil:
return fmt.Errorf(
"block db: found a pre-rename %q table at %s; the table is now named %q, so those "+
"blocks and QCs would be neither served nor reclaimed. Delete or move the "+
"directory aside to start from an empty store",
legacyTableName, legacy, tableName)
case !os.IsNotExist(err):
return fmt.Errorf("block db: check for a pre-rename %q table at %s: %w",
legacyTableName, legacy, err)
}
}
return nil
}

// recoverCursors reloads the write-order cursors (lastBlockNumber, lastQCNext,
// and their presence flags) from on-disk state. Without this, a reopened DB
// would treat itself as empty and let WriteBlock/WriteQC silently accept
Expand Down
Loading
Loading