Erigon uses a temporal database architecture separating hot (mutable) from cold (immutable) data.
- New state changes → hot MDBX tables
- Periodic snapshots freeze old data
- Cold data compressed for long-term storage
Unwindbeyond data in snapshots not allowed
datadir/
├── chaindata/ # Hot state (MDBX) - recent blocks and state
└── snapshots/
├── domain/ # Latest state values
├── history/ # Historical value changes
├── idx/ # Inverted indices for search
├── accessor/ # Additional lookup indices
└── caplin/ # Consensus-layer state-table snapshots
- Fork of LMDB optimized for Erigon's access patterns
- Hot database for recent, mutable state
- Tables defined in
kv/tables.go
kv_temporal.go-TemporalDBwraps MDBX + Aggregator- Enables time-travel queries via
GetAsOf(txNum) - Methods:
GetLatest(),HistorySeek(),RangeAsOf(),IndexRange()
aggregator.go- Central hub managing domains and indicesdomain.go- State domain with embedded historyhistory.go- Historical value trackinginverted_index.go- Time-travel indices (key → [txNums])
| Domain | Purpose | Content |
|---|---|---|
| AccountsDomain | Account state | nonce, balance, code hash |
| StorageDomain | Contract storage | key-value slots |
| CodeDomain | Contract bytecode | deployed code |
| CommitmentDomain | Merkle proofs | state root commitments |
Each domain manages:
- Current values (hot, in MDBX)
- Historical snapshots (.seg files)
- Indices for lookups
Sorts data before database insertion to reduce write amplification:
- Collect changes in memory/temp files
- Sort by key
- Batch insert in order
.segfiles store immutable historical data- Downloaded via BitTorrent with WebSeed fallback
- Piece size: 2MB default
- Verification on download
Preverified.Typed(snapcfg/util.go) filters block andcaplin/entries to the binary's supported version window for their type and keeps the newest version per name.domain/,history/,idx/,accessor/state entries bypass both — they are kept unconditionally under their full name.caplin/-prefixed entries are the consensus state tables (BlockRoot,EpochData, …); they have no snaptype of their own and borrowsnaptype.BeaconBlocks' window, so publishing a newer caplin state-table version requires bumping that type first, or existing binaries silently ignore the new files. Beacon blocks and blob sidecars live at the snapshots root, not undercaplin/, and take the ordinary typed path with their own per-type windows- Offline verification:
integrity.VerifyTorrentFiles(integrity/torrent_verify.go) re-hashes data files against their.torrentpiece hashes, walking<datadir>/snapshotsrecursively and following symlinked subtrees, so it coverscaplin/and the state dirs rather than only the top-level block segments.failFastonly decides whether the run stops at the first mismatch; a mismatch, an unreachable path or a cancelled context always fails the call - Caplin's two snapshot collections do not share a lifecycle:
freezeblocks.CaplinSnapshots(beacon blocks, blob sidecars) embedssnapshotsync.BaseRoSnapshotsand reads through pinned generations, whilesnapshotsync.CaplinStateSnapshots(thecaplin/state tables) still keeps its owndirty/visiblecopy. Three caplin accessors keep conventions the base does not, and consumers depend on each:SegmentsMaxis dirty-backed (it counts a dumped-but-unindexed tail, which the archive backfill stop condition needs),FrozenBlobsis the exclusiveTo(consumers compareslot < FrozenBlobs()), andOpenFolderkeeps caplin's own gap-filtered scan so a segment past a gap never reaches the dirty set
Per-datadir settings that travel with a snapshot set rather than the binary, resolved by state.ResolveErigonDBSettings (state/erigondb_settings.go). A downloaded erigondb.toml is synced snapshot metadata and is never rewritten, so a producer's published values survive on consumers.
| Field | Meaning |
|---|---|
step_size |
txs per step |
steps_in_frozen_file |
steps merged into a frozen (immutable) file |
references_in_commitment_branches |
whether new commitment merges write referenced (short-key, v2.1) .kv files; absent → default true |
references_in_commitment_branches governs writes only. Reads are version-aware (a commitment .kv is referenced iff its version < v2.2 and its range ≥ the referencing threshold), so flipping the flag on a populated datadir stays correct in both directions and old referenced files convert to plain lazily through merges. To publish plain (v2.2) snapshots, a producer sets it false before running merges (see the erigondb-sync-integration-test-plan skill).