Skip to content
Open
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
7 changes: 7 additions & 0 deletions sei-db/common/utils/path.go
Original file line number Diff line number Diff line change
Expand Up @@ -74,6 +74,13 @@ func GetReceiptStorePath(homePath string, backend string) string {
return filepath.Join(homePath, "data", "ledger", "receipt", backend)
}

// GetBlockStorePath returns the path for the ledger block store, which sits alongside the
// receipt store under data/ledger. There is no legacy fallback: the block store has only
// ever lived at this path.

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 block store has only ever lived at this path" doesn't match the tree: the only production block store today is autobahn's littblock, opened at filepath.Join(dir, "blockdb") under persistent_state_dir (sei-tendermint/node/setup.go:399), not data/ledger/block. Nothing breaks now since GetBlockStorePath has no callers outside DefaultGigaStorageConfig, but the comment will mislead whoever wires this up, and the migration story from the existing blockdb directory needs an answer before then.

func GetBlockStorePath(homePath string) string {
return filepath.Join(homePath, "data", "ledger", "block")
}

func GetChangelogPath(dbPath string) string {
return filepath.Join(dbPath, "changelog")
}
Expand Down
8 changes: 8 additions & 0 deletions sei-db/common/utils/path_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -135,6 +135,14 @@ func TestGetReceiptStorePath_LegacyExists(t *testing.T) {
assert.Equal(t, legacy, got)
}

// --- GetBlockStorePath ---

func TestGetBlockStorePath_NewNode(t *testing.T) {
home := t.TempDir()
got := GetBlockStorePath(home)
assert.Equal(t, filepath.Join(home, "data", "ledger", "block"), got)
}

// --- GetChangelogPath (unchanged, but verify) ---

func TestGetChangelogPath(t *testing.T) {
Expand Down
67 changes: 67 additions & 0 deletions sei-db/config/giga_config.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,67 @@
package config

import (
"fmt"

"github.com/sei-protocol/sei-chain/sei-db/common/utils"
"github.com/sei-protocol/sei-chain/sei-db/ledger_db/block/littblock"
"github.com/sei-protocol/sei-chain/sei-db/management/gc"

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] This makes sei-db/config (a broadly imported, leaf-ish config package) depend on sei-db/management/gc, a runtime package that starts goroutines. It works today because gc imports only stdlib + seilog, but it inverts the usual direction and becomes an import cycle the moment gc needs anything from config.

The stated motivation — one source of truth for RollbackWindow — could also be met by having the wiring layer (whoever constructs both the stores and the collector) call gc.DefaultStorageGarbageCollectorConfig() and hold PruningConfig there, keeping GigaStorageConfig to store configs only. Not worth blocking on, but worth a deliberate decision now rather than after more code depends on it.

flatkvConfig "github.com/sei-protocol/sei-chain/sei-db/state_db/sc/flatkv/config"
)

// GigaStorageConfig is an in-process composition of store configs for Giga storage.
// It is intentionally not read from app.toml (no mapstructure tags, no TOML section):
// callers build it via DefaultGigaStorageConfig. Nested knobs live on the store and
// collector configs they already own (StateStoreConfig, ReceiptStoreConfig,
// gc.StorageGarbageCollectorConfig) rather than being redeclared here — in particular
// RollbackWindow has a single source of truth in
// gc.DefaultStorageGarbageCollectorConfig; per-store extras live on each
// PrunableStore via GetRetentionWindow.
type GigaStorageConfig struct {

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] Every other config struct in this package (StateStoreConfig, ReceiptStoreConfig) carries mapstructure tags so the values can be read from app.toml. Without them GigaStorageConfig can only ever hold compiled-in defaults. If these are meant to be operator-tunable, add the tags now (and see testutil/configtest/AGENTS.md for pinning the new keys); if they're intentionally internal-only, a short comment saying so would prevent the next person from assuming they're configurable.

HomePath string

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] Two small gaps vs. the sibling configs in this package:

  • HomePath is stored but never read; it duplicates information already baked into every path below it, so it can drift if a caller hand-builds the struct. Either drop it or document it as the record of what the paths were derived from.
  • There's no GigaStorageConfig.Validate(), while ReceiptStoreConfig, flatkvConfig.Config, littblock.LittBlockConfig, and gc.StorageGarbageCollectorConfig all have one. A Validate() fanning out to the nested configs (and rejecting a nil PruningConfig/FlatKVConfig/BlockDBConfig) would catch a hand-built config at the wiring site rather than at first use.

