erigontech / erigontech/erigon

execution/commitment: v3 edge-record format and the hoodi 1M v2/v3 run

Open
#23,773 0 comments 0 reactions 0 assignees View on GitHub
ErigonDB
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

Open the contributing 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

Get new issues in your inbox

A short digest of beginner-friendly GitHub issues.