erigontech / erigontech/erigon
execution/commitment: v3 edge-record format and the hoodi 1M v2/v3 run
- Dominant language
- Go
- Stars
- 3.6k
- Forks
- 1.5k
- Avg merge
- 1d 16h
- Merged PRs (30d)
- 455
Description
Commitment v3 replaces each bundled branch row with one record per trie edge; leaves are records too, not fields of the parent row. The [design note](https://github.com/erigontech/erigon/blob/awskii/commitment-v3/docs/plans/20260827-commitment-per-slot-records.md) predates the implementation, and the cited code lines in [`awskii/commitment-v3`](https://github.com/erigontech/erigon/tree/awskii/commitment-v3) are the specification. Part of #20477.
## Format
| Item | Encoding or rule |
|---|---|
| Node key | Whole nibble pairs are packed two per byte. An even-length path ends with the terminator `0x00`; for an odd-length path the trailing nibble is not packed, it rides in the terminator as `0xf0\|nibble`. Length is `floor(n/2)+1`; no parity byte or pad nibble. `nibbles_v2.go:24`, `nibbles_v3.go:32-51` |
| Record key | `nodeKey(parent) \|\| (0x80\|childNibble)`, length `floor(depth/2)+2`. Siblings occupy `[nodeKey\|\|0x80, nodeKey\|\|0x90)`. `nibbles_v3.go:85-93`, `nibbles_v3.go:112-115`, `nibbles_v3.go:127-135` |
| State key | `0x00`, also the empty-path root node key; value shape distinguishes state from records. `branch_cache.go:40`, `branch_cache.go:104-107`, `commitment_context.go:1328-1330` |
| Flags | `0x01` leaf, `0x02` odd extension, `0x04` storage leaf, `0x08` account has storage, `0x10` hash present, `0x20` hoisted slot. For a branch child, `0x10` is always set and the 32-byte hash is mandatory; for a leaf, it marks an optional 32-byte state hash directly after the flags byte. `record.go:31-39` |
| Branch child | `[flags:1][childMask:2 BE][hash:32][extension:tail]`; `0x10` is set. `record.go:154-168` |
| Storage leaf | `[flags:1][stateHash:32?][storageSlot:32]`; no extension. `record.go:220-221`, `record.go:394` |
| Hoisted slot | `[flags:1][stateHash:32?][storageMask:2 BE][accountAddr:20][storageSlot:32][extension:tail]`. `record.go:222-228` |
| Account with storage | `[flags:1][stateHash:32?][storageRoot:32][storageMask:2 BE][accountAddr:20][extension:tail]`. `record.go:229-235` |
| Plain account | `[flags:1][stateHash:32?][accountAddr:20][extension:tail?]`. `record.go:236-240` |
| Extension | Packed two nibbles per byte; length follows from record length, `0x02` marks odd count, and the pad nibble is zero. `record.go:383-433` |
| Masks | A non-root node’s mask is stored in its incoming edge record. `RootMask` lives in the state blob. Unknown-mask reads request all 16 children. `record.go:126-168`, `hex_patricia_hashed.go:3120-3123`, `hex_patricia_hashed.go:3169`, `commitment_records.go:44-57` |
| Deletion | Write a zero-length value at the cleared child’s record key; mask-driven reads omit it, while merges drop it. `commitment.go:604-626` |
| Version gate | Edge `.kv` files use v3.0; bundled rows use v2.2, or v2.1 with references. A file is edge-record format iff version ≥3.0; merges derive output format from inputs. `state_schema.go:185-210` |
| Configuration | `COMMITMENT_EDGE_RECORDS` is read once, defaults true, and has no CLI or `erigondb.toml` setting. Existing commitment `.kv` files persist the datadir choice and override the default; mixed formats are rejected. `state_schema.go:217-220`, `commitment_format_resolve.go:33-71`, `aggregator.go:396` |
| Compatibility | Record reads skip files below v3.0. `erigon commitment convert` changes legacy key encoding and `.kv` squeeze state only, refuses edge-record files, and does not migrate history or accessors. `commitment_records.go:136-139`, `commitment_convert.go:84`, `commitment_convert.go:390`, `commitment_convert.go:404` |
| History | Each entry holds the previous value of one edge-record key, 34 B per entry in the raw step-44 file on hoodi; v2 holds the previous bundled row. Unchanged values are not written. `commitment.go:625`, `domain_shared.go:1856-1866`, `temporal_mem_batch.go:151-161` |
## Results
Two identical AMD EPYC 4344P hosts with 16 threads and 125 GiB RAM ran hoodi from wiped chaindata with no state snapshot files to block 1M using `3.7.0-dev-d7a9d63f`, `--chain=hoodi --prune.mode=full --prune.include-commitment-history --exec.serial --experimental.parallel-commitment`, and `KV_READ_METRICS=true`; the v2 arm set `COMMITMENT_EDGE_RECORDS=false`. Wall clock was 22,389 s for v2 and 20,609 s for v3: v3 was 8.0% faster and led every 100k-block range.
| On disk | v2 | v3 | v3/v2 |
|---|---:|---:|---:|
| `.kv` | 9,609,434,637 B | 7,536,176,763 B | 0.78 |
| `.bt` | 52,560,944 B | 150,406,752 B | 2.86 |
| `.kvei` | 36,260,880 B | 116,359,184 B | 3.21 |
| `.v` history | 28,704,912,438 B | 17,586,615,076 B | 0.61 |
| `.vi` | 1,954,684,787 B | 2,548,136,820 B | 1.30 |
| `.ef` | 1,818,839,737 B | 4,480,275,804 B | 2.46 |
| `.efi` | 160,629,568 B | 606,486,055 B | 3.78 |
| All commitment files | 42,337,322,991 B | 33,024,456,454 B | 0.78 |
| chaindata (mdbx file size, never shrinks) | 36,674,994,176 B | 19,864,223,744 B | 0.54 |
The v3 format saves 13.2 GB of data, adds 3.9 GB of index, and saves 9.3 GB net; every per-key index grows 2.5–3.8x. The v2 arm’s unmerged step file inflates its history total. Between-cycle flush falls from 2,748 s to 1,209 s, including MDBX sync from 984 s to 159 s; storage writes fall from 1,133 GB to 502 GB. The v3 format pays 1.93x in commitment compute and 17.6x in warmup file-record reads for +3.5% process CPU.
[#23199](https://github.com/erigontech/erigon/issues/23199) reports 110–140 MiB/block of MDBX copy-on-write writes at mainnet tip; v3 cuts mdbx cow page ops 4.0x and storage writes 2.26x on this from-0 hoodi run, while tip behaviour remains unmeasured.
## Open
- Whether the per-record cost is acceptable: keys 2.14x at step 44 (3.03x at step 0), history entries 1.30x, every per-key index 2.5-3.8x; the alternative is bundled rows for the trunk and edge records only inside storage subtrees.
- Reproduce the mainnet-tip write profile on v3.
- Measure unknown-mask reads across a deep file stack using the branch counters; the rig binary predates them.
- Add history and accessor migration before changing any published snapshot set.
Contributor guide
Research direction
Start with the cited commitment-v3 design note and the referenced files, especially nibbles_v3.go, record.go, commitment.go, state_schema.go, and commitment_records.go. Reproduce the hoodi 1M v2/v3 run with the documented command, then measure mainnet-tip writes and unknown-mask reads using the branch counters. Done means the open measurements are reported and history/accessor migration is added before published snapshots change.
Written by the indexing model from the issue text.
Assessment
- Tech stack
- go
- Domain
- blockchain, databases, performance
- Issue type
- Feature
- Difficulty
- 5/5
- Estimated time
- Over a week
- Activity status
- Active
- Clarity
- Mostly clear
- Newbie friendliness
- 35/100