FlatKVConfig *flatkvConfig.Config
SSConfig StateStoreConfig
ReceiptDBConfig ReceiptStoreConfig
BlockDBConfig *littblock.LittBlockConfig
PruningConfig *gc.StorageGarbageCollectorConfig
}

// DefaultGigaStorageConfig returns a GigaStorageConfig whose store directories match the
// layout below, and whose pruning knobs are gc.DefaultStorageGarbageCollectorConfig():
//
// data/state_commit/flatkv

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] This layout holds for new nodes only — GetFlatKVPath, GetEVMStateStorePath and GetReceiptStorePath each fall back to a legacy directory (data/flatkv, data/evm_ss, data/receipt.db) when it already exists. Worth a "(new nodes; legacy directories are preferred when present)" qualifier so nobody treats these paths as guaranteed.

// data/state_store/evm/{backend} (sole SS; no Cosmos SS in Giga)
// data/ledger/receipt/{backend}
// data/ledger/block
//
// Giga opens SS via evm.NewEVMStateStore(dir, ssConfig) directly — not through
// composite.NewCompositeStateStore — so the EVM path lives on EVMDBDirectory (the
// argument callers pass as dir). DBDirectory and EVMSplit are left at their defaults:
// they only matter for the composite path, which Giga does not use.
//
// Unlike the other Default*Config helpers in this package, this can fail: the block-store
// default wraps littdb.DefaultConfig, which validates the directory path.

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] This comment isn't accurate: littdb.DefaultConfig doesn't validate the directory path, it only errors when len(paths) == 0. Since utils.GetBlockStorePath is a plain filepath.Join that always returns a non-empty string, the error branch below is unreachable — so DefaultGigaStorageConfig returns (GigaStorageConfig, error), and forces every caller to handle an error, for a condition that cannot occur. Either reword to say what actually fails, or drop the error from the signature.

func DefaultGigaStorageConfig(homePath string) (GigaStorageConfig, error) {
Comment thread
yzang2019 marked this conversation as resolved.

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 constructor has no test coverage. A table test asserting each resolved directory equals the corresponding utils.Get*Path(home, backend) — and one covering the littblock.DefaultConfig failure path that motivated the (GigaStorageConfig, error) signature — would pin the layout the doc comment above promises, and would have surfaced the DBDirectory issue on line 51.

blockDBConfig, err := littblock.DefaultConfig(utils.GetBlockStorePath(homePath))
if err != nil {
return GigaStorageConfig{}, fmt.Errorf("failed to build block db config: %w", err)
}

flatKV := flatkvConfig.DefaultConfig()

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] flatkvConfig.DefaultConfig() brings its own retention policy that the collector cannot see: SnapshotInterval = 10000 and SnapshotKeepRecent = 1, and CommitStore.pruneSnapshots runs unconditionally after every snapshot (sei-db/state_db/sc/flatkv/snapshot.go:514). So flatKV holds at most ~2 snapshots — roughly 20,000 blocks of restorable state — while PruningConfig here promises 101,000 blocks of servable history.

The rollback invariant still holds (a snapshot at or below head - 1000 is always among the retained pair), and the collector never over-prunes, since flatKV answers its oldest snapshot and contiguous stores answer the lower cutLine. But blockDB/receiptDB will retain 101k blocks of history that flatKV can no longer be restored to, so RetentionBeyondRollbackWindow = 100_000 is not actually delivered for state. Either raise SnapshotKeepRecent here to cover the GC window, or narrow the doc claim at lines 16-18 — as composed, gc.DefaultStorageGarbageCollectorConfig is not the single source of truth for how much history survives.

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.

This will be fixed in the future PR, right now it's not wired up yet

flatKV.DataDir = utils.GetFlatKVPath(homePath)

ssConfig := DefaultStateStoreConfig()

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] Codex flagged this as a live double-pruner; it is currently latent, but worth zeroing anyway. DefaultStateStoreConfig() leaves KeepRecent = DefaultSSKeepRecent = 100000 (sei-db/config/ss_config.go:7), which is below the collector's 101,000-block window. It does not bite today because Giga opens SS through evm.NewEVMStateStore, which starts no pruning manager (only composite.NewCompositeStateStore does, at sei-db/state_db/ss/composite/store.go:170) and the pebble backend ignores the field.

Since the doc comment above advertises this config as the pruning authority, setting ssConfig.KeepRecent = 0 explicitly here would remove the trap for whoever later changes the SS wiring, and document the intent in the same place.

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.

Will take care in future PRs

ssConfig.EVMDBDirectory = utils.GetEVMStateStorePath(homePath, ssConfig.Backend)

receiptConfig := DefaultReceiptStoreConfig()
receiptConfig.DBDirectory = utils.GetReceiptStorePath(homePath, receiptConfig.Backend)

return GigaStorageConfig{
HomePath: homePath,
FlatKVConfig: flatKV,
SSConfig: ssConfig,
ReceiptDBConfig: receiptConfig,
BlockDBConfig: blockDBConfig,
PruningConfig: gc.DefaultStorageGarbageCollectorConfig(),
}, nil
}
34 changes: 34 additions & 0 deletions sei-db/config/giga_config_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,34 @@
package config

import (
"path/filepath"
"testing"

"github.com/stretchr/testify/require"

"github.com/sei-protocol/sei-chain/sei-db/common/utils"
)

// TestDefaultGigaStorageConfigDirectories pins Giga's EVM-only SS layout: the path
// for evm.NewEVMStateStore lives on EVMDBDirectory. Giga does not use composite, so
// DBDirectory stays empty and EVMSplit stays at its default.
func TestDefaultGigaStorageConfigDirectories(t *testing.T) {
home := t.TempDir()
cfg, err := DefaultGigaStorageConfig(home)
require.NoError(t, err)

require.Equal(t, home, cfg.HomePath)
require.False(t, cfg.SSConfig.EVMSplit)
require.Empty(t, cfg.SSConfig.DBDirectory)
require.Equal(t, utils.GetEVMStateStorePath(home, cfg.SSConfig.Backend), cfg.SSConfig.EVMDBDirectory)

require.Equal(t, utils.GetFlatKVPath(home), cfg.FlatKVConfig.DataDir)
require.Equal(t, utils.GetReceiptStorePath(home, cfg.ReceiptDBConfig.Backend), cfg.ReceiptDBConfig.DBDirectory)
require.NotNil(t, cfg.BlockDBConfig.Litt)
require.Equal(t, []string{utils.GetBlockStorePath(home)}, cfg.BlockDBConfig.Litt.Paths)

require.Equal(t,
filepath.Join(home, "data", "state_store", "evm", cfg.SSConfig.Backend),
cfg.SSConfig.EVMDBDirectory,
)
}
1 change: 0 additions & 1 deletion sei-db/ledger_db/event/placeholder.go

This file was deleted.

1 change: 0 additions & 1 deletion sei-db/ledger_db/transaction/placeholder.go

This file was deleted.

64 changes: 64 additions & 0 deletions sei-db/management/gc/api.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,64 @@
package gc

// InfiniteRetentionWindow is the conventional GetRetentionWindow value meaning "never
// prune this store." Any negative value is treated the same: the store does not vote
// and is never passed to PruneBelow.
//
// Note: elsewhere in this repo (KeepRecent / MinRetainBlocks) 0 often means "keep
// forever." Here 0 means "no extra beyond the shared RollbackWindow," so infinite
// retention needs a negative sentinel.
const InfiniteRetentionWindow int64 = -1

// PrunableStore is a store whose old data may be dropped by the StorageGarbageCollector.
type PrunableStore interface {
// Name identifies the store (logging / errors). Duplicate names are allowed;
// the collector keys answers by index, not by name.
Name() string

// PruneBelow may drop data for all blocks below blockNumber.
// The store may perform the deletion asynchronously.
PruneBelow(blockNumber uint64) error

// GetRetentionWindow is how many extra blocks beyond the shared RollbackWindow
// this store needs to keep servable. Cut line (head = min non-zero GetLatestBlock):
//
// retention < 0 → store ignored (cutLine treated as 0; conventionally -1)
// retention >= 0 → cutLine = head - RollbackWindow - retention
//
// RollbackWindow is shared so every store stays consistent under rollback.
// Retention is optional history on top — kept so the store can still serve queries
// after a rollback that consumes the entire rollback window.
//
// Because the collector prunes every participating store to the *lowest*
// GetPruningBoundary, a large retention on one store effectively deepens pruning
// for the others (see StorageGarbageCollector). Read this as an input to that
// shared min, not as an independently applied per-store policy.
//
// Contract by store kind:
// - Contiguous (blockDB, receiptDB, state WAL): -1 / 0 / positive as configured.
// - SC: always 0 (only needs the shared rollback window for snapshots).
// - SS: always 0 (SS prunes its own version history via KeepRecent; the collector
// only drives SS snapshot pruning for the shared rollback window).
GetRetentionWindow() int64

// GetPruningBoundary returns the oldest block this store must keep to remain able to
// serve cutLine, or 0 to opt out of this cycle (not participate in calculating min prune height).
//
// Contiguous stores can restore to any height they hold → return cutLine.
//
// Snapshot stores can restore only at snapshot heights → return the newest completed
// snapshot ≤ cutLine, or the oldest completed snapshot if every snapshot is above
// cutLine (none can be dropped yet). No completed snapshot → 0.
//
// Assumption: snapshot creation finishes quickly enough that an in-flight write need

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 assumption is a deliberate weakening of the base behavior and deserves to be called out as such rather than only as "out of scope". The code being deleted skipped the entire cycle when any store was empty, with the comment "a snapshot may be mid-write and take hours" — exactly the case now declared out of scope.

Concrete degradation: a snapshot store with no completed snapshot answers 0, so it neither votes nor gets pruned, but contiguous stores still prune to head - RollbackWindow. With the default RollbackWindow of 1000, head advances past any fixed height within minutes at Sei block times. If a first snapshot begins at height H and takes hours, the WAL will have pruned well above H by the time it lands, so the landed snapshot can never be replayed forward — the store stays unrestorable until a later snapshot lands at or above the WAL's start.

The stated invariant ("if rollback was possible before the prune, the prune does not take it away") does survive, since rollback was already impossible with no snapshot — so this isn't a blocker. But the window is materially larger than "seconds" once RollbackWindow is 1000 rather than 10,000. Please either (a) note in the interface doc that an implementation whose snapshot writes can outlast RollbackWindow blocks must report the in-progress height instead of 0, or (b) track it as a required follow-up before the first real snapshot-store adapter lands.

// not be reserved; modeling long-running snapshot writes is out of scope.
GetPruningBoundary(cutLine uint64) uint64

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 "no completed snapshot → return 0" rule drops a protection the deleted code had on purpose. Previously an empty snapshot store aborted the whole cycle, with the reason spelled out inline: "An empty store is treated as unknown rather than empty (a snapshot may be mid-write and take hours)." Now the other stores prune freely, so a WAL can be pruned to cutLine while a snapshot for a much lower height is still landing — that snapshot cannot then be replayed forward, and the node is stuck.

The PR body scopes this out on the assumption that snapshot writes complete within seconds, which is a reasonable call. The gap is that the interface offers no escape hatch for an implementer who is in that state: returning 0 here lets others prune, and returning InfiniteRetentionWindow from GetRetentionWindow doesn't help either, because a skipped store still doesn't constrain the WAL (see my note on storage_garbage_collector.go). Since GetRetentionWindow is re-read every cycle, a bootstrapping/mid-write store could signal dynamically — but only if the skip path is fixed to keep constraining others. Worth documenting whichever answer you intend, so store authors don't pick the unsafe one by default.


// GetLatestBlock returns the highest block this store has ingested.
//
// 0 means "nothing ingested yet" and is ignored when computing the global head, so a store
// still filling does not stall pruning for the rest of the fleet. It does not mean
// "disabled": a store disabled for this node is not instantiated and never reaches the
// collector. See StorageGarbageCollector for why skipping a 0 head is safe.
GetLatestBlock() (uint64, error)
}
Loading
Loading