From 962c07ef14428e4a3d10af45c9d08f7b797e462b Mon Sep 17 00:00:00 2001 From: awskii Date: Wed, 29 Jul 2026 17:10:23 +0700 Subject: [PATCH 01/56] docs: add PBin binary commitment engine implementation plan EIP-8297 binary state trie as a new commitment engine alongside HexPatriciaHashed. M0 scope: in-memory over MockState, account and storage zones, validated against a transcription of the spec's own reference algorithm. No external API changes. --- docs/plans/20260729-pbin-patricia-hashed.md | 300 ++++++++++++++++++++ 1 file changed, 300 insertions(+) create mode 100644 docs/plans/20260729-pbin-patricia-hashed.md diff --git a/docs/plans/20260729-pbin-patricia-hashed.md b/docs/plans/20260729-pbin-patricia-hashed.md new file mode 100644 index 00000000000..16dbc29edbe --- /dev/null +++ b/docs/plans/20260729-pbin-patricia-hashed.md @@ -0,0 +1,300 @@ +# PBinPatriciaHashed — binary commitment engine (EIP-8297) + +## Overview + +Add `PBinPatriciaHashed`: a **binary** commitment engine implementing EIP-8297 (Partitioned Binary Tree), as a sibling of `HexPatriciaHashed` reusing the same grid/fold/unfold idea with a different node model. + +**Problem it solves.** EIP-8297 is the EF's Standards Track successor to Verkle for the state tree: arity 2, hash-only (post-quantum), no `storage_root`, code chunked into the tree. Erigon has no binary trie. This lands one as a self-contained engine so the design can be evaluated — in particular its single-pass root computation, which has no account→storage sequential dependency and is therefore a clean testbed for parallel fold work. + +**Key constraint: new engine, no external API changes.** `Trie`, `PatriciaContext`, `Updates`/`Update`, `keyHasher`, `cellEncodeData`, `BranchData` and the `nibbles` package are **not modified**. PBin is additive: new files plus one variant registration. `PatriciaContext.Branch` returns opaque bytes, so PBin uses its own branch record codec without touching the shared one. + +**Scope: M0 only.** In-memory over `MockState`, `ModeDirect`, account + storage zones. Correctness against a reference oracle is the deliverable — not production wiring. + +## Context (from discovery) + +- Repo `/Users/awskii/org/wrk/erigon`, branch `main` @ `1e078ffb04`. Package `execution/commitment` (29,422 lines incl. tests; `hex_patricia_hashed.go` 3,164; `commitment.go` 2,345). +- Spec: `/Users/awskii/org/wrk/EIPs/EIPS/eip-8297.md`. Anchors: tags/merkelization `:187-222`, `insert`/split `:137-183`, constants `:271-278`, header values `:311-347`, storage `:399-437`, no-deletion `:441-447`, test cases `:583-630`. +- Reference points in HPH: grid `:129`, existing `cell` `:300-315`, `needUnfolding` `:1263-1319` (reads `cell.hashedExtension` at `:1310` — the mechanism that makes the cell prefix load-bearing for navigation), `fold` dispatch `:2031-2038`, `foldBranch` `:1660-1725`, `foldPropagate` `:1915-1953`, `RootHash` `:362`/`:1249`, branch DB key `:1443`, `updateKey` `:2023`. +- `BranchEncoder.CollectUpdate` `commitment.go:501-546` merges with `prev` before `PutBranch`. `keyHasher` `:1478`, `hasherReusesAddrPrefix` `:1485`. +- `MockState` test driver `patricia_state_mock_test.go:39-202`; note `Account`/`Storage` return `Flags = DeleteUpdate` for a **missing** key (`:92-95`, `:129-134`). +- Invariant tests worth porting: `hex_patricia_hashed_test.go:157-249`. + +## Development Approach + +- **testing approach**: TDD — in every task the failing test is written **before** the implementation it covers. The reference oracle (Task 4) exists before the engine it validates. +- **CRITICAL naming rule**: `package commitment` already declares `cell`, `computeCellHash`, `fold`, `unfold` and more. **Every new package-level identifier MUST carry a `pbin` prefix** — `pbinCell`, `pbinFold`, `pbinLeafHash`, `pbinTreeKeyAccount`, `pbinEmptyTreeHash`. Methods on new types need no prefix. A collision is a compile error, so this applies to every task. +- **CRITICAL no-API-change rule**: do not modify `Trie`, `PatriciaContext`, `Updates`/`Update`, `keyHasher`'s signature, `cellEncodeData`, `BranchData`, or the `nibbles` package. If a task appears to require it, stop and record it with ⚠️ rather than proceeding. +- complete each task fully before moving to the next +- **every task MUST include new/updated tests**, listed as separate checklist items +- **all tests must pass before starting the next task** +- **update this plan file when scope changes during implementation** +- plan is self-contained from a clean git state; no task depends on transient working-tree state + +## Testing Strategy + +- **unit tests**: required per task, table-driven where the input space is enumerable +- **differential tests**: root equality against the EIP reference oracle (Task 4). Note its blind spot: the oracle consumes the same value encoder as the engine, so it can **not** catch value-encoding bugs — those are pinned against hand-written hex in Task 3. +- **property tests**: permutation independence, fold/unfold round-trip, branch-record recompute across batches +- **fuzz**: codec round-trip across all bit lengths; process fuzzers with a low-entropy slot generator +- no e2e tests — library-internal engine, no UI surface + +## Hazard Register + +Each hazard has exactly one detecting guard. Guards are acceptance criteria, not nice-to-haves. + +| ID | Hazard | Detecting guard | Task | +|----|--------|-----------------|------| +| H1 | Stale branch-cell hash after a prefix split (prefix is inside the branch hash, so shrinking it invalidates a cached hash) | Oracle diff on a mined deep-shared-prefix corpus; debug assert that a cell whose prefix bit length changed has `hashLen == 0` | 8, 11 | +| H2 | Untouched sibling dropped across `Process` batches (at arity 2 the sibling is the entire other half of the subtree) | Two-phase test: batch A writes both children, batch B touches one, assert root equals oracle over A∪B | 11 | +| H3 | Implicit prefix bit length — byte length silently carries up to 7 spurious bits into `encode_bit_prefix` | Explicit uvarint bit count; decode asserts `byteLen == ceil(bitLen/8)` and zero pad bits | 5 | +| H4 | Prefix buffer truncation (a 66-byte prefix into a smaller field; Go `copy` is min-length and silent) | Cell encode/decode round-trip with prefix bit length drawn from `[0, 529)` | 5 | +| H5 | DB branch-key aliasing — two bit paths encoding to one key means one read, one stale record | Codec round-trip fuzz over every bit length 0..528 + explicit non-canonical-pad rejection | 1 | +| H6 | State-blob depth truncation (`byte(depth)` maps bit-depth 300 → 44) | **N/A in M0** — no state blob. Re-arm when save/restore lands. | — | +| H7 | Zero-length prefix overloaded to mean "not a stored branch" (EIP-8297 permits an empty branch prefix) | Unfold a stored branch record with `prefixBitLen == 0`, assert it is descended into, not treated as leaf/empty | 7 | +| H8 | Zone mis-routing of slots 0..63 (hottest slots land in the wrong zone; tree stays internally consistent) | Zone-boundary tests at slots 63/64/255/256 + plain-key validator over every written record | 2, 11 | +| H9 | Terminator arithmetic carried over from hex (`hashedExtLen-1` at `:1310` exists only to strip the hex terminator) | Table of `(cellPrefix, probeKey) → expected needUnfolding result` covering `cpl==0`, `cpl==len(prefix)`, `cpl=64` in `STORAGE_ZONE` with `tree_index = slot/256`, `sub_index = slot%256` — the sub-index is the **raw** low byte, not hashed, so adjacent slots co-locate. +2. **Key representation** `[9]uint64` big-endian words + `bitLen int16`. Divergence = XOR + `bits.LeadingZeros64`, **clamped** by `min(aLen,bLen)`. Both 272 and 528 are `8k+2` bytes, so both end in a 16-bit tail word — one mask constant. The tail **must** be masked or XOR reads garbage (H10). +3. **Hash = Keccak-256** via erigon's `keccak.KeccakState`, used for both `H` and `key_hash`. EIP-8297 defines `H` abstractly (`:187-189`) and names Keccak as a candidate (`:513`), so this is spec-conformant. Reached through one interface so it can be swapped. +4. **Grid** `[528][2]pbinCell`. Row-indexed arrays are `[528]`; depth-indexed arrays are `[529]` because depth is inclusive of 528. HPH measured: cell 456 B, grid 933,888 B. PBin ≈416 B/cell → ≈439 KB. +5. **touchMap/afterMap** stay `uint16` using bits 0-1 only, so `OnesCount16`/`TrailingZeros16` logic ports unchanged. Assert `(touch|after) &^ 0b11 == 0` at fold entry. +6. **Prefix lives in two places**: the branch record's DB **key** (full path from root, as HPH does at `:1443`/`:2023`) *and* in the parent's stored cell. The cell copy is **navigation** — `unfold` cannot reconstruct the descent key without it (verified at `hex_patricia_hashed.go:1310`). Representation changes nibbles→bits only. +7. **Branch DB key codec**: `packBitsMSBFirst(path) || byte(bitLen mod 8)`, zero-padded, **non-canonical pad rejected on read**. Max 67 B, `MaxPathBits = 528`. `bitLen == 0` → single `0x00`. A *leading* length field is forbidden because it would scatter a subtree's records across the keyspace; the property relied on is **subtree-range contiguity**, not ancestor-before-descendant ordering (a 7-bit path encodes `[0x00,0x07]` while its 8-bit descendant encodes `[0x00,0x00]`, so descendants can sort before ancestors — that is acceptable and must not be assumed away). +8. **Split rehash = materialize-on-split.** Because the prefix is inside the branch hash and `_insert` shrinks a split survivor's prefix to `node.prefix[matched+1:]` (`eip:174-176`), a split invalidates the cached child hash — a problem HPH never has, since `extensionHash` hashes at the parent over the child's hash. Resolution: when `needUnfolding` reports divergence **inside** a cell's prefix, unfold the survivor at its own path and recompute from its two children. **If the survivor is a leaf it has no record and needs no read** — its hash commits the complete key (`eip:106-109`). The survivor's DB key does not change, only its hash. +9. **Branch records are self-contained**: always encode **both** cells (`bitmap = afterMap`), so no merge-with-previous path exists. This diverges deliberately from `BranchEncoder.CollectUpdate`'s merge (`commitment.go:501-546`) and is what makes H2 tractable at arity 2. +10. **Oracle** = a naive Go transcription of the EIP's `BinaryTree`/`_insert`/`merkelize` (`eip:112-222`) in the test package. Root equality against it is the M0 gate, with the value-encoding blind spot noted in Testing Strategy. + +**How the no-API-change constraint is satisfied:** + +- `keyHasher` stays `func([]byte) []byte`, returning the **primary** leaf's tree key. PBin writes the `CODE_HASH` sibling leaf at `sub_index+1` during the same stem visit. Ordering holds because sub-indices ascend `0 → 1 → 64..`, so `Updates`, `HashSort`, `TouchPlainKey` are untouched. `hasherReusesAddrPrefix` (`:1485`) pointer-compares against `KeyToHexNibbleHash`, so a PBin hasher yields `addrCacheReuse=false` with no edit. +- PBin uses its **own** branch record codec; a 66-byte prefix does not fit the shared `cellEncodeData.extension [64]byte`. `PatriciaContext.Branch` returns opaque bytes, so nothing shared changes. +- The only edit to a pre-existing non-test file is additive: a variant constant plus a switch case in `commitment.go`. + +## Technical Details + +**bitpath** (`pbin_bitpath.go`) +```go +type bitpath struct { + w [9]uint64 // big-endian words; byte order == descent order + bitLen int16 // 0..528 +} +func (p *bitpath) bit(d int16) uint64 +func (p *bitpath) maskTail() +func pbinCommonPrefixBits(a, b *bitpath) int16 // XOR + LeadingZeros64, clamped by min(aLen,bLen) +``` + +**Values** (`pbin_values.go`) — BASIC_DATA is 32 bytes (`eip:332-339`): `version(1) || reserved(3) || code_size(4) || nonce(8) || balance(16)`, big-endian. `Update.Balance` is a `uint256.Int` (`commitment.go:2187`) but the EIP field is 16 bytes, so balances `>= 2^128` **error** rather than truncate. Storage values are left-padded to exactly 32 bytes (`eip:132`). + +**Branch record** (`pbin_branch.go`, PBin-local): `touchMap(2) || afterMap(2) || per-cell{ fields(1), prefixBitLen(uvarint), prefixBytes, hash|leafKey }`, both cells always present. + +## What Goes Where + +- **Implementation Steps** (`[ ]`): all code, tests, and the additive variant registration inside this repo +- **Post-Completion** (no checkboxes): measurements, deferred decisions, follow-on milestones + +## Implementation Steps + +### Task 1: bitpath type and bit-path DB key codec + +**Files:** +- Create: `execution/commitment/pbin_bitpath.go` +- Create: `execution/commitment/pbin_bitpath_test.go` + +- [ ] write failing tests for `pbinCommonPrefixBits` at bit lengths 271, 272, 273, 527, 528; a case seeding `w[]` with `0xFF` beyond `bitLen`; and a 272-bit path that is a bitwise prefix of a 528-bit path asserting the result is 272 (guards H10) +- [ ] write a failing fuzz test for codec round-trip across every bit length 0..528, plus explicit non-canonical-padding rejection cases (guards H5) +- [ ] write a failing unit test asserting no valid encoding equals the literal `"state"` (`0x7374617465`) +- [ ] implement `bitpath` with `[9]uint64` words, `bitLen int16`, `MaxPathBits = 528`, and `bit`/`slice`/`append`/`hasPrefix`/`maskTail` +- [ ] implement `pbinCommonPrefixBits` using XOR + `bits.LeadingZeros64`, clamped by `min(aLen, bLen)` +- [ ] implement `pbinEncodeBitPath`/`pbinDecodeBitPath` as `packBitsMSBFirst(path) || byte(bitLen mod 8)`, rejecting non-canonical padding on read; `bitLen == 0` encodes to a single `0x00` +- [ ] run tests - must pass before task 2 + +### Task 2: EIP-8297 tree key derivation and zone routing + +**Files:** +- Create: `execution/commitment/pbin_keys.go` +- Create: `execution/commitment/pbin_keys_test.go` + +- [ ] write failing tests reproducing the EIP's vectors (`eip:583-630`), each asserting the **full** 34/66-byte key against a `keccak` computed inline in the test body rather than via the helper under test: BASIC_DATA key; slot 5 → sub-index `0x45`; slot 1000 → `tree_index 3`/`sub_index 0xE8` with `tree_index` as 32-byte big-endian +- [ ] write failing zone-routing tests at slots 63/64/255/256 and for the 12-byte address padding (guards H8) +- [ ] implement `pbinAddr32`, `pbinTreeKeyAccount(addr, subIdx)`, `pbinTreeKeyStorage(addr, slot)` with the `slot < 64` account-zone route +- [ ] implement the two-level digest cache: `H(addr32)` per address, `H(addr32||tree_index)` per 256-slot group, with `tree_index` encoded as 32-byte big-endian +- [ ] provide a `keyHasher`-compatible `func([]byte) []byte` returning the primary leaf's tree key, and assert `len` is 34 or 66 at every construction site +- [ ] run tests - must pass before task 3 + +### Task 3: Leaf value encoding + +**Files:** +- Create: `execution/commitment/pbin_values.go` +- Create: `execution/commitment/pbin_values_test.go` + +- [ ] write failing tests pinning BASIC_DATA byte offsets 0/4/8/16 against hand-written hex — **not** against the encoder, since the Task 4 oracle shares this encoder and cannot catch its bugs +- [ ] write a failing test asserting a balance `>= 2^128` returns an error rather than truncating +- [ ] write failing tests for the CODE_HASH leaf value and for storage values left-padded to exactly 32 bytes +- [ ] implement `pbinEncodeBasicData` per `eip:332-339`: `version(1) || reserved(3) || code_size(4) || nonce(8) || balance(16)` big-endian +- [ ] implement `pbinCodeHashValue` and `pbinEncodeStorageValue` +- [ ] run tests - must pass before task 4 + +### Task 4: EIP reference oracle in the test package + +**Files:** +- Create: `execution/commitment/pbin_oracle_test.go` + +- [ ] transcribe the spec's `LeafNode`, `BranchNode`, `_insert` and `merkelize` (`eip:112-222`) as a naive in-memory Go tree, Keccak-256, no optimisation +- [ ] implement `encode_bit_prefix` exactly per `eip:196-201` and define the empty-tree hash as 32 zero bytes per `eip:208` +- [ ] add corpus builders: empty; single key (root **is** a leaf, `eip:133-135`); two keys diverging at bit 0; two diverging at bit 527; a split-inside-prefix triple forcing `node.prefix[matched+1:]`; a mined deep-shared-prefix cluster +- [ ] write tests asserting the oracle is self-consistent: permutation independence and prefix-freedom over every corpus +- [ ] run tests - must pass before task 5 + +### Task 5: pbinCell, grid, and branch record codec + +**Files:** +- Create: `execution/commitment/pbin_cell.go` +- Create: `execution/commitment/pbin_branch.go` +- Create: `execution/commitment/pbin_cell_test.go` + +- [ ] write failing cell encode/decode round-trip tests with prefix bit length drawn from `[0, 529)` (guards H4) +- [ ] write failing tests for record decode rejecting inconsistent `prefixBitLen`/byte length and non-zero pad bits (guards H3) +- [ ] define `pbinCell` with a tree-key-space `bitpath` prefix and plain-key fields; **use one prefix, not two** — HPH's `hashedExtension`/`extension` split exists to hold hashed and plain spaces separately, whereas PBin derives the tree key from the plain key on demand. No `stateHash` field: a leaf hash is `H(0x00||key||value)` with nothing to memoize +- [ ] define the grid as `[528][2]pbinCell` with row-indexed arrays `[528]` and depth-indexed arrays `[529]`, plus `reset`/`resetForReuse` clearing `bitLen` +- [ ] implement the PBin branch record codec with `prefixBitLen` as an explicit uvarint **bit** count, always encoding both cells (`bitmap = afterMap`, no merge path) +- [ ] run tests - must pass before task 6 + +### Task 6: node merkelization + +**Files:** +- Create: `execution/commitment/pbin_hash.go` +- Create: `execution/commitment/pbin_hash_test.go` + +- [ ] write failing tests asserting each node hash matches the Task 4 oracle for hand-built shapes: single leaf, one branch, nested branch with non-empty prefix, branch with **empty** prefix +- [ ] write a failing node-level test asserting the empty subtree is 32 zero bytes, explicitly not `empty.RootHash` (guards H11) +- [ ] implement `pbinLeafHash = H(0x00 || key || value)` over the complete 34/66-byte key +- [ ] implement `pbinBranchHash = H(0x01 || encode_bit_prefix(prefix) || left || right)` with one scratch buffer sized 133 B (1 tag + 2 count + 66 prefix + 64 children) +- [ ] implement exactly **one** cell hasher — do not port both `computeCellHash` and `witnessComputeCellHashWithStorage` (guards H14) +- [ ] run tests - must pass before task 7 + +### Task 7: unfold and needUnfolding + +**Files:** +- Create: `execution/commitment/pbin_patricia_hashed.go` +- Create: `execution/commitment/pbin_unfold_test.go` + +- [ ] write a failing table test of `(cellPrefix, probeKey) → expected pbinNeedUnfolding result` covering `cpl == 0`, `cpl == len(prefix)` (full match, descend), and `cpl < len(prefix)` (split signal) (guards H9) +- [ ] write a failing test unfolding a stored branch record whose `prefixBitLen == 0`, asserting it is descended into rather than treated as leaf or empty (guards H7) +- [ ] write failing unfold tests for divergence at bits 0, 63, 64, 65, 271 and 527 +- [ ] create `PBinPatriciaHashed` with the grid, `currentKey bitpath`, context and Keccak state +- [ ] implement `pbinNeedUnfolding` with bit reads and clamped common-prefix, dropping hex terminator arithmetic and `clampToAccountBoundary`; its return contract MUST distinguish "prefix fully matched" from "diverges inside prefix" +- [ ] implement `pbinUnfold`/`pbinUnfoldBranchNode` reading the parent's stored cell prefix to reconstruct the descent key, with an explicit node-kind flag so a zero-length prefix is not overloaded +- [ ] run tests - must pass before task 8 + +### Task 8: fold primitives + +**Files:** +- Modify: `execution/commitment/pbin_patricia_hashed.go` +- Create: `execution/commitment/pbin_fold_test.go` + +- [ ] write failing grid-seeded unit tests: hand-build one row, fold it, assert the emitted hash equals the oracle's `merkelize` of that node and that the record bytes round-trip +- [ ] write a failing test for a split whose survivor is a **leaf**, asserting no branch record is read +- [ ] write failing tests forcing splits inside prefixes at several depths, asserting each rehashed node matches the oracle +- [ ] implement `pbinFold` dispatching the three kinds — delete / propagate / branch — mirroring `hex_patricia_hashed.go:2031-2038` +- [ ] implement `pbinFoldBranch` writing records keyed by the encoded bit path, asserting `(touchMap|afterMap) &^ 0b11 == 0` at entry and `popcount(afterMap) == 2` (guards H12) +- [ ] implement `pbinFoldPropagate` accumulating the child's prefix bits into the parent cell and writing **no** record, asserting `prefixBits == depth - upDepth - 1` (guards H12) +- [ ] implement materialize-on-split with the leaf-survivor short circuit, plus a debug assert that a cell whose prefix bit length changed has `hashLen == 0` (guards H1) +- [ ] add instrumentation counters for splits-inside-prefix and extra `ctx.Branch` reads +- [ ] run tests - must pass before task 9 + +### Task 9: drive loop, Process and RootHash + +**Files:** +- Modify: `execution/commitment/pbin_patricia_hashed.go` +- Create: `execution/commitment/pbin_process_test.go` + +- [ ] write a failing test asserting `RootHash()` on a fresh engine is 32 zero bytes, not `empty.RootHash` (guards H11) +- [ ] write a failing test for a one-key tree asserting the root **is** the leaf hash `H(0x00||key||value)` (`eip:133-135`), and for a two-key tree asserting it is the branch hash +- [ ] write failing `Process` tests over `MockState` for account-only, storage-only and mixed corpora, asserting root equality with the oracle +- [ ] implement `pbinUpdateCell`, the key-path descent, and the `Process` drive loop +- [ ] implement `RootHash` including the root-as-leaf case +- [ ] implement the account fan-out: write the `CODE_HASH` leaf at `sub_index+1` during the same stem visit, leaving `Updates`/`HashSort`/`TouchPlainKey` untouched +- [ ] reject deletes originating from the **update stream** only; a missing-key `ctx.Account`/`ctx.Storage` read returns `DeleteUpdate` (`patricia_state_mock_test.go:92-95`, `:129-134`) and MUST be treated as absent, not as a delete (guards H13) +- [ ] run tests - must pass before task 10 + +### Task 10: variant registration + +**Files:** +- Modify: `execution/commitment/commitment.go` +- Create: `execution/commitment/pbin_variant_test.go` + +- [ ] write `TestInitializeTrieAndUpdates_BinVariant` first as the red test, asserting the constructed type, `Variant()`, and `Mode() == ModeDirect` +- [ ] add `VariantBinPatriciaTrie` plus a case in `ParseTrieVariant`/`InitializeTrieAndUpdates` — **additive only** +- [ ] implement the remaining `Trie` methods to satisfy the interface unchanged: `Reset`, `ResetContext`, `Release`, `Variant`, `SetTraceWriter`, `EnableCsvMetrics` +- [ ] write a test asserting `Reset` then reuse produces the same root as a fresh engine +- [ ] run tests - must pass before task 11 + +### Task 11: hazard guards and differential fuzzing + +**Files:** +- Create: `execution/commitment/pbin_verify_test.go` +- Create: `execution/commitment/pbin_hazard_test.go` +- Create: `execution/commitment/pbin_fuzz_test.go` + +- [ ] implement an independent branch-record recompute oracle: walk every written record, decode, recompute bottom-up, assert it reproduces the root +- [ ] implement a bit-space plain-key validator asserting `treeKey(plainKey) == branchPath || cellPrefix` for every written record (guards H8) +- [ ] write the two-phase sibling test: `Process` batch A writing both children, then batch B touching one child, asserting the root equals the oracle over A∪B (guards H2) +- [ ] write the mined deep-shared-prefix corpus test and assert oracle equality (guards H1) +- [ ] write permutation-independence tests porting `Test_HexPatriciaHashed_UniqueRepresentation`/`2`/`BrokenUniqueRepr` (`hex_patricia_hashed_test.go:157-249`) +- [ ] write a differential fuzzer over `Process` against the oracle with a **low-entropy slot generator** — random 32-byte slots essentially never share a stem, so a default corpus never exercises sub-index sharing +- [ ] run tests - must pass before task 12 + +### Task 12: Verify acceptance criteria + +- [ ] verify all requirements from Overview are implemented and M0 scope boundaries were respected +- [ ] verify no shared type, interface or signature was modified: `git diff --stat` shows `commitment.go` as the only pre-existing non-test file, additive only +- [ ] verify every hazard in the register except H6 has a named passing test +- [ ] verify every new package-level identifier carries the `pbin` prefix and the package compiles without collision +- [ ] run the package test suite: `go test ./execution/commitment/...` +- [ ] run fuzzers briefly: `go test ./execution/commitment/ -run=Fuzz -fuzz=FuzzPBin -fuzztime=60s` +- [ ] verify `go build ./...` and `go vet ./execution/commitment/...` are clean +- [ ] record the Task 8 instrumentation counters under Post-Completion + +### Task 13: [Final] Update documentation + +- [ ] add a package-level doc comment on `pbin_patricia_hashed.go` naming the EIP, the Keccak suite choice and the M0 scope boundaries +- [ ] update `CLAUDE.md` if new patterns were discovered +- [ ] move this plan to `docs/plans/completed/` + +## Post-Completion + +*Items requiring manual intervention, measurement, or follow-on milestones — no checkboxes* + +**Decisions deferred to data:** +- Split-rehash strategy. M0 ships materialize-on-split. If the Task 8 counters show split-inside-prefix reads dominating, revisit storing `left||right` (64 B) per cell instead of a 32-byte child hash — a wire-format change needing its own migration story. +- Record the one-prefix-per-cell rationale (Task 5) here and in the commit body rather than as a source comment. + +**Out of scope, in rough dependency order:** +- Code chunks (`chunkify_code`, `eip:374-397`), including the stateful PUSHDATA boundary byte and content-addressed overflow chunks shared between contracts. +- Deletion semantics. EIP-8297 never removes entries, but erigon's `StorageDomain` represents never-written and explicitly-zeroed identically (`execution/state/rw_v3.go:965` calls `DomainDel` on an empty value). Production needs a tombstone-capable encoding or a documented deviation. Under EIP-8297 SELFDESTRUCT must **not** remove storage leaves, which removes the rationale for erigon's storage-subtree collapse. +- Commitment state save/restore (re-arms H6). `SetState`/`EncodeCurrentState` are concrete `*HexPatriciaHashed` methods and `commitmentdb` type-switches on them (`commitment_context.go:895-901`, `:935-949`, panics at `:103`, silently no-ops `SetCollapseTracer` at `:411`). Promoting a `StatefulTrie` interface is an external API change, deliberately excluded from M0; `:411` should error rather than no-op before any variant ships. +- Parallel mounting. `mountedNib 0..15` plus a depth-63 fold wall does not translate to arity 2; a 2-way root split silently serialises rather than failing. +- Domain-layer wiring and branch-cache tuning (dense tiers land on bit depths 4/8/12/16; a literal port covers ~1 in 8 bit depths). + +**Upstream:** +- The EIP is a Draft with an unfixed hash function, unfixed witness gas constants, and an unresolved header code-chunk count (EIP-7864 sets `CODE_CHUNKS_IN_HEADER = 16` at `eip-7864.md:219`; EIP-8297 puts 128 chunks in the header via `CODE_OFFSET = 128` at `eip-8297.md:271`; neither cites data). Any conformance claim should name the spec commit it was built against. From db9c47b376057034f9ac4487f0979026eb39e3fa Mon Sep 17 00:00:00 2001 From: awskii Date: Wed, 29 Jul 2026 17:19:16 +0700 Subject: [PATCH 02/56] execution/commitment: add PBin bit-path type and DB key codec First step of the EIP-8297 binary commitment engine. pbinBitpath holds an up-to-528-bit tree key as big-endian words so divergence is XOR plus LeadingZeros64, clamped by the shorter path so bits past bitLen never leak into the result. pbinEncodeBitPath/pbinDecodeBitPath spell a path as packed MSB-first bits plus a trailing bitLen-mod-8 byte; the count trails so a subtree's records stay contiguous in the keyspace, and every non-canonical spelling is rejected on read so one path maps to exactly one DB key. Nothing outside the new files changes. --- docs/plans/20260729-pbin-patricia-hashed.md | 24 +- execution/commitment/pbin_bitpath.go | 205 +++++++++++++++++ execution/commitment/pbin_bitpath_test.go | 241 ++++++++++++++++++++ 3 files changed, 458 insertions(+), 12 deletions(-) create mode 100644 execution/commitment/pbin_bitpath.go create mode 100644 execution/commitment/pbin_bitpath_test.go diff --git a/docs/plans/20260729-pbin-patricia-hashed.md b/docs/plans/20260729-pbin-patricia-hashed.md index 16dbc29edbe..f3fd1c28bc3 100644 --- a/docs/plans/20260729-pbin-patricia-hashed.md +++ b/docs/plans/20260729-pbin-patricia-hashed.md @@ -92,15 +92,15 @@ empty tree = [0x00] * 32 ## Technical Details -**bitpath** (`pbin_bitpath.go`) +**bitpath** (`pbin_bitpath.go`) — named `pbinBitpath` per the `pbin` prefix rule, which wins over this sketch. ```go -type bitpath struct { +type pbinBitpath struct { w [9]uint64 // big-endian words; byte order == descent order bitLen int16 // 0..528 } -func (p *bitpath) bit(d int16) uint64 -func (p *bitpath) maskTail() -func pbinCommonPrefixBits(a, b *bitpath) int16 // XOR + LeadingZeros64, clamped by min(aLen,bLen) +func (p *pbinBitpath) bit(d int16) uint64 +func (p *pbinBitpath) maskTail() +func pbinCommonPrefixBits(a, b *pbinBitpath) int16 // XOR + LeadingZeros64, clamped by min(aLen,bLen) ``` **Values** (`pbin_values.go`) — BASIC_DATA is 32 bytes (`eip:332-339`): `version(1) || reserved(3) || code_size(4) || nonce(8) || balance(16)`, big-endian. `Update.Balance` is a `uint256.Int` (`commitment.go:2187`) but the EIP field is 16 bytes, so balances `>= 2^128` **error** rather than truncate. Storage values are left-padded to exactly 32 bytes (`eip:132`). @@ -120,13 +120,13 @@ func pbinCommonPrefixBits(a, b *bitpath) int16 // XOR + LeadingZeros64, clamped - Create: `execution/commitment/pbin_bitpath.go` - Create: `execution/commitment/pbin_bitpath_test.go` -- [ ] write failing tests for `pbinCommonPrefixBits` at bit lengths 271, 272, 273, 527, 528; a case seeding `w[]` with `0xFF` beyond `bitLen`; and a 272-bit path that is a bitwise prefix of a 528-bit path asserting the result is 272 (guards H10) -- [ ] write a failing fuzz test for codec round-trip across every bit length 0..528, plus explicit non-canonical-padding rejection cases (guards H5) -- [ ] write a failing unit test asserting no valid encoding equals the literal `"state"` (`0x7374617465`) -- [ ] implement `bitpath` with `[9]uint64` words, `bitLen int16`, `MaxPathBits = 528`, and `bit`/`slice`/`append`/`hasPrefix`/`maskTail` -- [ ] implement `pbinCommonPrefixBits` using XOR + `bits.LeadingZeros64`, clamped by `min(aLen, bLen)` -- [ ] implement `pbinEncodeBitPath`/`pbinDecodeBitPath` as `packBitsMSBFirst(path) || byte(bitLen mod 8)`, rejecting non-canonical padding on read; `bitLen == 0` encodes to a single `0x00` -- [ ] run tests - must pass before task 2 +- [x] write failing tests for `pbinCommonPrefixBits` at bit lengths 271, 272, 273, 527, 528; a case seeding `w[]` with `0xFF` beyond `bitLen`; and a 272-bit path that is a bitwise prefix of a 528-bit path asserting the result is 272 (guards H10) +- [x] write a failing fuzz test for codec round-trip across every bit length 0..528, plus explicit non-canonical-padding rejection cases (guards H5) +- [x] write a failing unit test asserting no valid encoding equals the literal `"state"` (`0x7374617465`) +- [x] implement `bitpath` with `[9]uint64` words, `bitLen int16`, `MaxPathBits = 528`, and `bit`/`slice`/`append`/`hasPrefix`/`maskTail` — as `pbinBitpath`/`pbinMaxPathBits` per the naming rule +- [x] implement `pbinCommonPrefixBits` using XOR + `bits.LeadingZeros64`, clamped by `min(aLen, bLen)` +- [x] implement `pbinEncodeBitPath`/`pbinDecodeBitPath` as `packBitsMSBFirst(path) || byte(bitLen mod 8)`, rejecting non-canonical padding on read; `bitLen == 0` encodes to a single `0x00` +- [x] run tests - must pass before task 2 ### Task 2: EIP-8297 tree key derivation and zone routing diff --git a/execution/commitment/pbin_bitpath.go b/execution/commitment/pbin_bitpath.go new file mode 100644 index 00000000000..6d1606aabc4 --- /dev/null +++ b/execution/commitment/pbin_bitpath.go @@ -0,0 +1,205 @@ +// Copyright 2026 The Erigon Authors +// This file is part of Erigon. +// +// Erigon is free software: you can redistribute it and/or modify +// it under the terms of the GNU Lesser General Public License as published by +// the Free Software Foundation, either version 3 of the License, or +// (at your option) any later version. +// +// Erigon is distributed in the hope that it will be useful, +// but WITHOUT ANY WARRANTY; without even the implied warranty of +// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +// GNU Lesser General Public License for more details. +// +// You should have received a copy of the GNU Lesser General Public License +// along with Erigon. If not, see . + +package commitment + +import ( + "errors" + "fmt" + "math/bits" +) + +const ( + // pbinMaxPathBits is the longest EIP-8297 tree key: 66 bytes for a storage leaf. + pbinMaxPathBits = 528 + pbinPathWords = (pbinMaxPathBits + 63) / 64 +) + +// pbinBitpath is a path of up to 528 bits through the binary tree, held as +// big-endian words so that word order equals descent order and divergence is a +// XOR plus LeadingZeros64. Bits at or past bitLen are not part of the path and +// may hold anything; every reader either clamps by bitLen or masks first. +type pbinBitpath struct { + w [pbinPathWords]uint64 + bitLen int16 +} + +func pbinPathFromBytes(b []byte) pbinBitpath { + return pbinPathFromBits(b, int16(len(b)*8)) +} + +func pbinPathFromBits(b []byte, bitLen int16) pbinBitpath { + if bitLen < 0 || bitLen > pbinMaxPathBits { + panic(fmt.Sprintf("pbin: bit length %d out of range", bitLen)) + } + var p pbinBitpath + n := min((int(bitLen)+7)/8, len(b)) + for i := range n { + p.w[i/8] |= uint64(b[i]) << (56 - 8*uint(i%8)) + } + p.bitLen = bitLen + p.maskTail() + return p +} + +func (p *pbinBitpath) bit(d int16) uint64 { + if d < 0 || d >= p.bitLen { + panic(fmt.Sprintf("pbin: bit %d out of range for %d-bit path", d, p.bitLen)) + } + return (p.w[d/64] >> (63 - uint(d%64))) & 1 +} + +func (p *pbinBitpath) setBitAt(d int16, v uint64) { + if d < 0 || d >= pbinMaxPathBits { + panic(fmt.Sprintf("pbin: bit %d out of range", d)) + } + m := uint64(1) << (63 - uint(d%64)) + if v != 0 { + p.w[d/64] |= m + } else { + p.w[d/64] &^= m + } +} + +func (p *pbinBitpath) maskTail() { + wi, off := int(p.bitLen)/64, uint(p.bitLen)%64 + if off == 0 { + p.w[wi] = 0 + } else { + p.w[wi] &= ^uint64(0) << (64 - off) + } + for i := wi + 1; i < pbinPathWords; i++ { + p.w[i] = 0 + } +} + +func (p *pbinBitpath) truncate(bitLen int16) { + if bitLen < 0 || bitLen > p.bitLen { + panic(fmt.Sprintf("pbin: cannot truncate %d-bit path to %d bits", p.bitLen, bitLen)) + } + p.bitLen = bitLen + p.maskTail() +} + +func (p *pbinBitpath) slice(from, to int16) pbinBitpath { + if from < 0 || to < from || to > p.bitLen { + panic(fmt.Sprintf("pbin: slice [%d,%d) out of range for %d-bit path", from, to, p.bitLen)) + } + var r pbinBitpath + for i := from; i < to; i++ { + r.setBitAt(i-from, p.bit(i)) + } + r.bitLen = to - from + return r +} + +func (p *pbinBitpath) appendBit(v uint64) { + p.setBitAt(p.bitLen, v) + p.bitLen++ +} + +func (p *pbinBitpath) append(o *pbinBitpath) { + if int(p.bitLen)+int(o.bitLen) > pbinMaxPathBits { + panic(fmt.Sprintf("pbin: appending %d bits to %d-bit path overflows", o.bitLen, p.bitLen)) + } + for i := int16(0); i < o.bitLen; i++ { + p.setBitAt(p.bitLen+i, o.bit(i)) + } + p.bitLen += o.bitLen +} + +func (p *pbinBitpath) hasPrefix(o *pbinBitpath) bool { + return o.bitLen <= p.bitLen && pbinCommonPrefixBits(p, o) == o.bitLen +} + +// pbinCommonPrefixBits reports how many leading bits a and b share, never more +// than the shorter path holds. +func pbinCommonPrefixBits(a, b *pbinBitpath) int16 { + limit := min(a.bitLen, b.bitLen) + n := int16(0) + for i := 0; i < pbinPathWords && n < limit; i++ { + if x := a.w[i] ^ b.w[i]; x != 0 { + n += int16(bits.LeadingZeros64(x)) + break + } + n += 64 + } + return min(n, limit) +} + +// pbinAppendPackedBits appends the path's bits MSB-first, zero-padded to a byte +// boundary. +func (p *pbinBitpath) appendPackedBits(dst []byte) []byte { + for i := range (int(p.bitLen) + 7) / 8 { + dst = append(dst, byte(p.w[i/8]>>(56-8*uint(i%8)))) + } + if used := p.bitLen % 8; used != 0 { + dst[len(dst)-1] &= ^byte(0) << (8 - uint(used)) + } + return dst +} + +var ( + errPBinEmptyBitPath = errors.New("pbin: empty bit-path key") + errPBinNonCanonicalPad = errors.New("pbin: non-canonical padding in bit-path key") +) + +// pbinAppendBitPath appends the DB key for p: packed bits followed by a single +// byte holding bitLen mod 8. The trailing count is a suffix on purpose — a +// leading length field would scatter one subtree's records across the keyspace, +// whereas this layout keeps a subtree contiguous. It does not order ancestors +// before descendants, and callers must not assume it does. +func pbinAppendBitPath(dst []byte, p *pbinBitpath) []byte { + return append(p.appendPackedBits(dst), byte(p.bitLen%8)) +} + +func pbinEncodeBitPath(p *pbinBitpath) []byte { + return pbinAppendBitPath(make([]byte, 0, (int(p.bitLen)+7)/8+1), p) +} + +// pbinDecodeBitPath is the inverse of pbinAppendBitPath and rejects every +// non-canonical spelling, so one path has exactly one DB key. +func pbinDecodeBitPath(buf []byte) (pbinBitpath, error) { + var p pbinBitpath + if len(buf) == 0 { + return p, errPBinEmptyBitPath + } + tailBits, packed := buf[len(buf)-1], buf[:len(buf)-1] + if tailBits > 7 { + return p, fmt.Errorf("pbin: invalid trailing bit count %d in bit-path key", tailBits) + } + bitLen := len(packed) * 8 + if tailBits != 0 { + if len(packed) == 0 { + return p, fmt.Errorf("pbin: trailing bit count %d with no payload", tailBits) + } + bitLen = bitLen - 8 + int(tailBits) + } + if bitLen > pbinMaxPathBits { + return p, fmt.Errorf("pbin: bit path of %d bits exceeds %d", bitLen, pbinMaxPathBits) + } + for i, b := range packed { + p.w[i/8] |= uint64(b) << (56 - 8*uint(i%8)) + } + p.bitLen = int16(bitLen) + + masked := p + masked.maskTail() + if masked.w != p.w { + return pbinBitpath{}, errPBinNonCanonicalPad + } + return p, nil +} diff --git a/execution/commitment/pbin_bitpath_test.go b/execution/commitment/pbin_bitpath_test.go new file mode 100644 index 00000000000..685f76e06d2 --- /dev/null +++ b/execution/commitment/pbin_bitpath_test.go @@ -0,0 +1,241 @@ +// Copyright 2026 The Erigon Authors +// This file is part of Erigon. +// +// Erigon is free software: you can redistribute it and/or modify +// it under the terms of the GNU Lesser General Public License as published by +// the Free Software Foundation, either version 3 of the License, or +// (at your option) any later version. +// +// Erigon is distributed in the hope that it will be useful, +// but WITHOUT ANY WARRANTY; without even the implied warranty of +// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +// GNU Lesser General Public License for more details. +// +// You should have received a copy of the GNU Lesser General Public License +// along with Erigon. If not, see . + +package commitment + +import ( + "bytes" + "testing" + + "github.com/stretchr/testify/require" +) + +func pbinTestPath(t *testing.T, pattern byte, bitLen int16) pbinBitpath { + t.Helper() + p := pbinPathFromBits(bytes.Repeat([]byte{pattern}, 66), bitLen) + require.Equal(t, bitLen, p.bitLen) + return p +} + +func pbinFlipBit(p pbinBitpath, at int16) pbinBitpath { + p.setBitAt(at, p.bit(at)^1) + return p +} + +func TestPBinCommonPrefixBits(t *testing.T) { + t.Parallel() + + for _, tc := range []struct { + name string + aLen int16 + bLen int16 + flipAt int16 // -1: no divergence + want int16 + }{ + {"equal-271", 271, 271, -1, 271}, + {"equal-272", 272, 272, -1, 272}, + {"equal-273", 273, 273, -1, 273}, + {"equal-527", 527, 527, -1, 527}, + {"equal-528", 528, 528, -1, 528}, + {"diff-at-0", 528, 528, 0, 0}, + {"diff-at-63", 528, 528, 63, 63}, + {"diff-at-64", 528, 528, 64, 64}, + {"diff-at-270-len-271", 271, 271, 270, 270}, + {"diff-at-271-len-272", 272, 272, 271, 271}, + {"diff-at-272-len-273", 273, 273, 272, 272}, + {"diff-at-526-len-527", 527, 527, 526, 526}, + {"diff-at-527-len-528", 528, 528, 527, 527}, + } { + t.Run(tc.name, func(t *testing.T) { + a := pbinTestPath(t, 0xA5, tc.aLen) + b := pbinTestPath(t, 0xA5, tc.bLen) + if tc.flipAt >= 0 { + b = pbinFlipBit(b, tc.flipAt) + } + require.Equal(t, tc.want, pbinCommonPrefixBits(&a, &b)) + require.Equal(t, tc.want, pbinCommonPrefixBits(&b, &a)) + }) + } +} + +// A 272-bit account key that is a bitwise prefix of a 528-bit storage key must +// report exactly 272 shared bits: without clamping by min(aLen, bLen) the words +// keep agreeing past the shorter path's end (guards H10). +func TestPBinCommonPrefixBits_ShorterPathIsPrefix(t *testing.T) { + t.Parallel() + + long := pbinTestPath(t, 0xAA, 528) + short := pbinTestPath(t, 0xAA, 272) + + require.Equal(t, int16(272), pbinCommonPrefixBits(&short, &long)) + require.Equal(t, int16(272), pbinCommonPrefixBits(&long, &short)) +} + +// Words carrying set bits beyond bitLen must not be read as real path bits +// (guards H10). +func TestPBinCommonPrefixBits_IgnoresBitsBeyondBitLen(t *testing.T) { + t.Parallel() + + long := pbinTestPath(t, 0xAA, 528) + + dirty := pbinTestPath(t, 0xAA, 272) + dirty.w[4] |= 0x0000FFFFFFFFFFFF // bits 272..319 + for i := 5; i < pbinPathWords; i++ { + dirty.w[i] = ^uint64(0) + } + + require.Equal(t, int16(272), pbinCommonPrefixBits(&dirty, &long)) + require.Equal(t, int16(272), pbinCommonPrefixBits(&long, &dirty)) + + clean := pbinTestPath(t, 0xAA, 272) + dirty.maskTail() + require.Equal(t, clean.w, dirty.w) +} + +func TestPBinBitpathAccessors(t *testing.T) { + t.Parallel() + + p := pbinPathFromBytes([]byte{0b10110001, 0b01000000}) + require.Equal(t, int16(16), p.bitLen) + for i, want := range []uint64{1, 0, 1, 1, 0, 0, 0, 1, 0, 1, 0, 0, 0, 0, 0, 0} { + require.Equalf(t, want, p.bit(int16(i)), "bit %d", i) + } + + mid := p.slice(3, 11) + require.Equal(t, int16(8), mid.bitLen) + require.Equal(t, pbinPathFromBytes([]byte{0b10001010}), mid) + + head, tail := p.slice(0, 3), p.slice(11, 16) + head.append(&mid) + head.append(&tail) + require.Equal(t, p, head) + + empty, short := p.slice(0, 0), p.slice(0, 7) + require.True(t, p.hasPrefix(&empty)) + require.True(t, p.hasPrefix(&short)) + require.True(t, p.hasPrefix(&p)) + + flipped := pbinFlipBit(p, 5) + other := flipped.slice(0, 7) + require.False(t, p.hasPrefix(&other)) + require.False(t, short.hasPrefix(&p)) + + var appended pbinBitpath + for i := int16(0); i < p.bitLen; i++ { + appended.appendBit(p.bit(i)) + } + require.Equal(t, p, appended) + + truncated := p + truncated.truncate(4) + require.Equal(t, pbinPathFromBits([]byte{0b10110000}, 4), truncated) +} + +func TestPBinBitPathCodecRoundTrip(t *testing.T) { + t.Parallel() + + src := make([]byte, 66) + for i := range src { + src[i] = byte(i*7 + 1) + } + + for bitLen := int16(0); bitLen <= pbinMaxPathBits; bitLen++ { + p := pbinPathFromBits(src, bitLen) + enc := pbinEncodeBitPath(&p) + require.Equalf(t, (int(bitLen)+7)/8+1, len(enc), "bitLen %d", bitLen) + require.LessOrEqual(t, len(enc), 67) + + got, err := pbinDecodeBitPath(enc) + require.NoErrorf(t, err, "bitLen %d", bitLen) + require.Equalf(t, p, got, "bitLen %d", bitLen) + } +} + +func TestPBinBitPathCodecEmpty(t *testing.T) { + t.Parallel() + + var empty pbinBitpath + require.Equal(t, []byte{0x00}, pbinEncodeBitPath(&empty)) + + got, err := pbinDecodeBitPath([]byte{0x00}) + require.NoError(t, err) + require.Equal(t, empty, got) +} + +func TestPBinBitPathCodecRejects(t *testing.T) { + t.Parallel() + + for _, tc := range []struct { + name string + buf []byte + }{ + {"empty-key", nil}, + {"tail-count-out-of-range", []byte{0xE0, 0x08}}, + {"tail-count-is-a-byte", []byte{0xE0, 0xFF}}, + {"tail-count-without-payload", []byte{0x05}}, + {"non-canonical-pad", []byte{0xFF, 0x03}}, + {"non-canonical-pad-single-bit", []byte{0x40, 0x01}}, + {"too-long", append(bytes.Repeat([]byte{0xAA}, 67), 0x00)}, + } { + t.Run(tc.name, func(t *testing.T) { + _, err := pbinDecodeBitPath(tc.buf) + require.Error(t, err) + }) + } + + got, err := pbinDecodeBitPath([]byte{0xE0, 0x03}) + require.NoError(t, err) + require.Equal(t, pbinPathFromBits([]byte{0xE0}, 3), got) +} + +// The commitment domain stores its state blob under the literal key "state", so +// no encoded bit path may collide with it (guards H5). +func TestPBinBitPathNeverEncodesToStateKey(t *testing.T) { + t.Parallel() + + _, err := pbinDecodeBitPath(KeyCommitmentState) + require.Error(t, err) + + src := bytes.Repeat([]byte{0x74}, 66) + for bitLen := int16(0); bitLen <= pbinMaxPathBits; bitLen++ { + p := pbinPathFromBits(src, bitLen) + require.NotEqualf(t, KeyCommitmentState, pbinEncodeBitPath(&p), "bitLen %d", bitLen) + } +} + +func FuzzPBinBitPathCodec(f *testing.F) { + f.Add([]byte{}, uint16(0)) + f.Add([]byte{0x00}, uint16(1)) + f.Add(bytes.Repeat([]byte{0xFF}, 66), uint16(528)) + f.Add(bytes.Repeat([]byte{0xA5}, 34), uint16(272)) + f.Add([]byte{0xFF, 0x03}, uint16(3)) + + f.Fuzz(func(t *testing.T, data []byte, n uint16) { + bitLen := int16(int(n) % (pbinMaxPathBits + 1)) + p := pbinPathFromBits(data, bitLen) + + enc := pbinEncodeBitPath(&p) + got, err := pbinDecodeBitPath(enc) + require.NoError(t, err) + require.Equal(t, p, got) + + // Decoding is total and canonical: anything that decodes must re-encode + // to the very bytes it came from, so one bit path has one DB key. + if q, err := pbinDecodeBitPath(data); err == nil { + require.Equal(t, data, pbinEncodeBitPath(&q)) + } + }) +} From d6b2de63cd41ecf8de136d0ac295eba45c612ce9 Mon Sep 17 00:00:00 2001 From: awskii Date: Wed, 29 Jul 2026 17:26:13 +0700 Subject: [PATCH 03/56] feat: EIP-8297 tree key derivation and zone routing Add pbinAddr32, pbinTreeKeyAccount and pbinTreeKeyStorage deriving the 34-byte account-zone and 66-byte storage-zone keys, with slots below 64 routed into the account header at HEADER_STORAGE_OFFSET+slot. A two-level digest cache memoizes key_hash(addr32) per address and key_hash(addr32||tree_index) per 256-slot group; pbinKeyHasher exposes the primary leaf's key as a keyHasher, leaving Updates untouched. Vectors are pinned against x/crypto keccak computed in the test body, not against the helper under test. --- docs/plans/20260729-pbin-patricia-hashed.md | 12 +- execution/commitment/pbin_keys.go | 188 +++++++++++++++ execution/commitment/pbin_keys_test.go | 245 ++++++++++++++++++++ 3 files changed, 439 insertions(+), 6 deletions(-) create mode 100644 execution/commitment/pbin_keys.go create mode 100644 execution/commitment/pbin_keys_test.go diff --git a/docs/plans/20260729-pbin-patricia-hashed.md b/docs/plans/20260729-pbin-patricia-hashed.md index f3fd1c28bc3..b5e37ebb3ad 100644 --- a/docs/plans/20260729-pbin-patricia-hashed.md +++ b/docs/plans/20260729-pbin-patricia-hashed.md @@ -134,12 +134,12 @@ func pbinCommonPrefixBits(a, b *pbinBitpath) int16 // XOR + LeadingZeros64, clam - Create: `execution/commitment/pbin_keys.go` - Create: `execution/commitment/pbin_keys_test.go` -- [ ] write failing tests reproducing the EIP's vectors (`eip:583-630`), each asserting the **full** 34/66-byte key against a `keccak` computed inline in the test body rather than via the helper under test: BASIC_DATA key; slot 5 → sub-index `0x45`; slot 1000 → `tree_index 3`/`sub_index 0xE8` with `tree_index` as 32-byte big-endian -- [ ] write failing zone-routing tests at slots 63/64/255/256 and for the 12-byte address padding (guards H8) -- [ ] implement `pbinAddr32`, `pbinTreeKeyAccount(addr, subIdx)`, `pbinTreeKeyStorage(addr, slot)` with the `slot < 64` account-zone route -- [ ] implement the two-level digest cache: `H(addr32)` per address, `H(addr32||tree_index)` per 256-slot group, with `tree_index` encoded as 32-byte big-endian -- [ ] provide a `keyHasher`-compatible `func([]byte) []byte` returning the primary leaf's tree key, and assert `len` is 34 or 66 at every construction site -- [ ] run tests - must pass before task 3 +- [x] write failing tests reproducing the EIP's vectors (`eip:583-630`), each asserting the **full** 34/66-byte key against a `keccak` computed inline in the test body rather than via the helper under test: BASIC_DATA key; slot 5 → sub-index `0x45`; slot 1000 → `tree_index 3`/`sub_index 0xE8` with `tree_index` as 32-byte big-endian +- [x] write failing zone-routing tests at slots 63/64/255/256 and for the 12-byte address padding (guards H8) +- [x] implement `pbinAddr32`, `pbinTreeKeyAccount(addr, subIdx)`, `pbinTreeKeyStorage(addr, slot)` with the `slot < 64` account-zone route +- [x] implement the two-level digest cache: `H(addr32)` per address, `H(addr32||tree_index)` per 256-slot group, with `tree_index` encoded as 32-byte big-endian +- [x] provide a `keyHasher`-compatible `func([]byte) []byte` returning the primary leaf's tree key, and assert `len` is 34 or 66 at every construction site +- [x] run tests - must pass before task 3 ### Task 3: Leaf value encoding diff --git a/execution/commitment/pbin_keys.go b/execution/commitment/pbin_keys.go new file mode 100644 index 00000000000..3e0bfffdb66 --- /dev/null +++ b/execution/commitment/pbin_keys.go @@ -0,0 +1,188 @@ +// Copyright 2026 The Erigon Authors +// This file is part of Erigon. +// +// Erigon is free software: you can redistribute it and/or modify +// it under the terms of the GNU Lesser General Public License as published by +// the Free Software Foundation, either version 3 of the License, or +// (at your option) any later version. +// +// Erigon is distributed in the hope that it will be useful, +// but WITHOUT ANY WARRANTY; without even the implied warranty of +// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +// GNU Lesser General Public License for more details. +// +// You should have received a copy of the GNU Lesser General Public License +// along with Erigon. If not, see . + +package commitment + +import ( + "fmt" + + keccak "github.com/erigontech/fastkeccak" + + "github.com/erigontech/erigon/common/length" +) + +// EIP-8297 embedding constants (eip:261-278). +const ( + pbinBasicDataLeafKey = 0 + pbinCodeHashLeafKey = 1 + pbinHeaderStorageOffset = 64 + pbinCodeOffset = 128 + + pbinAccountZone = 0x00 + pbinStorageZone = 0xFF + + pbinAccountKeyLength = 34 + pbinStorageKeyLength = 66 +) + +// pbinAddr32 widens a legacy address to the spec's Address32 by left-padding +// with zero bytes (eip:291-296). +func pbinAddr32(addr []byte) [32]byte { + if len(addr) > 32 { + panic(fmt.Sprintf("pbin: address of %d bytes exceeds 32", len(addr))) + } + var a32 [32]byte + copy(a32[32-len(addr):], addr) + return a32 +} + +// pbinTreeKey assembles zone || treePosition || subIndex and asserts the length +// fixed for that zone. The assert is load-bearing: one length per zone is what +// keeps keys prefix-free within a zone (eip:283-288). +func pbinTreeKey(zone byte, treePosition []byte, subIndex byte) []byte { + key := make([]byte, 0, len(treePosition)+2) + key = append(key, zone) + key = append(key, treePosition...) + key = append(key, subIndex) + + want := pbinAccountKeyLength + if zone == pbinStorageZone { + want = pbinStorageKeyLength + } + if len(key) != want { + panic(fmt.Sprintf("pbin: zone %#x key of %d bytes, want %d", zone, len(key), want)) + } + return key +} + +// pbinTreeKeyAccount returns the account-header key at subIndex (eip:311-320). +func pbinTreeKeyAccount(addr []byte, subIndex byte) []byte { + var c pbinDigestCache + return c.accountKey(addr, subIndex) +} + +// pbinTreeKeyStorage returns the key for a storage slot, routing slots below 64 +// into the account header and the rest into the storage zone (eip:415-437). +// slot is big-endian and at most 32 bytes. +func pbinTreeKeyStorage(addr, slot []byte) []byte { + var c pbinDigestCache + return c.storageKey(addr, slot) +} + +// pbinKeyHasher returns a keyHasher deriving the primary leaf's tree key: +// BASIC_DATA for an account, the slot's own leaf for storage. The CODE_HASH +// sibling shares the stem and is written by the engine during the same visit, +// so it needs no key of its own here. +func pbinKeyHasher() keyHasher { + var c pbinDigestCache + return c.treeKey +} + +// pbinDigestCache memoizes the two hash-derived key components across a run of +// keys: key_hash(addr32) per address and key_hash(addr32||tree_index) per +// 256-slot storage group. Both digests are immutable, so a hit is always +// correct; changing address invalidates the group entry, which is bound to the +// address as well as the index (eip:411-414). +type pbinDigestCache struct { + addr32 [32]byte + stem [32]byte + valid bool + + groupIndex [32]byte + groupHash [32]byte + groupValid bool + + buf [64]byte +} + +func (c *pbinDigestCache) stemDigest(addr32 *[32]byte) *[32]byte { + if c.valid && c.addr32 == *addr32 { + return &c.stem + } + c.stem = keccak.Sum256(addr32[:]) + c.addr32 = *addr32 + c.valid = true + c.groupValid = false + return &c.stem +} + +func (c *pbinDigestCache) groupDigest(addr32, treeIndex *[32]byte) *[32]byte { + if c.groupValid && c.addr32 == *addr32 && c.groupIndex == *treeIndex { + return &c.groupHash + } + copy(c.buf[:32], addr32[:]) + copy(c.buf[32:], treeIndex[:]) + c.groupHash = keccak.Sum256(c.buf[:]) + c.groupIndex = *treeIndex + c.groupValid = true + return &c.groupHash +} + +func (c *pbinDigestCache) accountKey(addr []byte, subIndex byte) []byte { + addr32 := pbinAddr32(addr) + return pbinTreeKey(pbinAccountZone, c.stemDigest(&addr32)[:], subIndex) +} + +func (c *pbinDigestCache) storageKey(addr, slot []byte) []byte { + addr32 := pbinAddr32(addr) + slot32 := pbinSlot32(slot) + if pbinSlotInHeader(&slot32) { + return pbinTreeKey(pbinAccountZone, c.stemDigest(&addr32)[:], pbinHeaderStorageOffset+slot32[31]) + } + treeIndex, subIndex := pbinSplitSlot(&slot32) + + var position [64]byte + copy(position[:32], c.stemDigest(&addr32)[:]) + copy(position[32:], c.groupDigest(&addr32, &treeIndex)[:]) + return pbinTreeKey(pbinStorageZone, position[:], subIndex) +} + +func (c *pbinDigestCache) treeKey(plainKey []byte) []byte { + switch len(plainKey) { + case length.Addr: + return c.accountKey(plainKey, pbinBasicDataLeafKey) + case length.Addr + length.Hash: + return c.storageKey(plainKey[:length.Addr], plainKey[length.Addr:]) + default: + panic(fmt.Sprintf("pbin: plain key of %d bytes is neither an account nor a storage key", len(plainKey))) + } +} + +func pbinSlot32(slot []byte) [32]byte { + if len(slot) > 32 { + panic(fmt.Sprintf("pbin: storage slot of %d bytes exceeds 32", len(slot))) + } + var s32 [32]byte + copy(s32[32-len(slot):], slot) + return s32 +} + +func pbinSlotInHeader(slot *[32]byte) bool { + for _, b := range slot[:31] { + if b != 0 { + return false + } + } + return slot[31] < pbinCodeOffset-pbinHeaderStorageOffset +} + +// pbinSplitSlot divides a slot into its storage group and position within it. +// STEM_SUBTREE_WIDTH is 256, so the division is a one-byte shift and the +// sub-index is the raw low byte — which is what co-locates adjacent slots. +func pbinSplitSlot(slot *[32]byte) (treeIndex [32]byte, subIndex byte) { + copy(treeIndex[1:], slot[:31]) + return treeIndex, slot[31] +} diff --git a/execution/commitment/pbin_keys_test.go b/execution/commitment/pbin_keys_test.go new file mode 100644 index 00000000000..5f7b809ae73 --- /dev/null +++ b/execution/commitment/pbin_keys_test.go @@ -0,0 +1,245 @@ +// Copyright 2026 The Erigon Authors +// This file is part of Erigon. +// +// Erigon is free software: you can redistribute it and/or modify +// it under the terms of the GNU Lesser General Public License as published by +// the Free Software Foundation, either version 3 of the License, or +// (at your option) any later version. +// +// Erigon is distributed in the hope that it will be useful, +// but WITHOUT ANY WARRANTY; without even the implied warranty of +// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +// GNU Lesser General Public License for more details. +// +// You should have received a copy of the GNU Lesser General Public License +// along with Erigon. If not, see . + +package commitment + +import ( + "encoding/binary" + "encoding/hex" + "testing" + + "github.com/stretchr/testify/require" + "golang.org/x/crypto/sha3" +) + +// pbinTestKeccak is an independent Keccak-256 (x/crypto, not the fastkeccak the +// engine uses) so the vectors below are pinned against the spec rather than +// against the helper under test. +func pbinTestKeccak(t *testing.T, parts ...[]byte) []byte { + t.Helper() + h := sha3.NewLegacyKeccak256() + for _, p := range parts { + _, err := h.Write(p) + require.NoError(t, err) + } + return h.Sum(nil) +} + +func pbinTestAddr(t *testing.T, s string) []byte { + t.Helper() + b, err := hex.DecodeString(s) + require.NoError(t, err) + require.Len(t, b, 20) + return b +} + +// pbinTestAddress32 is the spec's address20_to_address32 (eip:291-296). +func pbinTestAddress32(addr []byte) []byte { + a := make([]byte, 32) + copy(a[32-len(addr):], addr) + return a +} + +func pbinTestBE32(v uint64) []byte { + b := make([]byte, 32) + binary.BigEndian.PutUint64(b[24:], v) + return b +} + +func pbinTestSlot(v uint64) []byte { return pbinTestBE32(v) } + +func pbinTestConcat(parts ...[]byte) []byte { + var out []byte + for _, p := range parts { + out = append(out, p...) + } + return out +} + +// TestPBinTreeKeyEIPVectors pins the derivation against the spec's test cases +// (eip:583-630). +func TestPBinTreeKeyEIPVectors(t *testing.T) { + t.Parallel() + + addr := pbinTestAddr(t, "0102030405060708090a0b0c0d0e0f1011121314") + addr32 := pbinTestAddress32(addr) + stem := pbinTestKeccak(t, addr32) + + t.Run("basic-data", func(t *testing.T) { + got := pbinTreeKeyAccount(addr, pbinBasicDataLeafKey) + require.Len(t, got, pbinAccountKeyLength) + require.Equal(t, pbinTestConcat([]byte{0x00}, stem, []byte{0x00}), got) + }) + + t.Run("code-hash", func(t *testing.T) { + got := pbinTreeKeyAccount(addr, pbinCodeHashLeafKey) + require.Len(t, got, pbinAccountKeyLength) + require.Equal(t, pbinTestConcat([]byte{0x00}, stem, []byte{0x01}), got) + }) + + t.Run("slot-5-in-header", func(t *testing.T) { + got := pbinTreeKeyStorage(addr, pbinTestSlot(5)) + require.Len(t, got, pbinAccountKeyLength) + require.Equal(t, pbinTestConcat([]byte{0x00}, stem, []byte{0x45}), got) + }) + + t.Run("slot-1000-in-storage-zone", func(t *testing.T) { + suffix := pbinTestKeccak(t, addr32, pbinTestBE32(3)) + got := pbinTreeKeyStorage(addr, pbinTestSlot(1000)) + require.Len(t, got, pbinStorageKeyLength) + require.Equal(t, pbinTestConcat([]byte{0xFF}, stem, suffix, []byte{0xE8}), got) + }) +} + +// TestPBinStorageZoneRouting walks the header/storage-zone boundary and the +// group boundary, where a mis-route stays internally consistent and so is +// invisible to a root-equality test (guards H8). +func TestPBinStorageZoneRouting(t *testing.T) { + t.Parallel() + + addr := pbinTestAddr(t, "cafebabe000000000000000000000000deadbeef") + addr32 := pbinTestAddress32(addr) + stem := pbinTestKeccak(t, addr32) + + for _, tc := range []struct { + name string + slot uint64 + treeIndex uint64 // storage zone only + subIndex byte + inHeader bool + }{ + {name: "slot-0", slot: 0, subIndex: 64, inHeader: true}, + {name: "slot-63-last-in-header", slot: 63, subIndex: 127, inHeader: true}, + {name: "slot-64-first-in-storage-zone", slot: 64, treeIndex: 0, subIndex: 64}, + {name: "slot-255-last-in-group-0", slot: 255, treeIndex: 0, subIndex: 255}, + {name: "slot-256-first-in-group-1", slot: 256, treeIndex: 1, subIndex: 0}, + {name: "slot-257", slot: 257, treeIndex: 1, subIndex: 1}, + } { + t.Run(tc.name, func(t *testing.T) { + got := pbinTreeKeyStorage(addr, pbinTestSlot(tc.slot)) + if tc.inHeader { + require.Len(t, got, pbinAccountKeyLength) + require.Equal(t, pbinTestConcat([]byte{0x00}, stem, []byte{tc.subIndex}), got) + return + } + suffix := pbinTestKeccak(t, addr32, pbinTestBE32(tc.treeIndex)) + require.Len(t, got, pbinStorageKeyLength) + require.Equal(t, pbinTestConcat([]byte{0xFF}, stem, suffix, []byte{tc.subIndex}), got) + }) + } +} + +// TestPBinStorageZoneKeysAreDistinct guards against a routing bug that maps two +// slots onto one key, which a root-equality test cannot see either. +func TestPBinStorageZoneKeysAreDistinct(t *testing.T) { + t.Parallel() + + addr := pbinTestAddr(t, "cafebabe000000000000000000000000deadbeef") + seen := make(map[string]uint64) + for _, slot := range []uint64{0, 1, 62, 63, 64, 65, 254, 255, 256, 257, 511, 512, 1000} { + key := string(pbinTreeKeyStorage(addr, pbinTestSlot(slot))) + if prev, ok := seen[key]; ok { + t.Fatalf("slots %d and %d derive the same tree key", prev, slot) + } + seen[key] = slot + } +} + +// TestPBinHighSlotRouting covers slot numbers that do not fit a uint64, where +// the tree index is a 31-byte shift of the slot rather than arithmetic. +func TestPBinHighSlotRouting(t *testing.T) { + t.Parallel() + + addr := pbinTestAddr(t, "0102030405060708090a0b0c0d0e0f1011121314") + addr32 := pbinTestAddress32(addr) + stem := pbinTestKeccak(t, addr32) + + slot := make([]byte, 32) + for i := range slot { + slot[i] = byte(i + 1) + } + treeIndex := append([]byte{0x00}, slot[:31]...) + suffix := pbinTestKeccak(t, addr32, treeIndex) + + got := pbinTreeKeyStorage(addr, slot) + require.Len(t, got, pbinStorageKeyLength) + require.Equal(t, pbinTestConcat([]byte{0xFF}, stem, suffix, []byte{slot[31]}), got) +} + +// TestPBinAddr32Padding pins that the stem digest covers the 32-byte address, +// not the 20-byte one (guards H8). +func TestPBinAddr32Padding(t *testing.T) { + t.Parallel() + + addr := pbinTestAddr(t, "0102030405060708090a0b0c0d0e0f1011121314") + a32 := pbinAddr32(addr) + require.Equal(t, make([]byte, 12), a32[:12]) + require.Equal(t, addr, a32[12:]) + + key := pbinTreeKeyAccount(addr, pbinBasicDataLeafKey) + require.Equal(t, pbinTestKeccak(t, pbinTestAddress32(addr)), key[1:33]) + require.NotEqual(t, pbinTestKeccak(t, addr), key[1:33]) +} + +// TestPBinKeyHasherPrimaryLeaf pins the keyHasher contract: the primary leaf's +// tree key, sized 34 or 66 by zone. +func TestPBinKeyHasherPrimaryLeaf(t *testing.T) { + t.Parallel() + + hasher := pbinKeyHasher() + addr := pbinTestAddr(t, "0102030405060708090a0b0c0d0e0f1011121314") + + got := hasher(addr) + require.Len(t, got, pbinAccountKeyLength) + require.Equal(t, pbinTreeKeyAccount(addr, pbinBasicDataLeafKey), got) + + got = hasher(pbinTestConcat(addr, pbinTestSlot(1000))) + require.Len(t, got, pbinStorageKeyLength) + require.Equal(t, pbinTreeKeyStorage(addr, pbinTestSlot(1000)), got) +} + +func TestPBinKeyHasherRejectsMalformedPlainKey(t *testing.T) { + t.Parallel() + + hasher := pbinKeyHasher() + require.Panics(t, func() { hasher(make([]byte, 33)) }) + require.Panics(t, func() { hasher(nil) }) +} + +// TestPBinDigestCacheMatchesFreshDerivation drives one hasher across interleaved +// addresses and slot groups: a cache entry kept past its address or tree index +// would silently place a leaf under the wrong stem. +func TestPBinDigestCacheMatchesFreshDerivation(t *testing.T) { + t.Parallel() + + addrs := [][]byte{ + pbinTestAddr(t, "0102030405060708090a0b0c0d0e0f1011121314"), + pbinTestAddr(t, "cafebabe000000000000000000000000deadbeef"), + } + slots := []uint64{0, 63, 64, 255, 256, 257, 1000, 100000} + + hasher := pbinKeyHasher() + for range 3 { + for _, addr := range addrs { + require.Equal(t, pbinTreeKeyAccount(addr, pbinBasicDataLeafKey), hasher(addr)) + for _, slot := range slots { + plainKey := pbinTestConcat(addr, pbinTestSlot(slot)) + require.Equal(t, pbinTreeKeyStorage(addr, pbinTestSlot(slot)), hasher(plainKey), + "addr %x slot %d", addr, slot) + } + } + } +} From e5b5e7b73096c4bf622fbc4c0ab5b32205b19a56 Mon Sep 17 00:00:00 2001 From: awskii Date: Wed, 29 Jul 2026 17:31:03 +0700 Subject: [PATCH 04/56] feat: EIP-8297 leaf value encoding BASIC_DATA packs version/code_size/nonce/balance big-endian into 32 bytes; a balance the 16-byte field cannot hold errors instead of truncating, which would commit a wrong root. CODE_HASH maps an unset hash to the empty-code hash. Storage values are left-padded to the fixed leaf width. Expectations are hand-written hex rather than the encoder's own output: the Task 4 oracle consumes this same encoder, so a differential root test is structurally blind to value-encoding bugs. --- docs/plans/20260729-pbin-patricia-hashed.md | 12 +- execution/commitment/pbin_values.go | 83 ++++++++ execution/commitment/pbin_values_test.go | 200 ++++++++++++++++++++ 3 files changed, 289 insertions(+), 6 deletions(-) create mode 100644 execution/commitment/pbin_values.go create mode 100644 execution/commitment/pbin_values_test.go diff --git a/docs/plans/20260729-pbin-patricia-hashed.md b/docs/plans/20260729-pbin-patricia-hashed.md index b5e37ebb3ad..93d649e3043 100644 --- a/docs/plans/20260729-pbin-patricia-hashed.md +++ b/docs/plans/20260729-pbin-patricia-hashed.md @@ -147,12 +147,12 @@ func pbinCommonPrefixBits(a, b *pbinBitpath) int16 // XOR + LeadingZeros64, clam - Create: `execution/commitment/pbin_values.go` - Create: `execution/commitment/pbin_values_test.go` -- [ ] write failing tests pinning BASIC_DATA byte offsets 0/4/8/16 against hand-written hex — **not** against the encoder, since the Task 4 oracle shares this encoder and cannot catch its bugs -- [ ] write a failing test asserting a balance `>= 2^128` returns an error rather than truncating -- [ ] write failing tests for the CODE_HASH leaf value and for storage values left-padded to exactly 32 bytes -- [ ] implement `pbinEncodeBasicData` per `eip:332-339`: `version(1) || reserved(3) || code_size(4) || nonce(8) || balance(16)` big-endian -- [ ] implement `pbinCodeHashValue` and `pbinEncodeStorageValue` -- [ ] run tests - must pass before task 4 +- [x] write failing tests pinning BASIC_DATA byte offsets 0/4/8/16 against hand-written hex — **not** against the encoder, since the Task 4 oracle shares this encoder and cannot catch its bugs +- [x] write a failing test asserting a balance `>= 2^128` returns an error rather than truncating +- [x] write failing tests for the CODE_HASH leaf value and for storage values left-padded to exactly 32 bytes +- [x] implement `pbinEncodeBasicData` per `eip:332-339`: `version(1) || reserved(3) || code_size(4) || nonce(8) || balance(16)` big-endian +- [x] implement `pbinCodeHashValue` and `pbinEncodeStorageValue` +- [x] run tests - must pass before task 4 ### Task 4: EIP reference oracle in the test package diff --git a/execution/commitment/pbin_values.go b/execution/commitment/pbin_values.go new file mode 100644 index 00000000000..35928bdd0d3 --- /dev/null +++ b/execution/commitment/pbin_values.go @@ -0,0 +1,83 @@ +// Copyright 2026 The Erigon Authors +// This file is part of Erigon. +// +// Erigon is free software: you can redistribute it and/or modify +// it under the terms of the GNU Lesser General Public License as published by +// the Free Software Foundation, either version 3 of the License, or +// (at your option) any later version. +// +// Erigon is distributed in the hope that it will be useful, +// but WITHOUT ANY WARRANTY; without even the implied warranty of +// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +// GNU Lesser General Public License for more details. +// +// You should have received a copy of the GNU Lesser General Public License +// along with Erigon. If not, see . + +package commitment + +import ( + "encoding/binary" + "errors" + "fmt" + + "github.com/holiman/uint256" + + "github.com/erigontech/erigon/common" + "github.com/erigontech/erigon/common/empty" + "github.com/erigontech/erigon/common/length" +) + +// pbinValueLength is the one leaf value size EIP-8297 admits (eip:132). +const pbinValueLength = 32 + +// BASIC_DATA field offsets within the leaf value (eip:332-339). Bytes 1..3 are +// reserved and version is always zero, since writing any header field resets it. +const ( + pbinBasicDataCodeSizeOffset = 4 + pbinBasicDataNonceOffset = 8 + pbinBasicDataBalanceOffset = 16 +) + +var ( + errPBinBalanceOverflow = errors.New("pbin: balance does not fit the 16-byte BASIC_DATA field") + errPBinCodeSizeOverflow = errors.New("pbin: code size does not fit the 4-byte BASIC_DATA field") +) + +// pbinEncodeBasicData packs version, code_size, nonce and balance big-endian +// into the BASIC_DATA leaf value. A balance the 16-byte field cannot hold is an +// error rather than a silent truncation, which would commit a wrong root. +func pbinEncodeBasicData(nonce uint64, balance *uint256.Int, codeSize uint64) ([pbinValueLength]byte, error) { + var v [pbinValueLength]byte + if balance.BitLen() > 128 { + return v, fmt.Errorf("%w: %s", errPBinBalanceOverflow, balance) + } + if codeSize > 1<<32-1 { + return v, fmt.Errorf("%w: %d", errPBinCodeSizeOverflow, codeSize) + } + binary.BigEndian.PutUint32(v[pbinBasicDataCodeSizeOffset:], uint32(codeSize)) + binary.BigEndian.PutUint64(v[pbinBasicDataNonceOffset:], nonce) + b32 := balance.Bytes32() + copy(v[pbinBasicDataBalanceOffset:], b32[16:]) + return v, nil +} + +// pbinCodeHashValue returns the CODE_HASH leaf value, mapping an unset hash to +// the hash of empty bytecode as the spec requires for a codeless account +// (eip:345-347). +func pbinCodeHashValue(codeHash common.Hash) [pbinValueLength]byte { + if codeHash == (common.Hash{}) { + return empty.CodeHash + } + return codeHash +} + +// pbinEncodeStorageValue left-pads a storage value to the fixed leaf width. +func pbinEncodeStorageValue(value []byte) [pbinValueLength]byte { + if len(value) > length.Hash { + panic(fmt.Sprintf("pbin: storage value of %d bytes exceeds %d", len(value), length.Hash)) + } + var v [pbinValueLength]byte + copy(v[pbinValueLength-len(value):], value) + return v +} diff --git a/execution/commitment/pbin_values_test.go b/execution/commitment/pbin_values_test.go new file mode 100644 index 00000000000..a0ff8bab9d6 --- /dev/null +++ b/execution/commitment/pbin_values_test.go @@ -0,0 +1,200 @@ +// Copyright 2026 The Erigon Authors +// This file is part of Erigon. +// +// Erigon is free software: you can redistribute it and/or modify +// it under the terms of the GNU Lesser General Public License as published by +// the Free Software Foundation, either version 3 of the License, or +// (at your option) any later version. +// +// Erigon is distributed in the hope that it will be useful, +// but WITHOUT ANY WARRANTY; without even the implied warranty of +// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +// GNU Lesser General Public License for more details. +// +// You should have received a copy of the GNU Lesser General Public License +// along with Erigon. If not, see . + +package commitment + +import ( + "encoding/hex" + "testing" + + "github.com/holiman/uint256" + "github.com/stretchr/testify/require" + + "github.com/erigontech/erigon/common" +) + +// The expectations here are hand-written hex, never the encoder's own output: +// the Task 4 oracle consumes this same encoder, so a differential root test +// cannot see a value-encoding bug. +func TestPBinEncodeBasicData(t *testing.T) { + t.Parallel() + + maxU128 := new(uint256.Int).Sub(new(uint256.Int).Lsh(uint256.NewInt(1), 128), uint256.NewInt(1)) + + for _, tc := range []struct { + name string + codeSize uint64 + nonce uint64 + balance *uint256.Int + want string + }{ + { + name: "empty account", + balance: uint256.NewInt(0), + want: "0000000000000000000000000000000000000000000000000000000000000000", + }, + { + name: "distinct bytes in every field", + codeSize: 0xDEADBEEF, + nonce: 0x0102030405060708, + balance: new(uint256.Int).SetBytes(common.FromHex("0x0102030405060708090a0b0c0d0e0f10")), + want: "00000000deadbeef01020304050607080102030405060708090a0b0c0d0e0f10", + }, + { + name: "every field at its maximum", + codeSize: 0xFFFFFFFF, + nonce: 0xFFFFFFFFFFFFFFFF, + balance: maxU128, + want: "00000000ffffffffffffffffffffffffffffffffffffffffffffffffffffffff", + }, + { + name: "code_size occupies offsets 4..7 only", + codeSize: 1, + balance: uint256.NewInt(0), + want: "0000000000000001000000000000000000000000000000000000000000000000", + }, + { + name: "nonce occupies offsets 8..15 only", + nonce: 1, + balance: uint256.NewInt(0), + want: "0000000000000000000000000000000100000000000000000000000000000000", + }, + { + name: "balance occupies offsets 16..31 only", + balance: uint256.NewInt(1), + want: "0000000000000000000000000000000000000000000000000000000000000001", + }, + } { + t.Run(tc.name, func(t *testing.T) { + t.Parallel() + got, err := pbinEncodeBasicData(tc.nonce, tc.balance, tc.codeSize) + require.NoError(t, err) + require.Equal(t, tc.want, hex.EncodeToString(got[:])) + require.Len(t, got, pbinValueLength) + }) + } +} + +func TestPBinEncodeBasicDataVersionAndReservedAreZero(t *testing.T) { + t.Parallel() + + got, err := pbinEncodeBasicData(0xFFFFFFFFFFFFFFFF, uint256.NewInt(0), 0xFFFFFFFF) + require.NoError(t, err) + require.Equal(t, byte(0), got[0], "version") + require.Equal(t, []byte{0, 0, 0}, got[1:4], "reserved") +} + +func TestPBinEncodeBasicDataBalanceOverflow(t *testing.T) { + t.Parallel() + + twoPow128 := new(uint256.Int).Lsh(uint256.NewInt(1), 128) + + _, err := pbinEncodeBasicData(0, twoPow128, 0) + require.ErrorIs(t, err, errPBinBalanceOverflow) + + _, err = pbinEncodeBasicData(0, new(uint256.Int).Sub(twoPow128, uint256.NewInt(1)), 0) + require.NoError(t, err, "2^128-1 is the largest representable balance") + + _, err = pbinEncodeBasicData(0, new(uint256.Int).SetAllOne(), 0) + require.ErrorIs(t, err, errPBinBalanceOverflow) +} + +func TestPBinEncodeBasicDataCodeSizeOverflow(t *testing.T) { + t.Parallel() + + _, err := pbinEncodeBasicData(0, uint256.NewInt(0), 1<<32) + require.ErrorIs(t, err, errPBinCodeSizeOverflow) + + _, err = pbinEncodeBasicData(0, uint256.NewInt(0), 1<<32-1) + require.NoError(t, err) +} + +func TestPBinCodeHashValue(t *testing.T) { + t.Parallel() + + // keccak256("") — what a codeless account's CODE_HASH leaf holds. + const emptyCodeHash = "c5d2460186f7233c927e7db2dcc703c0e500b653ca82273b7bfad8045d85a470" + + t.Run("contract code hash passes through", func(t *testing.T) { + t.Parallel() + h := common.HexToHash("0x0102030405060708090a0b0c0d0e0f101112131415161718191a1b1c1d1e1f20") + got := pbinCodeHashValue(h) + require.Equal(t, "0102030405060708090a0b0c0d0e0f101112131415161718191a1b1c1d1e1f20", hex.EncodeToString(got[:])) + }) + + t.Run("zero hash becomes the empty-code hash", func(t *testing.T) { + t.Parallel() + got := pbinCodeHashValue(common.Hash{}) + require.Equal(t, emptyCodeHash, hex.EncodeToString(got[:])) + }) + + t.Run("empty-code hash passes through", func(t *testing.T) { + t.Parallel() + got := pbinCodeHashValue(common.HexToHash("0x" + emptyCodeHash)) + require.Equal(t, emptyCodeHash, hex.EncodeToString(got[:])) + }) +} + +func TestPBinEncodeStorageValue(t *testing.T) { + t.Parallel() + + for _, tc := range []struct { + name string + value string + want string + }{ + { + name: "absent value is 32 zero bytes", + value: "", + want: "0000000000000000000000000000000000000000000000000000000000000000", + }, + { + name: "one byte is left-padded", + value: "05", + want: "0000000000000000000000000000000000000000000000000000000000000005", + }, + { + name: "short value keeps its byte order", + value: "0102", + want: "0000000000000000000000000000000000000000000000000000000000000102", + }, + { + name: "full-width value passes through", + value: "0102030405060708090a0b0c0d0e0f101112131415161718191a1b1c1d1e1f20", + want: "0102030405060708090a0b0c0d0e0f101112131415161718191a1b1c1d1e1f20", + }, + { + name: "leading zero byte is preserved", + value: "0002030405060708090a0b0c0d0e0f101112131415161718191a1b1c1d1e1f20", + want: "0002030405060708090a0b0c0d0e0f101112131415161718191a1b1c1d1e1f20", + }, + } { + t.Run(tc.name, func(t *testing.T) { + t.Parallel() + raw, err := hex.DecodeString(tc.value) + require.NoError(t, err) + got := pbinEncodeStorageValue(raw) + require.Equal(t, tc.want, hex.EncodeToString(got[:])) + require.Len(t, got, pbinValueLength) + }) + } +} + +func TestPBinEncodeStorageValueRejectsOversizedValue(t *testing.T) { + t.Parallel() + + require.Panics(t, func() { pbinEncodeStorageValue(make([]byte, 33)) }) +} From 2482bff5de03e4cd6112196290d8a04fb6afe470 Mon Sep 17 00:00:00 2001 From: awskii Date: Wed, 29 Jul 2026 17:37:21 +0700 Subject: [PATCH 05/56] feat: EIP-8297 reference oracle for differential testing Naive transcription of the spec's BinaryTree/_insert/merkelize (eip:112-222) in the test package: one bit per byte, no memoised hashes, x/crypto Keccak rather than the engine's fastkeccak so a hasher bug cannot cancel out on both sides. This is the ground truth root equality is measured against. Corpus builders cover empty, single key (root is a leaf), divergence at bit 0 and bit 527, a split-inside-prefix triple that forces prefix[matched+1:], one account's full stem, and a mined deep-shared-prefix cluster. Self-consistency tests pin encode_bit_prefix and the leaf/branch hashes against hand-written bytes, and assert permutation independence and prefix-freedom over every corpus. --- docs/plans/20260729-pbin-patricia-hashed.md | 10 +- execution/commitment/pbin_oracle_test.go | 671 ++++++++++++++++++++ 2 files changed, 676 insertions(+), 5 deletions(-) create mode 100644 execution/commitment/pbin_oracle_test.go diff --git a/docs/plans/20260729-pbin-patricia-hashed.md b/docs/plans/20260729-pbin-patricia-hashed.md index 93d649e3043..12239869140 100644 --- a/docs/plans/20260729-pbin-patricia-hashed.md +++ b/docs/plans/20260729-pbin-patricia-hashed.md @@ -159,11 +159,11 @@ func pbinCommonPrefixBits(a, b *pbinBitpath) int16 // XOR + LeadingZeros64, clam **Files:** - Create: `execution/commitment/pbin_oracle_test.go` -- [ ] transcribe the spec's `LeafNode`, `BranchNode`, `_insert` and `merkelize` (`eip:112-222`) as a naive in-memory Go tree, Keccak-256, no optimisation -- [ ] implement `encode_bit_prefix` exactly per `eip:196-201` and define the empty-tree hash as 32 zero bytes per `eip:208` -- [ ] add corpus builders: empty; single key (root **is** a leaf, `eip:133-135`); two keys diverging at bit 0; two diverging at bit 527; a split-inside-prefix triple forcing `node.prefix[matched+1:]`; a mined deep-shared-prefix cluster -- [ ] write tests asserting the oracle is self-consistent: permutation independence and prefix-freedom over every corpus -- [ ] run tests - must pass before task 5 +- [x] transcribe the spec's `LeafNode`, `BranchNode`, `_insert` and `merkelize` (`eip:112-222`) as a naive in-memory Go tree, Keccak-256, no optimisation +- [x] implement `encode_bit_prefix` exactly per `eip:196-201` and define the empty-tree hash as 32 zero bytes per `eip:208` +- [x] add corpus builders: empty; single key (root **is** a leaf, `eip:133-135`); two keys diverging at bit 0; two diverging at bit 527; a split-inside-prefix triple forcing `node.prefix[matched+1:]`; a mined deep-shared-prefix cluster +- [x] write tests asserting the oracle is self-consistent: permutation independence and prefix-freedom over every corpus +- [x] run tests - must pass before task 5 ### Task 5: pbinCell, grid, and branch record codec diff --git a/execution/commitment/pbin_oracle_test.go b/execution/commitment/pbin_oracle_test.go new file mode 100644 index 00000000000..01b1692f961 --- /dev/null +++ b/execution/commitment/pbin_oracle_test.go @@ -0,0 +1,671 @@ +// Copyright 2026 The Erigon Authors +// This file is part of Erigon. +// +// Erigon is free software: you can redistribute it and/or modify +// it under the terms of the GNU Lesser General Public License as published by +// the Free Software Foundation, either version 3 of the License, or +// (at your option) any later version. +// +// Erigon is distributed in the hope that it will be useful, +// but WITHOUT ANY WARRANTY; without even the implied warranty of +// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +// GNU Lesser General Public License for more details. +// +// You should have received a copy of the GNU Lesser General Public License +// along with Erigon. If not, see . + +package commitment + +import ( + "bytes" + "encoding/binary" + "encoding/hex" + "fmt" + "math/rand" + "slices" + "sync" + "testing" + + "github.com/stretchr/testify/require" + "golang.org/x/crypto/sha3" + + "github.com/erigontech/erigon/common/empty" + "github.com/erigontech/erigon/common/length" +) + +// The reference implementation of EIP-8297's binary tree (eip:112-222), +// transcribed from the spec's Python with no optimisation: no memoised hashes, +// no shared buffers, one bit per byte. It is the ground truth the engine is +// diffed against, so it is written to be recognisably the same algorithm rather +// than a fast one. Its Keccak comes from x/crypto, not the fastkeccak the +// engine uses, so a hasher bug cannot cancel out on both sides. + +const ( + pbinOracleMaxKeyLength = 8192 + pbinOracleLeafTag = 0x00 + pbinOracleBranchTag = 0x01 +) + +type pbinOracleNode interface{ pbinOracleNodeKind() } + +type pbinOracleLeaf struct { + key []byte + value []byte +} + +// prefix holds one bit per byte, mirroring the spec's list[int]. +type pbinOracleBranch struct { + prefix []byte + left, right pbinOracleNode +} + +func (*pbinOracleLeaf) pbinOracleNodeKind() {} +func (*pbinOracleBranch) pbinOracleNodeKind() {} + +type pbinOracleTree struct { + root pbinOracleNode +} + +func pbinOracleBytesToBits(data []byte) []byte { + bits := make([]byte, 0, len(data)*8) + for _, b := range data { + for i := range 8 { + bits = append(bits, (b>>(7-i))&1) + } + } + return bits +} + +func (t *pbinOracleTree) insert(key, value []byte) { + if len(key) < 1 || len(key) > pbinOracleMaxKeyLength { + panic(fmt.Sprintf("pbin oracle: key length %d out of range", len(key))) + } + if len(value) != pbinValueLength { + panic(fmt.Sprintf("pbin oracle: value of %d bytes, want %d", len(value), pbinValueLength)) + } + if t.root == nil { + t.root = &pbinOracleLeaf{key: slices.Clone(key), value: slices.Clone(value)} + return + } + t.root = pbinOracleInsert(t.root, pbinOracleBytesToBits(key), key, value, 0) +} + +func pbinOracleInsert(node pbinOracleNode, bits, key, value []byte, depth int) pbinOracleNode { + if leaf, ok := node.(*pbinOracleLeaf); ok { + if bytes.Equal(leaf.key, key) { + leaf.value = slices.Clone(value) + return leaf + } + otherBits := pbinOracleBytesToBits(leaf.key) + limit := min(len(bits), len(otherBits)) + run := 0 + for depth+run < limit && bits[depth+run] == otherBits[depth+run] { + run++ + } + if depth+run >= limit { + panic("pbin oracle: insert violates prefix-freedom") + } + newLeaf := &pbinOracleLeaf{key: slices.Clone(key), value: slices.Clone(value)} + branch := &pbinOracleBranch{prefix: slices.Clone(bits[depth : depth+run])} + if bits[depth+run] == 0 { + branch.left, branch.right = newLeaf, leaf + } else { + branch.left, branch.right = leaf, newLeaf + } + return branch + } + + branch := node.(*pbinOracleBranch) + matched := 0 + for matched < len(branch.prefix) && depth+matched < len(bits) && bits[depth+matched] == branch.prefix[matched] { + matched++ + } + if depth+matched >= len(bits) { + panic("pbin oracle: insert violates prefix-freedom") + } + if matched == len(branch.prefix) { + split := depth + matched + if bits[split] == 0 { + branch.left = pbinOracleInsert(branch.left, bits, key, value, split+1) + } else { + branch.right = pbinOracleInsert(branch.right, bits, key, value, split+1) + } + return branch + } + + // The key diverges inside the prefix (eip:171-182). The survivor keeps the + // bits after the divergence, dropping the bit the new branch consumes. + survivor := &pbinOracleBranch{ + prefix: slices.Clone(branch.prefix[matched+1:]), + left: branch.left, + right: branch.right, + } + newLeaf := &pbinOracleLeaf{key: slices.Clone(key), value: slices.Clone(value)} + newBranch := &pbinOracleBranch{prefix: slices.Clone(branch.prefix[:matched])} + if bits[depth+matched] == 0 { + newBranch.left, newBranch.right = newLeaf, survivor + } else { + newBranch.left, newBranch.right = survivor, newLeaf + } + return newBranch +} + +// pbinOracleEncodeBitPrefix is the spec's encode_bit_prefix (eip:196-201). +func pbinOracleEncodeBitPrefix(prefix []byte) []byte { + if len(prefix) >= 1<<16 { + panic(fmt.Sprintf("pbin oracle: prefix of %d bits exceeds the encodable count", len(prefix))) + } + out := make([]byte, 2+(len(prefix)+7)/8) + binary.BigEndian.PutUint16(out, uint16(len(prefix))) + for i, bit := range prefix { + out[2+i/8] |= bit << (7 - i%8) + } + return out +} + +func pbinOracleMerkelize(node pbinOracleNode) [32]byte { + var out [32]byte + if node == nil { + return out + } + h := sha3.NewLegacyKeccak256() + switch n := node.(type) { + case *pbinOracleLeaf: + h.Write([]byte{pbinOracleLeafTag}) + h.Write(n.key) + h.Write(n.value) + case *pbinOracleBranch: + left, right := pbinOracleMerkelize(n.left), pbinOracleMerkelize(n.right) + h.Write([]byte{pbinOracleBranchTag}) + h.Write(pbinOracleEncodeBitPrefix(n.prefix)) + h.Write(left[:]) + h.Write(right[:]) + } + copy(out[:], h.Sum(nil)) + return out +} + +func (t *pbinOracleTree) rootHash() [32]byte { return pbinOracleMerkelize(t.root) } + +type pbinOracleEntry struct { + key []byte + value []byte +} + +type pbinOracleCorpus struct { + name string + entries []pbinOracleEntry +} + +func pbinOracleRoot(entries []pbinOracleEntry) [32]byte { + var tree pbinOracleTree + for _, e := range entries { + tree.insert(e.key, e.value) + } + return tree.rootHash() +} + +func pbinOracleSharedBits(a, b []byte) int { + aBits, bBits := pbinOracleBytesToBits(a), pbinOracleBytesToBits(b) + n := 0 + for n < len(aBits) && n < len(bBits) && aBits[n] == bBits[n] { + n++ + } + return n +} + +func pbinOracleValue(seed uint64) []byte { + v := make([]byte, pbinValueLength) + binary.BigEndian.PutUint64(v, 0xA5A5A5A5A5A5A5A5) + binary.BigEndian.PutUint64(v[24:], seed) + return v +} + +func pbinOracleAddr(seed uint64) []byte { + addr := make([]byte, length.Addr) + binary.BigEndian.PutUint64(addr[12:], seed) + return addr +} + +func pbinOracleSlot(v uint64) []byte { + slot := make([]byte, length.Hash) + binary.BigEndian.PutUint64(slot[24:], v) + return slot +} + +func pbinOracleCorpora() []pbinOracleCorpus { + return []pbinOracleCorpus{ + pbinOracleCorpusEmpty(), + pbinOracleCorpusSingleKey(), + pbinOracleCorpusSplitAtBit0(), + pbinOracleCorpusSplitAtLastBit(), + pbinOracleCorpusSplitInsidePrefix(), + pbinOracleCorpusOneAccount(), + pbinOracleCorpusDeepSharedPrefix(), + } +} + +func pbinOracleCorpusEmpty() pbinOracleCorpus { + return pbinOracleCorpus{name: "empty"} +} + +func pbinOracleCorpusSingleKey() pbinOracleCorpus { + return pbinOracleCorpus{ + name: "single key", + entries: []pbinOracleEntry{ + {key: pbinTreeKeyAccount(pbinOracleAddr(1), pbinBasicDataLeafKey), value: pbinOracleValue(1)}, + }, + } +} + +// pbinOracleCorpusSplitAtBit0 diverges on the zone byte, so the root branch +// carries an empty prefix. +func pbinOracleCorpusSplitAtBit0() pbinOracleCorpus { + addr := pbinOracleAddr(2) + return pbinOracleCorpus{ + name: "split at bit 0", + entries: []pbinOracleEntry{ + {key: pbinTreeKeyAccount(addr, pbinBasicDataLeafKey), value: pbinOracleValue(1)}, + {key: pbinTreeKeyStorage(addr, pbinOracleSlot(1000)), value: pbinOracleValue(2)}, + }, + } +} + +// pbinOracleCorpusSplitAtLastBit picks two slots in one storage group whose +// sub-indices differ in their low bit, the deepest split 528-bit keys admit. +func pbinOracleCorpusSplitAtLastBit() pbinOracleCorpus { + addr := pbinOracleAddr(3) + return pbinOracleCorpus{ + name: "split at bit 527", + entries: []pbinOracleEntry{ + {key: pbinTreeKeyStorage(addr, pbinOracleSlot(256)), value: pbinOracleValue(1)}, + {key: pbinTreeKeyStorage(addr, pbinOracleSlot(257)), value: pbinOracleValue(2)}, + }, + } +} + +// pbinOracleCorpusSplitInsidePrefix uses synthetic account-zone keys so the +// divergence bit is exact: the first two share 15 bits, the third leaves at bit +// 9, forcing _insert down the survivor path with a non-empty remainder. +func pbinOracleCorpusSplitInsidePrefix() pbinOracleCorpus { + return pbinOracleCorpus{ + name: "split inside prefix", + entries: []pbinOracleEntry{ + {key: pbinOracleSyntheticAccountKey(0x00), value: pbinOracleValue(1)}, + {key: pbinOracleSyntheticAccountKey(0x01), value: pbinOracleValue(2)}, + {key: pbinOracleSyntheticAccountKey(0x40), value: pbinOracleValue(3)}, + }, + } +} + +func pbinOracleSyntheticAccountKey(stemByte byte) []byte { + key := make([]byte, pbinAccountKeyLength) + key[1] = stemByte + return key +} + +// pbinOracleCorpusOneAccount is the realistic shape: header leaves, header-zone +// slots and storage-zone slots for a single address, all sharing a stem. +func pbinOracleCorpusOneAccount() pbinOracleCorpus { + addr := pbinOracleAddr(4) + entries := []pbinOracleEntry{ + {key: pbinTreeKeyAccount(addr, pbinBasicDataLeafKey), value: pbinOracleValue(1)}, + {key: pbinTreeKeyAccount(addr, pbinCodeHashLeafKey), value: pbinOracleValue(2)}, + } + for i, slot := range []uint64{0, 1, 63, 64, 65, 255, 256, 1000} { + entries = append(entries, pbinOracleEntry{ + key: pbinTreeKeyStorage(addr, pbinOracleSlot(slot)), + value: pbinOracleValue(uint64(10 + i)), + }) + } + return pbinOracleCorpus{name: "one account", entries: entries} +} + +const ( + pbinOracleMinedPrefixBits = 20 + pbinOracleMinedCluster = 4 +) + +func pbinOracleCorpusDeepSharedPrefix() pbinOracleCorpus { + entries := make([]pbinOracleEntry, 0, pbinOracleMinedCluster) + for i, addr := range pbinOracleMinedAddrs() { + entries = append(entries, pbinOracleEntry{ + key: pbinTreeKeyAccount(addr, pbinBasicDataLeafKey), + value: pbinOracleValue(uint64(i)), + }) + } + return pbinOracleCorpus{name: "mined deep shared prefix", entries: entries} +} + +var pbinOracleMinedAddrs = sync.OnceValue(func() [][]byte { + return pbinOracleMineSharedStems(pbinOracleMinedPrefixBits, pbinOracleMinedCluster) +}) + +// pbinOracleMineSharedStems searches for addresses whose account keys agree on +// shared leading bits. The stem is a digest, so a deep shared prefix cannot be +// constructed and has to be found by trial. +func pbinOracleMineSharedStems(shared, n int) [][]byte { + const limit = 1 << 24 + var target []byte + found := make([][]byte, 0, n) + for i := uint64(0); i < limit && len(found) < n; i++ { + addr := pbinOracleAddr(i) + key := pbinTreeKeyAccount(addr, pbinBasicDataLeafKey) + if target == nil { + target, found = key, append(found, addr) + continue + } + if pbinOracleSharedBits(target, key) >= shared { + found = append(found, addr) + } + } + if len(found) < n { + panic(fmt.Sprintf("pbin oracle: found only %d of %d addresses sharing %d bits", len(found), n, shared)) + } + return found +} + +// TestPBinOracleEncodeBitPrefix pins encode_bit_prefix (eip:196-201) against +// hand-written bytes, since every branch hash the oracle produces depends on it. +func TestPBinOracleEncodeBitPrefix(t *testing.T) { + t.Parallel() + + for _, tc := range []struct { + name string + prefix []byte + want string + }{ + {name: "empty prefix is a bare count", prefix: nil, want: "0000"}, + {name: "one zero bit", prefix: []byte{0}, want: "000100"}, + {name: "one set bit lands in the MSB", prefix: []byte{1}, want: "000180"}, + {name: "three bits", prefix: []byte{1, 0, 1}, want: "0003a0"}, + {name: "seven bits pad low", prefix: []byte{1, 1, 1, 1, 1, 1, 1}, want: "0007fe"}, + {name: "full byte", prefix: []byte{1, 0, 1, 0, 1, 0, 1, 0}, want: "0008aa"}, + {name: "nine bits open a second byte", prefix: []byte{1, 0, 1, 0, 1, 0, 1, 0, 1}, want: "0009aa80"}, + } { + t.Run(tc.name, func(t *testing.T) { + t.Parallel() + require.Equal(t, tc.want, hex.EncodeToString(pbinOracleEncodeBitPrefix(tc.prefix))) + }) + } +} + +func TestPBinOracleEncodeBitPrefixLongRun(t *testing.T) { + t.Parallel() + + // 528 bits of 1: count 0x0210 followed by 66 0xFF bytes. + prefix := bytes.Repeat([]byte{1}, pbinMaxPathBits) + got := pbinOracleEncodeBitPrefix(prefix) + require.Len(t, got, 2+66) + require.Equal(t, []byte{0x02, 0x10}, got[:2]) + require.Equal(t, bytes.Repeat([]byte{0xFF}, 66), got[2:]) +} + +// TestPBinOracleEmptyTreeHash guards H11 at the oracle: the empty tree is 32 +// zero bytes (eip:208), not the empty-MPT root the rest of erigon uses. +func TestPBinOracleEmptyTreeHash(t *testing.T) { + t.Parallel() + + var tree pbinOracleTree + root := tree.rootHash() + require.Equal(t, make([]byte, 32), root[:]) + require.NotEqual(t, empty.RootHash[:], root[:]) +} + +func TestPBinOracleSingleKeyRootIsLeafHash(t *testing.T) { + t.Parallel() + + corpus := pbinOracleCorpusSingleKey() + require.Len(t, corpus.entries, 1) + e := corpus.entries[0] + + var tree pbinOracleTree + tree.insert(e.key, e.value) + + require.IsType(t, &pbinOracleLeaf{}, tree.root, "a one-key tree's root is the leaf itself (eip:133-135)") + + want := pbinTestKeccak(t, []byte{0x00}, e.key, e.value) + got := tree.rootHash() + require.Equal(t, want, got[:]) +} + +func TestPBinOracleTwoKeyRootIsBranchHash(t *testing.T) { + t.Parallel() + + corpus := pbinOracleCorpusSplitAtBit0() + require.Len(t, corpus.entries, 2) + a, b := corpus.entries[0], corpus.entries[1] + + var tree pbinOracleTree + tree.insert(a.key, a.value) + tree.insert(b.key, b.value) + + branch, ok := tree.root.(*pbinOracleBranch) + require.True(t, ok) + require.Empty(t, branch.prefix, "keys diverging at bit 0 leave the root prefix empty") + + left := pbinTestKeccak(t, []byte{0x00}, a.key, a.value) + right := pbinTestKeccak(t, []byte{0x00}, b.key, b.value) + want := pbinTestKeccak(t, []byte{0x01}, pbinOracleEncodeBitPrefix(nil), left, right) + + got := tree.rootHash() + require.Equal(t, want, got[:]) +} + +// TestPBinOracleSplitAtLastBit exercises the deepest split two 528-bit keys can +// have: they agree on all but the final bit. +func TestPBinOracleSplitAtLastBit(t *testing.T) { + t.Parallel() + + corpus := pbinOracleCorpusSplitAtLastBit() + require.Len(t, corpus.entries, 2) + a, b := corpus.entries[0], corpus.entries[1] + require.Equal(t, pbinMaxPathBits-1, pbinOracleSharedBits(a.key, b.key)) + + var tree pbinOracleTree + tree.insert(a.key, a.value) + tree.insert(b.key, b.value) + + branch, ok := tree.root.(*pbinOracleBranch) + require.True(t, ok) + require.Len(t, branch.prefix, pbinMaxPathBits-1) + + left := pbinTestKeccak(t, []byte{0x00}, a.key, a.value) + right := pbinTestKeccak(t, []byte{0x00}, b.key, b.value) + want := pbinTestKeccak(t, []byte{0x01}, pbinOracleEncodeBitPrefix(branch.prefix), left, right) + + got := tree.rootHash() + require.Equal(t, want, got[:]) +} + +// TestPBinOracleSplitInsidePrefix pins the shape the split-inside-prefix branch +// of _insert produces (eip:171-182): the survivor keeps prefix[matched+1:], so +// the bit consumed by the new branch must not reappear below it. +func TestPBinOracleSplitInsidePrefix(t *testing.T) { + t.Parallel() + + corpus := pbinOracleCorpusSplitInsidePrefix() + require.Len(t, corpus.entries, 3) + a, b, c := corpus.entries[0], corpus.entries[1], corpus.entries[2] + + var pair pbinOracleTree + pair.insert(a.key, a.value) + pair.insert(b.key, b.value) + pairRoot, ok := pair.root.(*pbinOracleBranch) + require.True(t, ok) + require.Len(t, pairRoot.prefix, 15, "a and b must share a prefix long enough to split inside") + + var tree pbinOracleTree + tree.insert(a.key, a.value) + tree.insert(b.key, b.value) + tree.insert(c.key, c.value) + + root, ok := tree.root.(*pbinOracleBranch) + require.True(t, ok) + require.Len(t, root.prefix, 9, "the new branch keeps the bits before the divergence") + + // c has a 1 bit where the old prefix had 0, so the new leaf takes the right + // side and the survivor keeps the left. + survivor, ok := root.left.(*pbinOracleBranch) + require.True(t, ok) + require.Len(t, survivor.prefix, 5, "the survivor drops the divergence bit itself") + require.Equal(t, pairRoot.prefix[10:], survivor.prefix) + require.IsType(t, &pbinOracleLeaf{}, root.right) +} + +func TestPBinOracleDuplicateKeyUpdatesValue(t *testing.T) { + t.Parallel() + + corpus := pbinOracleCorpusSplitAtBit0() + a, b := corpus.entries[0], corpus.entries[1] + updated := pbinOracleValue(0xDEAD) + + var tree pbinOracleTree + tree.insert(a.key, a.value) + tree.insert(b.key, b.value) + tree.insert(a.key, updated) + + var want pbinOracleTree + want.insert(a.key, updated) + want.insert(b.key, b.value) + + require.Equal(t, want.rootHash(), tree.rootHash()) + require.NotEqual(t, pbinOracleRoot(corpus.entries), tree.rootHash()) +} + +func TestPBinOracleRejectsInvalidInsert(t *testing.T) { + t.Parallel() + + key := pbinOracleCorpusSingleKey().entries[0].key + + t.Run("value must be 32 bytes", func(t *testing.T) { + t.Parallel() + var tree pbinOracleTree + require.Panics(t, func() { tree.insert(key, make([]byte, 31)) }) + }) + t.Run("key must be non-empty", func(t *testing.T) { + t.Parallel() + var tree pbinOracleTree + require.Panics(t, func() { tree.insert(nil, pbinOracleValue(0)) }) + }) + t.Run("key must fit MAX_KEY_LENGTH", func(t *testing.T) { + t.Parallel() + var tree pbinOracleTree + require.Panics(t, func() { tree.insert(make([]byte, pbinOracleMaxKeyLength+1), pbinOracleValue(0)) }) + }) + t.Run("a key that is a prefix of another is rejected", func(t *testing.T) { + t.Parallel() + var tree pbinOracleTree + tree.insert(key, pbinOracleValue(0)) + require.Panics(t, func() { tree.insert(key[:8], pbinOracleValue(1)) }) + }) + t.Run("a key extending another is rejected", func(t *testing.T) { + t.Parallel() + var tree pbinOracleTree + tree.insert(key[:8], pbinOracleValue(0)) + require.Panics(t, func() { tree.insert(key, pbinOracleValue(1)) }) + }) +} + +// TestPBinOracleCorporaArePrefixFree checks every corpus satisfies the +// invariant _insert asserts, so a later differential failure is a tree bug and +// not a malformed corpus. +func TestPBinOracleCorporaArePrefixFree(t *testing.T) { + t.Parallel() + + for _, corpus := range pbinOracleCorpora() { + t.Run(corpus.name, func(t *testing.T) { + t.Parallel() + for i, a := range corpus.entries { + require.Contains(t, []int{pbinAccountKeyLength, pbinStorageKeyLength}, len(a.key), + "key %d has no zone-fixed length", i) + require.Len(t, a.value, pbinValueLength) + for j, b := range corpus.entries { + if i == j { + continue + } + require.False(t, bytes.HasPrefix(b.key, a.key), + "key %d is a prefix of key %d", i, j) + } + } + }) + } +} + +// TestPBinOraclePermutationIndependence is the property that makes the oracle +// usable as ground truth: the root depends on the key/value set, not on the +// order entries arrive in. +func TestPBinOraclePermutationIndependence(t *testing.T) { + t.Parallel() + + for _, corpus := range pbinOracleCorpora() { + t.Run(corpus.name, func(t *testing.T) { + t.Parallel() + want := pbinOracleRoot(corpus.entries) + for name, order := range pbinOracleOrderings(corpus.entries) { + require.Equal(t, want, pbinOracleRoot(order), "ordering %s", name) + } + }) + } +} + +// TestPBinOracleDeepSharedPrefixCorpus checks the mined cluster really does +// share a deep prefix — without that, the corpus never exercises a split far +// from the root. +func TestPBinOracleDeepSharedPrefixCorpus(t *testing.T) { + t.Parallel() + + corpus := pbinOracleCorpusDeepSharedPrefix() + require.GreaterOrEqual(t, len(corpus.entries), 4) + + first := corpus.entries[0].key + for _, e := range corpus.entries[1:] { + require.GreaterOrEqual(t, pbinOracleSharedBits(first, e.key), pbinOracleMinedPrefixBits) + } + + var tree pbinOracleTree + for _, e := range corpus.entries { + tree.insert(e.key, e.value) + } + root, ok := tree.root.(*pbinOracleBranch) + require.True(t, ok) + require.GreaterOrEqual(t, len(root.prefix), pbinOracleMinedPrefixBits-1) +} + +// TestPBinOracleStemSharedCorpus pins that one account's keys land under a +// shared stem: the storage-zone keys agree on the 264 zone+stem bits. +func TestPBinOracleStemSharedCorpus(t *testing.T) { + t.Parallel() + + corpus := pbinOracleCorpusOneAccount() + var storage [][]byte + for _, e := range corpus.entries { + if len(e.key) == pbinStorageKeyLength { + storage = append(storage, e.key) + } + } + require.GreaterOrEqual(t, len(storage), 2) + for _, k := range storage[1:] { + require.GreaterOrEqual(t, pbinOracleSharedBits(storage[0], k), 8+256) + } +} + +func pbinOracleOrderings(entries []pbinOracleEntry) map[string][]pbinOracleEntry { + byKeyAsc := slices.Clone(entries) + slices.SortFunc(byKeyAsc, func(a, b pbinOracleEntry) int { return bytes.Compare(a.key, b.key) }) + byKeyDesc := slices.Clone(byKeyAsc) + slices.Reverse(byKeyDesc) + reversed := slices.Clone(entries) + slices.Reverse(reversed) + + shuffled := slices.Clone(entries) + rnd := rand.New(rand.NewSource(0x8297)) + rnd.Shuffle(len(shuffled), func(i, j int) { shuffled[i], shuffled[j] = shuffled[j], shuffled[i] }) + + return map[string][]pbinOracleEntry{ + "reversed": reversed, + "key ascending": byKeyAsc, + "key descending": byKeyDesc, + "shuffled": shuffled, + } +} From 81b0889a12d068b955e4d91eca2f25ea584ae321 Mon Sep 17 00:00:00 2001 From: awskii Date: Wed, 29 Jul 2026 17:47:39 +0700 Subject: [PATCH 06/56] feat: EIP-8297 pbinCell, grid and branch record codec MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit pbinCell carries a single tree-key-space prefix instead of HPH's hashedExtension/extension pair: that split exists to hold the hashed and plain key spaces separately, while PBin derives the tree key from the plain key on demand. There is no memoized leaf hash either — H(0x00||key||value) commits the complete key, so it never depends on where the leaf sits and nothing is worth caching. The record codec is PBin-local. A 66-byte storage prefix does not fit the shared cellEncodeData.extension field, and PatriciaContext moves branch payloads as opaque bytes, so nothing shared changes. Every record encodes both child cells (bitmap = afterMap), which removes the merge-with-previous path: at arity 2 the untouched sibling is the whole other half of the subtree, and merging is what loses it. The prefix bit count is an explicit uvarint and the authority on length; decode derives the byte count from it, rejects a short buffer, rejects non-zero pad bits, and rejects trailing bytes, so one node has one record spelling. --- docs/plans/20260729-pbin-patricia-hashed.md | 12 +- execution/commitment/pbin_branch.go | 237 +++++++++++++ execution/commitment/pbin_cell.go | 106 ++++++ execution/commitment/pbin_cell_test.go | 350 ++++++++++++++++++++ 4 files changed, 699 insertions(+), 6 deletions(-) create mode 100644 execution/commitment/pbin_branch.go create mode 100644 execution/commitment/pbin_cell.go create mode 100644 execution/commitment/pbin_cell_test.go diff --git a/docs/plans/20260729-pbin-patricia-hashed.md b/docs/plans/20260729-pbin-patricia-hashed.md index 12239869140..88efe2d7fc7 100644 --- a/docs/plans/20260729-pbin-patricia-hashed.md +++ b/docs/plans/20260729-pbin-patricia-hashed.md @@ -172,12 +172,12 @@ func pbinCommonPrefixBits(a, b *pbinBitpath) int16 // XOR + LeadingZeros64, clam - Create: `execution/commitment/pbin_branch.go` - Create: `execution/commitment/pbin_cell_test.go` -- [ ] write failing cell encode/decode round-trip tests with prefix bit length drawn from `[0, 529)` (guards H4) -- [ ] write failing tests for record decode rejecting inconsistent `prefixBitLen`/byte length and non-zero pad bits (guards H3) -- [ ] define `pbinCell` with a tree-key-space `bitpath` prefix and plain-key fields; **use one prefix, not two** — HPH's `hashedExtension`/`extension` split exists to hold hashed and plain spaces separately, whereas PBin derives the tree key from the plain key on demand. No `stateHash` field: a leaf hash is `H(0x00||key||value)` with nothing to memoize -- [ ] define the grid as `[528][2]pbinCell` with row-indexed arrays `[528]` and depth-indexed arrays `[529]`, plus `reset`/`resetForReuse` clearing `bitLen` -- [ ] implement the PBin branch record codec with `prefixBitLen` as an explicit uvarint **bit** count, always encoding both cells (`bitmap = afterMap`, no merge path) -- [ ] run tests - must pass before task 6 +- [x] write failing cell encode/decode round-trip tests with prefix bit length drawn from `[0, 529)` (guards H4) +- [x] write failing tests for record decode rejecting inconsistent `prefixBitLen`/byte length and non-zero pad bits (guards H3) +- [x] define `pbinCell` with a tree-key-space `bitpath` prefix and plain-key fields; **use one prefix, not two** — HPH's `hashedExtension`/`extension` split exists to hold hashed and plain spaces separately, whereas PBin derives the tree key from the plain key on demand. No `stateHash` field: a leaf hash is `H(0x00||key||value)` with nothing to memoize +- [x] define the grid as `[528][2]pbinCell` with row-indexed arrays `[528]` and depth-indexed arrays `[529]`, plus `reset`/`resetForReuse` clearing `bitLen` +- [x] implement the PBin branch record codec with `prefixBitLen` as an explicit uvarint **bit** count, always encoding both cells (`bitmap = afterMap`, no merge path) +- [x] run tests - must pass before task 6 ### Task 6: node merkelization diff --git a/execution/commitment/pbin_branch.go b/execution/commitment/pbin_branch.go new file mode 100644 index 00000000000..f37c1945bf6 --- /dev/null +++ b/execution/commitment/pbin_branch.go @@ -0,0 +1,237 @@ +// Copyright 2026 The Erigon Authors +// This file is part of Erigon. +// +// Erigon is free software: you can redistribute it and/or modify +// it under the terms of the GNU Lesser General Public License as published by +// the Free Software Foundation, either version 3 of the License, or +// (at your option) any later version. +// +// Erigon is distributed in the hope that it will be useful, +// but WITHOUT ANY WARRANTY; without even the implied warranty of +// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +// GNU Lesser General Public License for more details. +// +// You should have received a copy of the GNU Lesser General Public License +// along with Erigon. If not, see . + +package commitment + +import ( + "encoding/binary" + "errors" + "fmt" + "math/bits" + + "github.com/erigontech/erigon/common/length" +) + +// pbinCellBits are the only child slots a binary node has. +const pbinCellBits = 0b11 + +type pbinCellFields uint8 + +const ( + pbinFieldLeaf pbinCellFields = 1 + pbinFieldBranch pbinCellFields = 2 + pbinFieldAccountAddr pbinCellFields = 4 + pbinFieldStorageAddr pbinCellFields = 8 + pbinFieldHash pbinCellFields = 16 + + pbinFieldsAll = pbinFieldLeaf | pbinFieldBranch | pbinFieldAccountAddr | pbinFieldStorageAddr | pbinFieldHash + pbinFieldKind = pbinFieldLeaf | pbinFieldBranch +) + +var ( + errPBinMalformedBranch = errors.New("pbin: malformed branch record") + errPBinCellMaps = errors.New("pbin: branch maps address more than two cells") +) + +// pbinBranchData is one serialised binary node. It is deliberately not +// BranchData: a 66-byte tree-key prefix does not fit the shared codec's cell +// fields, and PatriciaContext moves branch payloads as opaque bytes. +type pbinBranchData []byte + +// pbinBranchEncoder serialises a binary node. Every record carries both child +// cells, so a record read back replaces its predecessor outright and no +// merge-with-previous path exists — at arity 2 the untouched sibling is the +// whole other half of the subtree, and merging is what loses it. +type pbinBranchEncoder struct { + buf []byte +} + +func (e *pbinBranchEncoder) encode(touchMap, afterMap uint16, cells *[2]pbinCell) (pbinBranchData, error) { + if err := pbinCheckCellMaps(touchMap, afterMap); err != nil { + return nil, err + } + e.buf = binary.BigEndian.AppendUint16(e.buf[:0], touchMap) + e.buf = binary.BigEndian.AppendUint16(e.buf, afterMap) + + var err error + for bitset := afterMap; bitset != 0; { + bit := bitset & -bitset + if e.buf, err = pbinAppendCell(e.buf, &cells[bits.TrailingZeros16(bit)]); err != nil { + return nil, err + } + bitset ^= bit + } + return e.buf, nil +} + +func pbinAppendCell(dst []byte, c *pbinCell) ([]byte, error) { + var fields pbinCellFields + switch c.kind { + case pbinNodeLeaf: + fields = pbinFieldLeaf + case pbinNodeBranch: + fields = pbinFieldBranch + default: + return nil, fmt.Errorf("%w: cell present in afterMap has no node kind", errPBinMalformedBranch) + } + if c.accountAddrLen > 0 { + fields |= pbinFieldAccountAddr + } + if c.storageAddrLen > 0 { + fields |= pbinFieldStorageAddr + } + if c.hashLen > 0 { + fields |= pbinFieldHash + } + + dst = append(dst, byte(fields)) + dst = binary.AppendUvarint(dst, uint64(c.prefix.bitLen)) + dst = c.prefix.appendPackedBits(dst) + + if fields&pbinFieldAccountAddr != 0 { + dst = pbinAppendLenAndVal(dst, c.accountAddr[:c.accountAddrLen]) + } + if fields&pbinFieldStorageAddr != 0 { + dst = pbinAppendLenAndVal(dst, c.storageAddr[:c.storageAddrLen]) + } + if fields&pbinFieldHash != 0 { + dst = pbinAppendLenAndVal(dst, c.hash[:c.hashLen]) + } + return dst, nil +} + +func pbinAppendLenAndVal(dst, val []byte) []byte { + return append(binary.AppendUvarint(dst, uint64(len(val))), val...) +} + +// pbinDecodeBranch fills both cells from a record, rejecting every spelling the +// encoder would not produce so a record has one canonical form. +func pbinDecodeBranch(data []byte, cells *[2]pbinCell) (touchMap, afterMap uint16, err error) { + cells[0].reset() + cells[1].reset() + + if len(data) < 4 { + return 0, 0, fmt.Errorf("%w: %d bytes is shorter than the header", errPBinMalformedBranch, len(data)) + } + touchMap, afterMap = binary.BigEndian.Uint16(data), binary.BigEndian.Uint16(data[2:]) + if err = pbinCheckCellMaps(touchMap, afterMap); err != nil { + return 0, 0, err + } + + pos := 4 + for bitset := afterMap; bitset != 0; { + bit := bitset & -bitset + if pos, err = pbinDecodeCell(data, pos, &cells[bits.TrailingZeros16(bit)]); err != nil { + return 0, 0, err + } + bitset ^= bit + } + if pos != len(data) { + return 0, 0, fmt.Errorf("%w: %d trailing bytes", errPBinMalformedBranch, len(data)-pos) + } + return touchMap, afterMap, nil +} + +func pbinDecodeCell(data []byte, pos int, c *pbinCell) (int, error) { + if pos >= len(data) { + return 0, fmt.Errorf("%w: no cell body at offset %d", errPBinMalformedBranch, pos) + } + fields := pbinCellFields(data[pos]) + pos++ + if fields&^pbinFieldsAll != 0 { + return 0, fmt.Errorf("%w: unknown cell fields %08b", errPBinMalformedBranch, fields) + } + switch fields & pbinFieldKind { + case pbinFieldLeaf: + c.kind = pbinNodeLeaf + case pbinFieldBranch: + c.kind = pbinNodeBranch + default: + return 0, fmt.Errorf("%w: cell fields %08b name no single node kind", errPBinMalformedBranch, fields) + } + + pos, err := pbinDecodePrefix(data, pos, c) + if err != nil { + return 0, err + } + if fields&pbinFieldAccountAddr != 0 { + if pos, err = pbinDecodeFixedVal(data, pos, c.accountAddr[:], length.Addr); err != nil { + return 0, err + } + c.accountAddrLen = length.Addr + } + if fields&pbinFieldStorageAddr != 0 { + if pos, err = pbinDecodeFixedVal(data, pos, c.storageAddr[:], length.Addr+length.Hash); err != nil { + return 0, err + } + c.storageAddrLen = length.Addr + length.Hash + } + if fields&pbinFieldHash != 0 { + if pos, err = pbinDecodeFixedVal(data, pos, c.hash[:], length.Hash); err != nil { + return 0, err + } + c.hashLen = length.Hash + } + return pos, nil +} + +// pbinDecodePrefix reads the explicit bit count and exactly the bytes it +// implies. The count is the authority: a byte length left to speak for itself +// would carry up to seven pad bits into the branch hash. +func pbinDecodePrefix(data []byte, pos int, c *pbinCell) (int, error) { + bitLen, n := binary.Uvarint(data[pos:]) + if n <= 0 { + return 0, fmt.Errorf("%w: unreadable prefix bit count at offset %d", errPBinMalformedBranch, pos) + } + pos += n + if bitLen > pbinMaxPathBits { + return 0, fmt.Errorf("%w: prefix of %d bits exceeds %d", errPBinMalformedBranch, bitLen, pbinMaxPathBits) + } + byteLen := (int(bitLen) + 7) / 8 + if pos+byteLen > len(data) { + return 0, fmt.Errorf("%w: prefix of %d bits needs %d bytes, %d left", errPBinMalformedBranch, bitLen, byteLen, len(data)-pos) + } + if used := bitLen % 8; used != 0 && data[pos+byteLen-1]&(0xFF>>used) != 0 { + return 0, fmt.Errorf("%w: non-zero pad bits after a %d-bit prefix", errPBinMalformedBranch, bitLen) + } + c.prefix = pbinPathFromBits(data[pos:pos+byteLen], int16(bitLen)) + return pos + byteLen, nil +} + +func pbinDecodeFixedVal(data []byte, pos int, dst []byte, want int) (int, error) { + l, n := binary.Uvarint(data[pos:]) + if n <= 0 { + return 0, fmt.Errorf("%w: unreadable value length at offset %d", errPBinMalformedBranch, pos) + } + pos += n + if l != uint64(want) { + return 0, fmt.Errorf("%w: value of %d bytes, want %d", errPBinMalformedBranch, l, want) + } + if pos+want > len(data) { + return 0, fmt.Errorf("%w: value of %d bytes needs more than the %d left", errPBinMalformedBranch, want, len(data)-pos) + } + copy(dst, data[pos:pos+want]) + return pos + want, nil +} + +// pbinCheckCellMaps enforces the arity: a binary node has cells 0 and 1 and +// nothing else, in either map. +func pbinCheckCellMaps(touchMap, afterMap uint16) error { + if (touchMap|afterMap)&^pbinCellBits != 0 { + return fmt.Errorf("%w: touch %016b after %016b", errPBinCellMaps, touchMap, afterMap) + } + return nil +} diff --git a/execution/commitment/pbin_cell.go b/execution/commitment/pbin_cell.go new file mode 100644 index 00000000000..3076c37bea1 --- /dev/null +++ b/execution/commitment/pbin_cell.go @@ -0,0 +1,106 @@ +// Copyright 2026 The Erigon Authors +// This file is part of Erigon. +// +// Erigon is free software: you can redistribute it and/or modify +// it under the terms of the GNU Lesser General Public License as published by +// the Free Software Foundation, either version 3 of the License, or +// (at your option) any later version. +// +// Erigon is distributed in the hope that it will be useful, +// but WITHOUT ANY WARRANTY; without even the implied warranty of +// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +// GNU Lesser General Public License for more details. +// +// You should have received a copy of the GNU Lesser General Public License +// along with Erigon. If not, see . + +package commitment + +import ( + "github.com/erigontech/erigon/common" + "github.com/erigontech/erigon/common/length" +) + +// pbinNodeKind says what a cell points at. EIP-8297 admits a branch whose +// prefix is empty, so the prefix length cannot stand in for the kind. +type pbinNodeKind uint8 + +const ( + pbinNodeEmpty pbinNodeKind = iota + pbinNodeLeaf + pbinNodeBranch +) + +// pbinCell is one of the two child slots of a binary node. +// +// It carries a single prefix, unlike the hex engine's cell: HPH keeps hashed and +// plain key spaces apart because it navigates in one and stores plain keys in +// the other, whereas PBin derives the tree key from the plain key on demand, so +// the one prefix is always tree-key-space bits. There is no memoized leaf hash +// either — H(0x00||key||value) commits the complete key and has nothing worth +// caching. +type pbinCell struct { + prefix pbinBitpath + hash common.Hash + accountAddr common.Address + storageAddr [length.Addr + length.Hash]byte + + accountAddrLen int16 + storageAddrLen int16 + hashLen int16 + kind pbinNodeKind + loaded loadFlags + Update +} + +func (c *pbinCell) reset() { + *c = pbinCell{} + c.Update.Reset() +} + +const ( + // pbinGridRows bounds the active rows: a row consumes at least the bit it + // splits on, so one row per path bit is enough. + pbinGridRows = pbinMaxPathBits + // pbinMaxDepths bounds anything indexed by bit depth, which is inclusive of a + // full-length path and so runs one past the row count. + pbinMaxDepths = pbinMaxPathBits + 1 +) + +// pbinGrid is the unfolded part of the tree: one row per level of descent, two +// cells per row. touchMap/afterMap are uint16 so the OnesCount16 / +// TrailingZeros16 arithmetic ports from the hex engine unchanged; only bits 0 +// and 1 are ever set. +type pbinGrid struct { + root pbinCell + rows [pbinGridRows][2]pbinCell + depths [pbinGridRows]int16 + branchBefore [pbinGridRows]bool + touchMap [pbinGridRows]uint16 + afterMap [pbinGridRows]uint16 + activeRows int +} + +func (g *pbinGrid) reset() { + g.resetRows(len(g.rows)) +} + +// resetForReuse clears only the rows the finished run left live. Rows above +// activeRows keep stale cells, which is safe because unfold initializes a row +// before anything reads it. +func (g *pbinGrid) resetForReuse() { + g.resetRows(g.activeRows) +} + +func (g *pbinGrid) resetRows(rows int) { + g.root.reset() + g.activeRows = 0 + for row := range rows { + g.rows[row][0].reset() + g.rows[row][1].reset() + g.depths[row] = 0 + g.branchBefore[row] = false + g.touchMap[row] = 0 + g.afterMap[row] = 0 + } +} diff --git a/execution/commitment/pbin_cell_test.go b/execution/commitment/pbin_cell_test.go new file mode 100644 index 00000000000..6d5d69dbbcb --- /dev/null +++ b/execution/commitment/pbin_cell_test.go @@ -0,0 +1,350 @@ +// Copyright 2026 The Erigon Authors +// This file is part of Erigon. +// +// Erigon is free software: you can redistribute it and/or modify +// it under the terms of the GNU Lesser General Public License as published by +// the Free Software Foundation, either version 3 of the License, or +// (at your option) any later version. +// +// Erigon is distributed in the hope that it will be useful, +// but WITHOUT ANY WARRANTY; without even the implied warranty of +// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +// GNU Lesser General Public License for more details. +// +// You should have received a copy of the GNU Lesser General Public License +// along with Erigon. If not, see . + +package commitment + +import ( + "bytes" + "encoding/binary" + "testing" + + "github.com/stretchr/testify/require" + + "github.com/erigontech/erigon/common/empty" + "github.com/erigontech/erigon/common/length" +) + +func pbinTestEmptyCell() pbinCell { + var c pbinCell + c.reset() + return c +} + +// pbinTestBranchCell builds a branch-pointing cell with a prefix of the given +// bit length and a distinguishable hash. +func pbinTestBranchCell(pattern byte, bitLen int16) pbinCell { + c := pbinTestEmptyCell() + c.kind = pbinNodeBranch + c.prefix = pbinPathFromBits(bytes.Repeat([]byte{pattern}, 66), bitLen) + for i := range c.hash { + c.hash[i] = pattern ^ byte(i) + } + c.hashLen = length.Hash + return c +} + +// pbinTestLeafCell builds a leaf cell carrying a storage plain key, the widest +// plain key a cell holds. +func pbinTestLeafCell(pattern byte, bitLen int16) pbinCell { + c := pbinTestBranchCell(pattern, bitLen) + c.kind = pbinNodeLeaf + for i := range c.storageAddr { + c.storageAddr[i] = pattern + byte(i) + } + c.storageAddrLen = length.Addr + length.Hash + return c +} + +// A prefix of any admissible bit length must survive a record round-trip: the +// 66-byte storage path does not fit the shared codec's fields, and a silent +// truncation would commit a wrong root (guards H4). +func TestPBinBranchCodecRoundTripPrefixBitLengths(t *testing.T) { + t.Parallel() + + var enc pbinBranchEncoder + for bitLen := int16(0); bitLen <= pbinMaxPathBits; bitLen++ { + cells := [2]pbinCell{ + pbinTestBranchCell(0xA5, bitLen), + pbinTestLeafCell(0x5A, pbinMaxPathBits-bitLen), + } + + rec, err := enc.encode(0b11, 0b11, &cells) + require.NoErrorf(t, err, "bitLen %d", bitLen) + + var got [2]pbinCell + touchMap, afterMap, err := pbinDecodeBranch(bytes.Clone(rec), &got) + require.NoErrorf(t, err, "bitLen %d", bitLen) + require.Equal(t, uint16(0b11), touchMap) + require.Equal(t, uint16(0b11), afterMap) + require.Equalf(t, cells, got, "bitLen %d", bitLen) + } +} + +func TestPBinBranchCodecRoundTripCellShapes(t *testing.T) { + t.Parallel() + + accountLeaf := pbinTestEmptyCell() + accountLeaf.kind = pbinNodeLeaf + accountLeaf.prefix = pbinPathFromBits(bytes.Repeat([]byte{0x11}, 66), 17) + copy(accountLeaf.accountAddr[:], bytes.Repeat([]byte{0x42}, length.Addr)) + accountLeaf.accountAddrLen = length.Addr + + for _, tc := range []struct { + name string + touchMap uint16 + afterMap uint16 + cells [2]pbinCell + }{ + {"both branches", 0b11, 0b11, [2]pbinCell{pbinTestBranchCell(0x01, 3), pbinTestBranchCell(0x02, 528)}}, + {"leaf and branch", 0b11, 0b11, [2]pbinCell{pbinTestLeafCell(0x03, 271), pbinTestBranchCell(0x04, 5)}}, + {"hashless account leaf", 0b11, 0b11, [2]pbinCell{accountLeaf, pbinTestLeafCell(0x05, 64)}}, + {"only the right cell present", 0b10, 0b10, [2]pbinCell{pbinTestEmptyCell(), pbinTestBranchCell(0x07, 9)}}, + {"deleted left cell", 0b11, 0b10, [2]pbinCell{pbinTestEmptyCell(), pbinTestBranchCell(0x08, 9)}}, + } { + t.Run(tc.name, func(t *testing.T) { + t.Parallel() + + var enc pbinBranchEncoder + rec, err := enc.encode(tc.touchMap, tc.afterMap, &tc.cells) + require.NoError(t, err) + + var got [2]pbinCell + touchMap, afterMap, err := pbinDecodeBranch(rec, &got) + require.NoError(t, err) + require.Equal(t, tc.touchMap, touchMap) + require.Equal(t, tc.afterMap, afterMap) + require.Equal(t, tc.cells, got) + }) + } +} + +// The record is self-contained by construction: re-encoding what was decoded +// must reproduce the bytes, so no merge-with-previous path can be needed. +func TestPBinBranchCodecIsCanonical(t *testing.T) { + t.Parallel() + + cells := [2]pbinCell{pbinTestLeafCell(0x7C, 33), pbinTestBranchCell(0x3E, 528)} + + var enc pbinBranchEncoder + rec, err := enc.encode(0b11, 0b11, &cells) + require.NoError(t, err) + want := bytes.Clone(rec) + + var got [2]pbinCell + _, _, err = pbinDecodeBranch(want, &got) + require.NoError(t, err) + + again, err := enc.encode(0b11, 0b11, &got) + require.NoError(t, err) + require.Equal(t, want, []byte(again)) +} + +// pbinTestRecord assembles a record by hand so decode can be probed with bytes +// the encoder would never produce. +func pbinTestRecord(touchMap, afterMap uint16, bodies ...[]byte) []byte { + rec := make([]byte, 4) + binary.BigEndian.PutUint16(rec, touchMap) + binary.BigEndian.PutUint16(rec[2:], afterMap) + for _, b := range bodies { + rec = append(rec, b...) + } + return rec +} + +// pbinTestCellBody spells one cell body: fields, uvarint bit count, then the +// caller's raw prefix bytes — deliberately not derived from the bit count. +func pbinTestCellBody(fields pbinCellFields, prefixBitLen uint64, prefix []byte, tail ...byte) []byte { + body := []byte{byte(fields)} + body = binary.AppendUvarint(body, prefixBitLen) + body = append(body, prefix...) + return append(body, tail...) +} + +func pbinTestLenAndVal(val []byte) []byte { + return append(binary.AppendUvarint(nil, uint64(len(val))), val...) +} + +// A declared bit count that disagrees with the bytes behind it must be +// rejected rather than read as a shorter or longer prefix: the prefix is inside +// the branch hash, so spurious pad bits silently change the root (guards H3). +func TestPBinBranchDecodeRejects(t *testing.T) { + t.Parallel() + + body := pbinTestCellBody(pbinFieldBranch, 8, []byte{0xFF}) + + for _, tc := range []struct { + name string + rec []byte + }{ + {"truncated header", []byte{0x00, 0x03, 0x00}}, + {"cell bit outside the arity", pbinTestRecord(0b100, 0b100, body)}, + {"touched bit outside the arity", pbinTestRecord(0b1011, 0b11, body, body)}, + {"missing cell body", pbinTestRecord(0b11, 0b11, body)}, + {"unknown field bit", pbinTestRecord(0b01, 0b01, pbinTestCellBody(0x80, 0, nil))}, + {"no node kind", pbinTestRecord(0b01, 0b01, pbinTestCellBody(0, 0, nil))}, + {"both node kinds", pbinTestRecord(0b01, 0b01, pbinTestCellBody(pbinFieldBranch|pbinFieldLeaf, 0, nil))}, + {"prefix shorter than its bit count", pbinTestRecord(0b01, 0b01, pbinTestCellBody(pbinFieldBranch, 16, []byte{0xFF}))}, + {"prefix longer than its bit count", pbinTestRecord(0b01, 0b01, pbinTestCellBody(pbinFieldBranch, 8, []byte{0xFF, 0xFF}))}, + {"non-zero pad bits", pbinTestRecord(0b01, 0b01, pbinTestCellBody(pbinFieldBranch, 3, []byte{0xFF}))}, + {"bit count beyond the longest path", pbinTestRecord(0b01, 0b01, pbinTestCellBody(pbinFieldBranch, pbinMaxPathBits+1, bytes.Repeat([]byte{0xFF}, 67)))}, + {"truncated uvarint", pbinTestRecord(0b01, 0b01, []byte{byte(pbinFieldBranch), 0x80})}, + {"hash longer than a digest", pbinTestRecord(0b01, 0b01, pbinTestCellBody(pbinFieldBranch|pbinFieldHash, 0, nil, pbinTestLenAndVal(bytes.Repeat([]byte{0xEE}, 33))...))}, + {"truncated hash", pbinTestRecord(0b01, 0b01, pbinTestCellBody(pbinFieldBranch|pbinFieldHash, 0, nil, 32, 0xEE))}, + {"account address of the wrong length", pbinTestRecord(0b01, 0b01, pbinTestCellBody(pbinFieldLeaf|pbinFieldAccountAddr, 0, nil, pbinTestLenAndVal(bytes.Repeat([]byte{0xEE}, 21))...))}, + {"storage address of the wrong length", pbinTestRecord(0b01, 0b01, pbinTestCellBody(pbinFieldLeaf|pbinFieldStorageAddr, 0, nil, pbinTestLenAndVal(bytes.Repeat([]byte{0xEE}, 51))...))}, + {"trailing bytes", pbinTestRecord(0b01, 0b01, pbinTestCellBody(pbinFieldBranch, 0, nil), []byte{0x00})}, + } { + t.Run(tc.name, func(t *testing.T) { + t.Parallel() + var cells [2]pbinCell + _, _, err := pbinDecodeBranch(tc.rec, &cells) + require.Error(t, err) + }) + } +} + +func TestPBinBranchEncodeRejects(t *testing.T) { + t.Parallel() + + for _, tc := range []struct { + name string + touchMap uint16 + afterMap uint16 + cells [2]pbinCell + }{ + {"cell bit outside the arity", 0b100, 0b100, [2]pbinCell{}}, + {"touched bit outside the arity", 0b1011, 0b11, [2]pbinCell{pbinTestBranchCell(1, 1), pbinTestBranchCell(2, 1)}}, + {"present cell with no node kind", 0b01, 0b01, [2]pbinCell{}}, + } { + t.Run(tc.name, func(t *testing.T) { + t.Parallel() + var enc pbinBranchEncoder + _, err := enc.encode(tc.touchMap, tc.afterMap, &tc.cells) + require.Error(t, err) + }) + } +} + +// A record carries keys and hashes, never state, so a decoded cell must come +// back unloaded no matter what the encoder was handed. +func TestPBinBranchCodecDropsLoadedState(t *testing.T) { + t.Parallel() + + cells := [2]pbinCell{pbinTestLeafCell(0x2B, 40), pbinTestBranchCell(0x4D, 8)} + cells[0].loaded = cellLoadStorage + cells[0].Nonce = 9 + cells[0].Flags = NonceUpdate + + var enc pbinBranchEncoder + rec, err := enc.encode(0b11, 0b11, &cells) + require.NoError(t, err) + + var got [2]pbinCell + _, _, err = pbinDecodeBranch(bytes.Clone(rec), &got) + require.NoError(t, err) + require.Equal(t, cellLoadNone, got[0].loaded) + require.Zero(t, got[0].Nonce) + require.Zero(t, got[0].Flags) +} + +// Decoding into a reused cell must not leave any bits of the previous prefix +// behind — a stale bitLen would extend the new prefix with foreign bits. +func TestPBinBranchDecodeClearsReusedCells(t *testing.T) { + t.Parallel() + + cells := [2]pbinCell{pbinTestLeafCell(0xFF, 528), pbinTestLeafCell(0xFF, 528)} + want := [2]pbinCell{pbinTestBranchCell(0x0F, 3), pbinTestBranchCell(0xF0, 0)} + + var enc pbinBranchEncoder + rec, err := enc.encode(0b11, 0b11, &want) + require.NoError(t, err) + + _, _, err = pbinDecodeBranch(bytes.Clone(rec), &cells) + require.NoError(t, err) + require.Equal(t, want, cells) +} + +func TestPBinCellReset(t *testing.T) { + t.Parallel() + + c := pbinTestLeafCell(0xC3, 271) + c.Nonce = 7 + c.Balance.SetUint64(11) + c.Flags = BalanceUpdate | NonceUpdate + + c.reset() + require.Equal(t, int16(0), c.prefix.bitLen) + require.Zero(t, c.prefix.w) + require.Equal(t, empty.CodeHash, c.CodeHash) + require.Equal(t, pbinTestEmptyCell(), c) +} + +func pbinTestFillGrid(g *pbinGrid, rows int) { + g.activeRows = rows + g.root = pbinTestBranchCell(0x99, 5) + for row := range rows { + g.rows[row][0] = pbinTestLeafCell(byte(row), 271) + g.rows[row][1] = pbinTestBranchCell(byte(row), 33) + g.depths[row] = int16(row * 7) + g.branchBefore[row] = true + g.touchMap[row] = 0b11 + g.afterMap[row] = 0b10 + } +} + +func pbinTestRequireRowEmpty(t *testing.T, g *pbinGrid, row int) { + t.Helper() + require.Equal(t, pbinTestEmptyCell(), g.rows[row][0]) + require.Equal(t, pbinTestEmptyCell(), g.rows[row][1]) + require.Zero(t, g.depths[row]) + require.False(t, g.branchBefore[row]) + require.Zero(t, g.touchMap[row]) + require.Zero(t, g.afterMap[row]) +} + +func TestPBinGridReset(t *testing.T) { + t.Parallel() + + g := new(pbinGrid) + pbinTestFillGrid(g, 3) + g.reset() + + require.Zero(t, g.activeRows) + require.Equal(t, pbinTestEmptyCell(), g.root) + for row := range 3 { + pbinTestRequireRowEmpty(t, g, row) + } +} + +// resetForReuse only has to clear what the finished run left live; rows above +// activeRows are initialized by unfold before anything reads them. +func TestPBinGridResetForReuse(t *testing.T) { + t.Parallel() + + g := new(pbinGrid) + pbinTestFillGrid(g, 3) + stale := g.rows[2][0] + g.activeRows = 2 + g.resetForReuse() + + require.Zero(t, g.activeRows) + require.Equal(t, pbinTestEmptyCell(), g.root) + pbinTestRequireRowEmpty(t, g, 0) + pbinTestRequireRowEmpty(t, g, 1) + require.Equal(t, stale, g.rows[2][0]) +} + +// A row consumes at least the bit it splits on, so 528 rows cover the deepest +// path; depth is inclusive of a full-length path and needs one entry more. +func TestPBinGridBounds(t *testing.T) { + t.Parallel() + + g := new(pbinGrid) + require.Equal(t, pbinMaxPathBits, len(g.rows)) + require.Equal(t, pbinGridRows, len(g.depths)) + require.Equal(t, 2, len(g.rows[0])) + require.Equal(t, pbinGridRows+1, pbinMaxDepths) +} From 88ea19224e5bb22e29940aaf4cd087779197e80e Mon Sep 17 00:00:00 2001 From: awskii Date: Wed, 29 Jul 2026 17:56:32 +0700 Subject: [PATCH 07/56] feat: EIP-8297 node merkelization Add the binary tree's two node hashes and the single cell hasher they back. - pbinLeafHash = H(0x00 || key || value) over the complete 34/66-byte tree key, so a leaf's hash is independent of where it sits. - pbinBranchHash = H(0x01 || encode_bit_prefix(prefix) || left || right), with encode_bit_prefix's explicit two-byte bit count keeping a 7-bit prefix distinct from an 8-bit one agreeing on the pad bit. - One 133-byte scratch buffer (tag + count + 66-byte prefix + two children) holds every preimage either shape produces, so a node costs one hash call and no allocation. - Exactly one cell hasher: cellHash dispatches on node kind and derives a leaf's complete key from the descent path plus the cell prefix. That key is also self-describing enough to pick the leaf value, so no extra cell field is needed to tell BASIC_DATA, CODE_HASH and a header-resident slot apart. - The empty subtree is 32 zero bytes per eip:208, deliberately not empty.RootHash. Tests diff every shape against the Task 4 reference oracle: single leaf, a branch at eight prefix lengths including empty and the deepest a 528-bit key admits, a nested branch, an absent child, and two-key corpus roots folded by hand through the primitives. M0 gap: the shared Update carries no code size, and extending it is an external API change, so BASIC_DATA encodes code_size as 0 until code chunking lands. Engine and oracle share the encoder, so the M0 root-equality gate is unaffected. --- docs/plans/20260729-pbin-patricia-hashed.md | 14 +- execution/commitment/pbin_hash.go | 145 +++++++++ execution/commitment/pbin_hash_test.go | 332 ++++++++++++++++++++ 3 files changed, 484 insertions(+), 7 deletions(-) create mode 100644 execution/commitment/pbin_hash.go create mode 100644 execution/commitment/pbin_hash_test.go diff --git a/docs/plans/20260729-pbin-patricia-hashed.md b/docs/plans/20260729-pbin-patricia-hashed.md index 88efe2d7fc7..f06c9c55d64 100644 --- a/docs/plans/20260729-pbin-patricia-hashed.md +++ b/docs/plans/20260729-pbin-patricia-hashed.md @@ -185,12 +185,12 @@ func pbinCommonPrefixBits(a, b *pbinBitpath) int16 // XOR + LeadingZeros64, clam - Create: `execution/commitment/pbin_hash.go` - Create: `execution/commitment/pbin_hash_test.go` -- [ ] write failing tests asserting each node hash matches the Task 4 oracle for hand-built shapes: single leaf, one branch, nested branch with non-empty prefix, branch with **empty** prefix -- [ ] write a failing node-level test asserting the empty subtree is 32 zero bytes, explicitly not `empty.RootHash` (guards H11) -- [ ] implement `pbinLeafHash = H(0x00 || key || value)` over the complete 34/66-byte key -- [ ] implement `pbinBranchHash = H(0x01 || encode_bit_prefix(prefix) || left || right)` with one scratch buffer sized 133 B (1 tag + 2 count + 66 prefix + 64 children) -- [ ] implement exactly **one** cell hasher — do not port both `computeCellHash` and `witnessComputeCellHashWithStorage` (guards H14) -- [ ] run tests - must pass before task 7 +- [x] write failing tests asserting each node hash matches the Task 4 oracle for hand-built shapes: single leaf, one branch, nested branch with non-empty prefix, branch with **empty** prefix +- [x] write a failing node-level test asserting the empty subtree is 32 zero bytes, explicitly not `empty.RootHash` (guards H11) +- [x] implement `pbinLeafHash = H(0x00 || key || value)` over the complete 34/66-byte key +- [x] implement `pbinBranchHash = H(0x01 || encode_bit_prefix(prefix) || left || right)` with one scratch buffer sized 133 B (1 tag + 2 count + 66 prefix + 64 children) +- [x] implement exactly **one** cell hasher — do not port both `computeCellHash` and `witnessComputeCellHashWithStorage` (guards H14) +- [x] run tests - must pass before task 7 ### Task 7: unfold and needUnfolding @@ -290,7 +290,7 @@ func pbinCommonPrefixBits(a, b *pbinBitpath) int16 // XOR + LeadingZeros64, clam - Record the one-prefix-per-cell rationale (Task 5) here and in the commit body rather than as a source comment. **Out of scope, in rough dependency order:** -- Code chunks (`chunkify_code`, `eip:374-397`), including the stateful PUSHDATA boundary byte and content-addressed overflow chunks shared between contracts. +- Code chunks (`chunkify_code`, `eip:374-397`), including the stateful PUSHDATA boundary byte and content-addressed overflow chunks shared between contracts. Discovered in Task 6: `Update` carries **no code size**, and adding one is an external API change, so M0 encodes BASIC_DATA `code_size` as 0. Both the engine and the oracle see the same value, so the M0 gate still holds, but a conformance claim needs a real code size sourced alongside code chunking. - Deletion semantics. EIP-8297 never removes entries, but erigon's `StorageDomain` represents never-written and explicitly-zeroed identically (`execution/state/rw_v3.go:965` calls `DomainDel` on an empty value). Production needs a tombstone-capable encoding or a documented deviation. Under EIP-8297 SELFDESTRUCT must **not** remove storage leaves, which removes the rationale for erigon's storage-subtree collapse. - Commitment state save/restore (re-arms H6). `SetState`/`EncodeCurrentState` are concrete `*HexPatriciaHashed` methods and `commitmentdb` type-switches on them (`commitment_context.go:895-901`, `:935-949`, panics at `:103`, silently no-ops `SetCollapseTracer` at `:411`). Promoting a `StatefulTrie` interface is an external API change, deliberately excluded from M0; `:411` should error rather than no-op before any variant ships. - Parallel mounting. `mountedNib 0..15` plus a depth-63 fold wall does not translate to arity 2; a 2-way root split silently serialises rather than failing. diff --git a/execution/commitment/pbin_hash.go b/execution/commitment/pbin_hash.go new file mode 100644 index 00000000000..59c7bd5cb48 --- /dev/null +++ b/execution/commitment/pbin_hash.go @@ -0,0 +1,145 @@ +// Copyright 2026 The Erigon Authors +// This file is part of Erigon. +// +// Erigon is free software: you can redistribute it and/or modify +// it under the terms of the GNU Lesser General Public License as published by +// the Free Software Foundation, either version 3 of the License, or +// (at your option) any later version. +// +// Erigon is distributed in the hope that it will be useful, +// but WITHOUT ANY WARRANTY; without even the implied warranty of +// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +// GNU Lesser General Public License for more details. +// +// You should have received a copy of the GNU Lesser General Public License +// along with Erigon. If not, see . + +package commitment + +import ( + "encoding/binary" + "errors" + "fmt" + + keccak "github.com/erigontech/fastkeccak" + + "github.com/erigontech/erigon/common" + "github.com/erigontech/erigon/common/length" +) + +// Node tags separating the two preimage shapes EIP-8297 defines (eip:191-206). +const ( + pbinLeafTag = 0x00 + pbinBranchTag = 0x01 + + // pbinHashBufLen holds the longest preimage either node shape produces: the + // branch tag, the two-byte prefix bit count, the longest encodable prefix and + // both child hashes. + pbinHashBufLen = 1 + 2 + (pbinMaxPathBits+7)/8 + 2*length.Hash +) + +// pbinEmptyTreeHash is the hash of an absent subtree: 32 zero bytes (eip:208). +// It is not empty.RootHash — that constant is the RLP empty-string MPT root and +// substituting it here would silently produce a different tree. +var pbinEmptyTreeHash common.Hash + +var errPBinCellHash = errors.New("pbin: cell cannot be hashed") + +// pbinHasher is the one place H is applied, so swapping the hash function is a +// change to this type alone. Every preimage fits its single scratch buffer, so +// each node costs one hash call and no allocation. Its zero value is ready. +type pbinHasher struct { + buf [pbinHashBufLen]byte +} + +// pbinAppendBitPrefix is the spec's encode_bit_prefix (eip:196-201): a two-byte +// big-endian bit count, then the bits MSB-first zero-padded to a byte boundary. +// The count is what keeps a 7-bit prefix distinct from an 8-bit one that agrees +// with it on the pad bit. +func pbinAppendBitPrefix(dst []byte, p *pbinBitpath) []byte { + return p.appendPackedBits(binary.BigEndian.AppendUint16(dst, uint16(p.bitLen))) +} + +// leafHash is H(0x00 || key || value) over the complete tree key, so a leaf's +// hash does not depend on where in the tree it sits. +func (h *pbinHasher) leafHash(key, value []byte) common.Hash { + if len(key) != pbinAccountKeyLength && len(key) != pbinStorageKeyLength { + panic(fmt.Sprintf("pbin: leaf key of %d bytes is neither zone length", len(key))) + } + if len(value) != pbinValueLength { + panic(fmt.Sprintf("pbin: leaf value of %d bytes, want %d", len(value), pbinValueLength)) + } + buf := append(h.buf[:0], pbinLeafTag) + buf = append(buf, key...) + buf = append(buf, value...) + return keccak.Sum256(buf) +} + +// branchHash is H(0x01 || encode_bit_prefix(prefix) || left || right). An absent +// child passes pbinEmptyTreeHash rather than being omitted. +func (h *pbinHasher) branchHash(prefix *pbinBitpath, left, right *common.Hash) common.Hash { + buf := pbinAppendBitPrefix(append(h.buf[:0], pbinBranchTag), prefix) + buf = append(buf, left[:]...) + buf = append(buf, right[:]...) + return keccak.Sum256(buf) +} + +// cellHash is the only way a cell becomes a hash. Keeping it single is what +// stops a second hasher drifting from this one. +// +// path is the descent to the cell; a leaf's complete key is path followed by the +// cell's own prefix, which is also what tells the leaf value apart. +func (h *pbinHasher) cellHash(c *pbinCell, path *pbinBitpath) (common.Hash, error) { + switch c.kind { + case pbinNodeEmpty: + return pbinEmptyTreeHash, nil + case pbinNodeBranch: + if c.hashLen != length.Hash { + return common.Hash{}, fmt.Errorf("%w: branch cell holds %d hash bytes", errPBinCellHash, c.hashLen) + } + return c.hash, nil + case pbinNodeLeaf: + return h.leafCellHash(c, path) + default: + return common.Hash{}, fmt.Errorf("%w: unknown node kind %d", errPBinCellHash, c.kind) + } +} + +func (h *pbinHasher) leafCellHash(c *pbinCell, path *pbinBitpath) (common.Hash, error) { + full := *path + if int(full.bitLen)+int(c.prefix.bitLen) > pbinMaxPathBits { + return common.Hash{}, fmt.Errorf("%w: leaf key of %d+%d bits overflows", errPBinCellHash, full.bitLen, c.prefix.bitLen) + } + full.append(&c.prefix) + if full.bitLen != pbinAccountKeyLength*8 && full.bitLen != pbinStorageKeyLength*8 { + return common.Hash{}, fmt.Errorf("%w: leaf key of %d bits is neither zone length", errPBinCellHash, full.bitLen) + } + + buf := full.appendPackedBits(append(h.buf[:0], pbinLeafTag)) + value, err := pbinLeafValue(buf[1:], &c.Update) + if err != nil { + return common.Hash{}, err + } + return keccak.Sum256(append(buf, value[:]...)), nil +} + +// pbinLeafValue picks the encoding the key's own position names: the zone byte +// separates storage from the account header, and within the header the +// sub-index selects between BASIC_DATA, CODE_HASH and a header-resident slot. +func pbinLeafValue(key []byte, u *Update) ([pbinValueLength]byte, error) { + if key[0] == pbinStorageZone { + return pbinEncodeStorageValue(u.Storage[:u.StorageLen]), nil + } + switch subIndex := key[len(key)-1]; { + case subIndex == pbinBasicDataLeafKey: + // code_size stays zero while code chunking is out of scope: the shared + // Update carries no code size and adding one is an external API change. + return pbinEncodeBasicData(u.Nonce, &u.Balance, 0) + case subIndex == pbinCodeHashLeafKey: + return pbinCodeHashValue(u.CodeHash), nil + case subIndex >= pbinHeaderStorageOffset && subIndex < pbinCodeOffset: + return pbinEncodeStorageValue(u.Storage[:u.StorageLen]), nil + default: + return [pbinValueLength]byte{}, fmt.Errorf("%w: account-zone sub-index %d names no leaf", errPBinCellHash, subIndex) + } +} diff --git a/execution/commitment/pbin_hash_test.go b/execution/commitment/pbin_hash_test.go new file mode 100644 index 00000000000..b67e3516359 --- /dev/null +++ b/execution/commitment/pbin_hash_test.go @@ -0,0 +1,332 @@ +// Copyright 2026 The Erigon Authors +// This file is part of Erigon. +// +// Erigon is free software: you can redistribute it and/or modify +// it under the terms of the GNU Lesser General Public License as published by +// the Free Software Foundation, either version 3 of the License, or +// (at your option) any later version. +// +// Erigon is distributed in the hope that it will be useful, +// but WITHOUT ANY WARRANTY; without even the implied warranty of +// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +// GNU Lesser General Public License for more details. +// +// You should have received a copy of the GNU Lesser General Public License +// along with Erigon. If not, see . + +package commitment + +import ( + "testing" + + "github.com/holiman/uint256" + "github.com/stretchr/testify/require" + + "github.com/erigontech/erigon/common" + "github.com/erigontech/erigon/common/empty" +) + +func pbinTestPathFromBits(t *testing.T, bits []byte) pbinBitpath { + t.Helper() + require.LessOrEqual(t, len(bits), pbinMaxPathBits) + var p pbinBitpath + for i, b := range bits { + p.setBitAt(int16(i), uint64(b)) + } + p.bitLen = int16(len(bits)) + return p +} + +// pbinTestBitSpec reads a "1011" style literal into the oracle's one-bit-per-byte +// form, so a test can name a short prefix by writing it out. +func pbinTestBitSpec(t *testing.T, spec string) []byte { + t.Helper() + bits := make([]byte, 0, len(spec)) + for _, r := range spec { + switch r { + case '0': + bits = append(bits, 0) + case '1': + bits = append(bits, 1) + default: + t.Fatalf("bit spec %q holds %q", spec, r) + } + } + return bits +} + +func pbinTestBitPattern(n int) []byte { + bits := make([]byte, n) + for i := range bits { + bits[i] = byte((i*7 + i/3) & 1) + } + return bits +} + +func pbinTestOracleLeaf(addr, slot uint64) *pbinOracleLeaf { + return &pbinOracleLeaf{ + key: pbinTreeKeyStorage(pbinOracleAddr(addr), pbinOracleSlot(slot)), + value: pbinOracleValue(addr*1000 + slot), + } +} + +// TestPBinEmptyTreeHash guards H11: EIP-8297's empty subtree is 32 zero bytes +// (eip:208), not the empty-MPT root the rest of erigon reaches for. +func TestPBinEmptyTreeHash(t *testing.T) { + t.Parallel() + + require.Equal(t, make([]byte, 32), pbinEmptyTreeHash[:]) + require.NotEqual(t, empty.RootHash, pbinEmptyTreeHash) + + var h pbinHasher + var c pbinCell + var path pbinBitpath + got, err := h.cellHash(&c, &path) + require.NoError(t, err) + require.Equal(t, pbinEmptyTreeHash, got) + require.NotEqual(t, empty.RootHash, got) +} + +// TestPBinAppendBitPrefixMatchesOracle checks the engine's encode_bit_prefix +// against the spec transcription at every length where padding can go wrong. +func TestPBinAppendBitPrefixMatchesOracle(t *testing.T) { + t.Parallel() + + for _, n := range []int{0, 1, 7, 8, 9, 15, 16, 17, 63, 64, 65, 255, 256, 271, 272, 527, pbinMaxPathBits} { + bits := pbinTestBitPattern(n) + path := pbinTestPathFromBits(t, bits) + require.Equal(t, pbinOracleEncodeBitPrefix(bits), pbinAppendBitPrefix(nil, &path), "%d bits", n) + } +} + +func TestPBinLeafHashMatchesOracle(t *testing.T) { + t.Parallel() + + var h pbinHasher + for _, tc := range []struct { + name string + leaf *pbinOracleLeaf + }{ + { + name: "account key", + leaf: &pbinOracleLeaf{ + key: pbinTreeKeyAccount(pbinOracleAddr(1), pbinBasicDataLeafKey), + value: pbinOracleValue(1), + }, + }, + { + name: "storage key", + leaf: pbinTestOracleLeaf(2, 1000), + }, + } { + t.Run(tc.name, func(t *testing.T) { + want := pbinOracleMerkelize(tc.leaf) + require.Equal(t, common.Hash(want), h.leafHash(tc.leaf.key, tc.leaf.value)) + }) + } +} + +func TestPBinBranchHashMatchesOracle(t *testing.T) { + t.Parallel() + + var h pbinHasher + left, right := pbinTestOracleLeaf(1, 0), pbinTestOracleLeaf(2, 0) + leftHash := h.leafHash(left.key, left.value) + rightHash := h.leafHash(right.key, right.value) + + for _, tc := range []struct { + name string + bits []byte + }{ + {name: "empty prefix", bits: nil}, + {name: "one bit", bits: pbinTestBitSpec(t, "1")}, + {name: "seven bits", bits: pbinTestBitSpec(t, "1011010")}, + {name: "eight bits", bits: pbinTestBitSpec(t, "10110101")}, + {name: "nine bits", bits: pbinTestBitSpec(t, "101101011")}, + {name: "one word", bits: pbinTestBitPattern(64)}, + {name: "past one word", bits: pbinTestBitPattern(65)}, + {name: "deepest branch a 528-bit key admits", bits: pbinTestBitPattern(pbinMaxPathBits - 1)}, + } { + t.Run(tc.name, func(t *testing.T) { + want := pbinOracleMerkelize(&pbinOracleBranch{prefix: tc.bits, left: left, right: right}) + path := pbinTestPathFromBits(t, tc.bits) + require.Equal(t, common.Hash(want), h.branchHash(&path, &leftHash, &rightHash)) + }) + } +} + +// TestPBinNestedBranchHashMatchesOracle folds a two-level shape bottom-up the +// way the engine will, so a branch hash feeding another branch is covered and +// not just a branch over two leaves. +func TestPBinNestedBranchHashMatchesOracle(t *testing.T) { + t.Parallel() + + var h pbinHasher + a, b, c := pbinTestOracleLeaf(1, 0), pbinTestOracleLeaf(2, 0), pbinTestOracleLeaf(3, 0) + innerBits := pbinTestBitSpec(t, "10110") + outerBits := pbinTestBitSpec(t, "011") + + inner := &pbinOracleBranch{prefix: innerBits, left: a, right: b} + outer := &pbinOracleBranch{prefix: outerBits, left: inner, right: c} + want := pbinOracleMerkelize(outer) + + aHash := h.leafHash(a.key, a.value) + bHash := h.leafHash(b.key, b.value) + cHash := h.leafHash(c.key, c.value) + innerPath := pbinTestPathFromBits(t, innerBits) + innerHash := h.branchHash(&innerPath, &aHash, &bHash) + outerPath := pbinTestPathFromBits(t, outerBits) + + require.Equal(t, common.Hash(want), h.branchHash(&outerPath, &innerHash, &cHash)) +} + +// TestPBinBranchHashEmptyChild pins that an absent child contributes the +// empty-subtree constant rather than being skipped. +func TestPBinBranchHashEmptyChild(t *testing.T) { + t.Parallel() + + var h pbinHasher + leaf := pbinTestOracleLeaf(4, 7) + leafHash := h.leafHash(leaf.key, leaf.value) + bits := pbinTestBitSpec(t, "0101") + + want := pbinOracleMerkelize(&pbinOracleBranch{prefix: bits, left: leaf, right: nil}) + path := pbinTestPathFromBits(t, bits) + require.Equal(t, common.Hash(want), h.branchHash(&path, &leafHash, &pbinEmptyTreeHash)) +} + +func TestPBinCellHashBranch(t *testing.T) { + t.Parallel() + + var h pbinHasher + var path pbinBitpath + + t.Run("returns the stored hash", func(t *testing.T) { + c := pbinCell{kind: pbinNodeBranch, hash: common.Hash{0xAB}, hashLen: 32} + got, err := h.cellHash(&c, &path) + require.NoError(t, err) + require.Equal(t, c.hash, got) + }) + t.Run("rejects a branch cell with no hash", func(t *testing.T) { + c := pbinCell{kind: pbinNodeBranch} + _, err := h.cellHash(&c, &path) + require.ErrorIs(t, err, errPBinCellHash) + }) +} + +func TestPBinCellHashLeaf(t *testing.T) { + t.Parallel() + + addr := pbinOracleAddr(9) + storageKey := pbinTreeKeyStorage(addr, pbinOracleSlot(1000)) + headerSlotKey := pbinTreeKeyStorage(addr, pbinOracleSlot(5)) + codeHash := common.Hash{0xC0, 0xDE} + + balance := new(uint256.Int).SetUint64(0xDEADBEEF) + + basicData, err := pbinEncodeBasicData(7, balance, 0) + require.NoError(t, err) + + for _, tc := range []struct { + name string + key []byte + cell pbinCell + value [pbinValueLength]byte + }{ + { + name: "BASIC_DATA", + key: pbinTreeKeyAccount(addr, pbinBasicDataLeafKey), + cell: pbinCell{Update: Update{Nonce: 7, Balance: *balance}}, + value: basicData, + }, + { + name: "CODE_HASH", + key: pbinTreeKeyAccount(addr, pbinCodeHashLeafKey), + cell: pbinCell{Update: Update{CodeHash: codeHash}}, + value: pbinCodeHashValue(codeHash), + }, + { + name: "header-zone storage slot", + key: headerSlotKey, + cell: pbinCell{Update: Update{Storage: common.Hash{0x11, 0x22}, StorageLen: 2}}, + value: pbinEncodeStorageValue([]byte{0x11, 0x22}), + }, + { + name: "storage-zone slot", + key: storageKey, + cell: pbinCell{Update: Update{Storage: common.Hash{0x33}, StorageLen: 1}}, + value: pbinEncodeStorageValue([]byte{0x33}), + }, + } { + t.Run(tc.name, func(t *testing.T) { + var h pbinHasher + full := pbinPathFromBytes(tc.key) + // Split the key so that both the descent path and the cell prefix carry + // real bits: the complete key is their concatenation, not either alone. + const split = 100 + path := full.slice(0, split) + cell := tc.cell + cell.kind = pbinNodeLeaf + cell.prefix = full.slice(split, full.bitLen) + + got, err := h.cellHash(&cell, &path) + require.NoError(t, err) + + want := pbinOracleMerkelize(&pbinOracleLeaf{key: tc.key, value: tc.value[:]}) + require.Equal(t, common.Hash(want), got) + }) + } +} + +func TestPBinCellHashRejectsMalformedLeaf(t *testing.T) { + t.Parallel() + + var h pbinHasher + key := pbinTreeKeyAccount(pbinOracleAddr(3), pbinBasicDataLeafKey) + full := pbinPathFromBytes(key) + + t.Run("key of neither zone length", func(t *testing.T) { + path := full.slice(0, 100) + c := pbinCell{kind: pbinNodeLeaf, prefix: full.slice(100, full.bitLen-1)} + _, err := h.cellHash(&c, &path) + require.ErrorIs(t, err, errPBinCellHash) + }) + t.Run("account-zone sub-index naming no leaf", func(t *testing.T) { + bad := pbinPathFromBytes(pbinTreeKey(pbinAccountZone, make([]byte, 32), pbinCodeOffset)) + path := bad.slice(0, 100) + c := pbinCell{kind: pbinNodeLeaf, prefix: bad.slice(100, bad.bitLen)} + _, err := h.cellHash(&c, &path) + require.ErrorIs(t, err, errPBinCellHash) + }) +} + +// TestPBinCellHashBuildsCorpusRoots folds each oracle corpus of two keys by hand +// through the cell hasher, checking the primitives compose into the same root +// the reference tree produces. +func TestPBinCellHashBuildsCorpusRoots(t *testing.T) { + t.Parallel() + + for _, corpus := range []pbinOracleCorpus{ + pbinOracleCorpusSplitAtBit0(), + pbinOracleCorpusSplitAtLastBit(), + } { + t.Run(corpus.name, func(t *testing.T) { + require.Len(t, corpus.entries, 2) + var h pbinHasher + a, b := corpus.entries[0], corpus.entries[1] + + aPath, bPath := pbinPathFromBytes(a.key), pbinPathFromBytes(b.key) + shared := pbinCommonPrefixBits(&aPath, &bPath) + prefix := aPath.slice(0, shared) + + left, right := a, b + if aPath.bit(shared) == 1 { + left, right = b, a + } + leftHash := h.leafHash(left.key, left.value) + rightHash := h.leafHash(right.key, right.value) + + require.Equal(t, common.Hash(pbinOracleRoot(corpus.entries)), h.branchHash(&prefix, &leftHash, &rightHash)) + }) + } +} From 167591c352d93406aabc937dc9ea80e3bf2e2a0c Mon Sep 17 00:00:00 2001 From: awskii Date: Wed, 29 Jul 2026 18:10:09 +0700 Subject: [PATCH 08/56] feat: EIP-8297 unfold and needUnfolding MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Add PBinPatriciaHashed with the [528][2] grid, a bit-path currentKey, the context and the Keccak state, plus the two navigation primitives. needUnfolding answers with an action rather than the hex engine's cpl+1 nibble count. That count folds three different situations into one number and only works because the trailing +1 discounts a hex terminator, which the binary tree does not have. Descend (the probe key agrees with the whole cell prefix) and Split (it leaves the prefix partway) are kept apart because only Split shortens a stored node's prefix, and EIP-8297 puts the prefix inside the branch hash — so a split invalidates a cached child hash, which the hex engine never has to deal with. unfold ports the hex engine's two-step descent: consuming a cell's prefix materializes a row whose cell has no prefix left, and only then is the record read, keyed by the parent path plus that stored prefix. The parent's copy of the prefix is the only place those bits exist, so it is what makes the descent key reconstructible. Cell kind is an explicit field rather than "prefix length is zero", since EIP-8297 admits a branch with an empty prefix and a missing record below one is an inconsistency, not an empty subtree. Dropped from the hex port: terminator arithmetic and clampToAccountBoundary (one key space, no account/storage split), and the depth>64 accountAddrLen reset in fillFromUpperCell. Split rehash is structural only here; materialize-on-split lands with fold. --- docs/plans/20260729-pbin-patricia-hashed.md | 14 +- execution/commitment/pbin_bitpath.go | 11 + execution/commitment/pbin_patricia_hashed.go | 251 +++++++++++++ execution/commitment/pbin_unfold_test.go | 361 +++++++++++++++++++ 4 files changed, 630 insertions(+), 7 deletions(-) create mode 100644 execution/commitment/pbin_patricia_hashed.go create mode 100644 execution/commitment/pbin_unfold_test.go diff --git a/docs/plans/20260729-pbin-patricia-hashed.md b/docs/plans/20260729-pbin-patricia-hashed.md index f06c9c55d64..4aa44ee1a6d 100644 --- a/docs/plans/20260729-pbin-patricia-hashed.md +++ b/docs/plans/20260729-pbin-patricia-hashed.md @@ -198,13 +198,13 @@ func pbinCommonPrefixBits(a, b *pbinBitpath) int16 // XOR + LeadingZeros64, clam - Create: `execution/commitment/pbin_patricia_hashed.go` - Create: `execution/commitment/pbin_unfold_test.go` -- [ ] write a failing table test of `(cellPrefix, probeKey) → expected pbinNeedUnfolding result` covering `cpl == 0`, `cpl == len(prefix)` (full match, descend), and `cpl < len(prefix)` (split signal) (guards H9) -- [ ] write a failing test unfolding a stored branch record whose `prefixBitLen == 0`, asserting it is descended into rather than treated as leaf or empty (guards H7) -- [ ] write failing unfold tests for divergence at bits 0, 63, 64, 65, 271 and 527 -- [ ] create `PBinPatriciaHashed` with the grid, `currentKey bitpath`, context and Keccak state -- [ ] implement `pbinNeedUnfolding` with bit reads and clamped common-prefix, dropping hex terminator arithmetic and `clampToAccountBoundary`; its return contract MUST distinguish "prefix fully matched" from "diverges inside prefix" -- [ ] implement `pbinUnfold`/`pbinUnfoldBranchNode` reading the parent's stored cell prefix to reconstruct the descent key, with an explicit node-kind flag so a zero-length prefix is not overloaded -- [ ] run tests - must pass before task 8 +- [x] write a failing table test of `(cellPrefix, probeKey) → expected pbinNeedUnfolding result` covering `cpl == 0`, `cpl == len(prefix)` (full match, descend), and `cpl < len(prefix)` (split signal) (guards H9) +- [x] write a failing test unfolding a stored branch record whose `prefixBitLen == 0`, asserting it is descended into rather than treated as leaf or empty (guards H7) +- [x] write failing unfold tests for divergence at bits 0, 63, 64, 65, 271 and 527 +- [x] create `PBinPatriciaHashed` with the grid, `currentKey bitpath`, context and Keccak state +- [x] implement `pbinNeedUnfolding` with bit reads and clamped common-prefix, dropping hex terminator arithmetic and `clampToAccountBoundary`; its return contract MUST distinguish "prefix fully matched" from "diverges inside prefix" — landed as the method `needUnfolding` returning `pbinUnfolding{action, matched}`; the `pbin` prefix rule covers package-level identifiers only, and methods on `PBinPatriciaHashed` cannot collide with the hex engine's +- [x] implement `pbinUnfold`/`pbinUnfoldBranchNode` reading the parent's stored cell prefix to reconstruct the descent key, with an explicit node-kind flag so a zero-length prefix is not overloaded — landed as the methods `unfold`/`unfoldBranchNode` +- [x] run tests - must pass before task 8 ### Task 8: fold primitives diff --git a/execution/commitment/pbin_bitpath.go b/execution/commitment/pbin_bitpath.go index 6d1606aabc4..029c9a46e11 100644 --- a/execution/commitment/pbin_bitpath.go +++ b/execution/commitment/pbin_bitpath.go @@ -140,6 +140,17 @@ func pbinCommonPrefixBits(a, b *pbinBitpath) int16 { return min(n, limit) } +// pbinCommonPrefixBitsAt reports how many leading bits of prefix agree with key +// read from bit `from`, never past the end of either operand. +func pbinCommonPrefixBitsAt(key *pbinBitpath, from int16, prefix *pbinBitpath) int16 { + limit := min(key.bitLen-from, prefix.bitLen) + n := int16(0) + for n < limit && key.bit(from+n) == prefix.bit(n) { + n++ + } + return n +} + // pbinAppendPackedBits appends the path's bits MSB-first, zero-padded to a byte // boundary. func (p *pbinBitpath) appendPackedBits(dst []byte) []byte { diff --git a/execution/commitment/pbin_patricia_hashed.go b/execution/commitment/pbin_patricia_hashed.go new file mode 100644 index 00000000000..faa051f097d --- /dev/null +++ b/execution/commitment/pbin_patricia_hashed.go @@ -0,0 +1,251 @@ +// Copyright 2026 The Erigon Authors +// This file is part of Erigon. +// +// Erigon is free software: you can redistribute it and/or modify +// it under the terms of the GNU Lesser General Public License as published by +// the Free Software Foundation, either version 3 of the License, or +// (at your option) any later version. +// +// Erigon is distributed in the hope that it will be useful, +// but WITHOUT ANY WARRANTY; without even the implied warranty of +// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +// GNU Lesser General Public License for more details. +// +// You should have received a copy of the GNU Lesser General Public License +// along with Erigon. If not, see . + +package commitment + +import ( + "errors" + "fmt" +) + +// PBinPatriciaHashed computes commitment over EIP-8297's partitioned binary +// tree. It borrows the hex engine's grid, unfold and fold shape and none of its +// node model: arity is 2, there is no extension node and no storage root, and a +// leaf commits its complete tree key. +type PBinPatriciaHashed struct { + grid pbinGrid + currentKey pbinBitpath // path from the root to the deepest active row, one bit per level + ctx PatriciaContext + hasher pbinHasher + + rootChecked bool // whether the root record is known to be absent + rootTouched bool + rootPresent bool +} + +func NewPBinPatriciaHashed(ctx PatriciaContext) *PBinPatriciaHashed { + return &PBinPatriciaHashed{ctx: ctx} +} + +var errPBinMissingBranch = errors.New("pbin: branch record missing") + +// pbinUnfoldAction is what needUnfolding tells unfold to do about one cell. +type pbinUnfoldAction uint8 + +const ( + // pbinUnfoldNone means the probe key's slot is already in the grid. + pbinUnfoldNone pbinUnfoldAction = iota + // pbinUnfoldRecord means the cell points straight at a stored node: read it. + pbinUnfoldRecord + // pbinUnfoldDescend means the probe key agrees with the cell's whole prefix, + // so the descent runs through it and nothing below moves. + pbinUnfoldDescend + // pbinUnfoldSplit means the probe key leaves the cell's prefix partway, so the + // node below drops a level and its prefix shrinks. + pbinUnfoldSplit +) + +// pbinUnfolding is needUnfolding's answer. Descend and Split are separate +// answers rather than one bit count because only Split shortens a stored node's +// prefix, and the prefix is inside that node's hash. +type pbinUnfolding struct { + action pbinUnfoldAction + // matched counts the cell prefix bits the probe key agrees with: the whole + // prefix for Descend, short of it for Split. + matched int16 +} + +// needUnfolding reports what the grid still needs before probe's slot is in it. +// Unlike the hex engine there is no terminator to discount and no account +// boundary to clamp to — one key space, one bit per level. +func (pph *PBinPatriciaHashed) needUnfolding(probe *pbinBitpath) pbinUnfolding { + var cell *pbinCell + var depth int16 + + if pph.grid.activeRows == 0 { + if pph.grid.root.kind == pbinNodeEmpty { + if pph.rootChecked { + return pbinUnfolding{} + } + return pbinUnfolding{action: pbinUnfoldRecord} + } + cell = &pph.grid.root + } else { + row := pph.grid.activeRows - 1 + depth = pph.grid.depths[row] + if probe.bitLen <= depth { + return pbinUnfolding{} + } + cell = &pph.grid.rows[row][probe.bit(depth-1)] + } + + if cell.kind == pbinNodeEmpty { + return pbinUnfolding{} + } + if cell.prefix.bitLen == 0 { + if cell.kind == pbinNodeBranch { + return pbinUnfolding{action: pbinUnfoldRecord} + } + return pbinUnfolding{} + } + + matched := pbinCommonPrefixBitsAt(probe, depth, &cell.prefix) + if matched < cell.prefix.bitLen { + return pbinUnfolding{action: pbinUnfoldSplit, matched: matched} + } + if cell.kind == pbinNodeLeaf { + return pbinUnfolding{} // keys are prefix-free, so probe is this leaf's key + } + return pbinUnfolding{action: pbinUnfoldDescend, matched: matched} +} + +// unfold opens one more level of the grid along probe, per the plan needUnfolding +// produced. +func (pph *PBinPatriciaHashed) unfold(probe *pbinBitpath, u pbinUnfolding) error { + if u.action == pbinUnfoldNone { + return nil + } + g := &pph.grid + + var upCell *pbinCell + var touched, present bool + var upDepth int16 + + if g.activeRows == 0 { + if pph.rootChecked && g.root.kind == pbinNodeEmpty { + return nil + } + upCell = &g.root + touched, present = pph.rootTouched, pph.rootPresent + } else { + upRow := g.activeRows - 1 + upDepth = g.depths[upRow] + upBit := probe.bit(upDepth - 1) + upCell = &g.rows[upRow][upBit] + touched = g.touchMap[upRow]&(uint16(1)< 1 { + head := upCell.prefix.slice(0, consumed-1) + pph.currentKey.append(&head) + } + g.depths[row] = upDepth + consumed + g.activeRows++ + return nil +} + +// pbinUnfoldConsumed is how many of the cell's prefix bits this unfold takes: +// all of them when the probe key matched, and one past the divergence when it +// did not — that extra bit is what the new row branches on. +func pbinUnfoldConsumed(u pbinUnfolding, prefix *pbinBitpath) (int16, error) { + switch u.action { + case pbinUnfoldDescend: + return prefix.bitLen, nil + case pbinUnfoldSplit: + if u.matched >= prefix.bitLen { + return 0, fmt.Errorf("pbin: %d matched bits of a %d-bit prefix is not a split", u.matched, prefix.bitLen) + } + return u.matched + 1, nil + default: + return 0, fmt.Errorf("pbin: unfold action %d consumes no prefix bits", u.action) + } +} + +// unfoldBranchNode loads the record at the current descent key into a row. The +// key is reconstructed from the parent cell's stored prefix, which is the only +// place the bits between the two nodes exist. +func (pph *PBinPatriciaHashed) unfoldBranchNode(row int, depth int16, deleted bool) error { + g := &pph.grid + key := pbinEncodeBitPath(&pph.currentKey) + + data, _, err := pph.ctx.Branch(key) + if err != nil { + return fmt.Errorf("pbin: read branch at %x: %w", key, err) + } + if len(data) == 0 { + if !pph.rootChecked && pph.currentKey.bitLen == 0 { + pph.rootChecked = true + return nil + } + return fmt.Errorf("%w at %x (%d bits)", errPBinMissingBranch, key, pph.currentKey.bitLen) + } + + _, afterMap, err := pbinDecodeBranch(data, &g.rows[row]) + if err != nil { + return fmt.Errorf("pbin: decode branch at %x: %w", key, err) + } + // The record's own touch map is write-time bookkeeping; nothing in this run + // has touched the row yet. A parent cell that is touched but gone takes the + // whole subtree with it. + if deleted { + g.touchMap[row], g.afterMap[row] = afterMap, 0 + } else { + g.touchMap[row], g.afterMap[row] = 0, afterMap + } + g.branchBefore[row] = true + g.depths[row] = depth + g.activeRows++ + return nil +} + +// fillFromUpperCell moves a cell one level down, dropping the prefix bits the +// descent has taken over. skip counts those bits and includes the one the new +// row branches on. +func (c *pbinCell) fillFromUpperCell(up *pbinCell, skip int16) { + c.reset() + if skip < up.prefix.bitLen { + c.prefix = up.prefix.slice(skip, up.prefix.bitLen) + } + c.kind = up.kind + c.accountAddrLen = up.accountAddrLen + if up.accountAddrLen > 0 { + c.accountAddr = up.accountAddr + } + c.storageAddrLen = up.storageAddrLen + if up.storageAddrLen > 0 { + c.storageAddr = up.storageAddr + } + c.hashLen = up.hashLen + if up.hashLen > 0 { + c.hash = up.hash + } + c.loaded = up.loaded + c.Update = up.Update +} diff --git a/execution/commitment/pbin_unfold_test.go b/execution/commitment/pbin_unfold_test.go new file mode 100644 index 00000000000..5f487307161 --- /dev/null +++ b/execution/commitment/pbin_unfold_test.go @@ -0,0 +1,361 @@ +// Copyright 2026 The Erigon Authors +// This file is part of Erigon. +// +// Erigon is free software: you can redistribute it and/or modify +// it under the terms of the GNU Lesser General Public License as published by +// the Free Software Foundation, either version 3 of the License, or +// (at your option) any later version. +// +// Erigon is distributed in the hope that it will be useful, +// but WITHOUT ANY WARRANTY; without even the implied warranty of +// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +// GNU Lesser General Public License for more details. +// +// You should have received a copy of the GNU Lesser General Public License +// along with Erigon. If not, see . + +package commitment + +import ( + "bytes" + "fmt" + "testing" + + "github.com/stretchr/testify/require" + + "github.com/erigontech/erigon/common" + "github.com/erigontech/erigon/common/length" +) + +func pbinTestEngine(t *testing.T) (*PBinPatriciaHashed, *MockState) { + t.Helper() + ms := NewMockState(t) + return NewPBinPatriciaHashed(ms), ms +} + +// pbinTestSpecCell builds a cell whose prefix is spelled out bit by bit, so a +// test can name a divergence point instead of deriving one. +func pbinTestSpecCell(t *testing.T, kind pbinNodeKind, spec string) pbinCell { + t.Helper() + c := pbinTestEmptyCell() + c.kind = kind + c.prefix = pbinTestPathFromBits(t, pbinTestBitSpec(t, spec)) + c.hash = common.Hash{0xB1, byte(len(spec))} + c.hashLen = length.Hash + return c +} + +func pbinTestPutRecord(t *testing.T, ms *MockState, path pbinBitpath, cells [2]pbinCell) { + t.Helper() + var enc pbinBranchEncoder + rec, err := enc.encode(0b11, 0b11, &cells) + require.NoError(t, err) + require.NoError(t, ms.PutBranch(pbinEncodeBitPath(&path), bytes.Clone(rec), nil)) +} + +// TestPBinNeedUnfolding guards H9: the hex engine's cpl+1 hides a terminator +// nibble, so the binary engine states each outcome instead. What matters is that +// "the probe agrees with the whole prefix" and "the probe leaves the prefix +// partway" are different answers — only the second shortens a stored prefix, +// which is inside that node's hash. +func TestPBinNeedUnfolding(t *testing.T) { + t.Parallel() + + for _, tc := range []struct { + name string + root pbinCell + rootChecked bool + probe string + want pbinUnfolding + }{ + { + name: "an unchecked empty root reads the root record", + root: pbinTestEmptyCell(), + probe: "1010", + want: pbinUnfolding{action: pbinUnfoldRecord}, + }, + { + name: "a checked empty root needs nothing", + root: pbinTestEmptyCell(), + rootChecked: true, + probe: "1010", + want: pbinUnfolding{}, + }, + { + name: "a branch with an empty prefix is a record read", + root: pbinTestSpecCell(t, pbinNodeBranch, ""), + probe: "1010", + want: pbinUnfolding{action: pbinUnfoldRecord}, + }, + { + name: "cpl == 0 splits at the first bit", + root: pbinTestSpecCell(t, pbinNodeBranch, "1011"), + probe: "0011", + want: pbinUnfolding{action: pbinUnfoldSplit, matched: 0}, + }, + { + name: "cpl < len(prefix) splits inside it", + root: pbinTestSpecCell(t, pbinNodeBranch, "1011"), + probe: "1001", + want: pbinUnfolding{action: pbinUnfoldSplit, matched: 2}, + }, + { + name: "cpl == len(prefix) descends through it", + root: pbinTestSpecCell(t, pbinNodeBranch, "1011"), + probe: "10110", + want: pbinUnfolding{action: pbinUnfoldDescend, matched: 4}, + }, + { + name: "a leaf the probe fully matches is already the target", + root: pbinTestSpecCell(t, pbinNodeLeaf, "1011"), + probe: "1011", + want: pbinUnfolding{}, + }, + { + name: "a leaf the probe leaves splits", + root: pbinTestSpecCell(t, pbinNodeLeaf, "1011"), + probe: "1000", + want: pbinUnfolding{action: pbinUnfoldSplit, matched: 2}, + }, + } { + t.Run(tc.name, func(t *testing.T) { + t.Parallel() + + pph, _ := pbinTestEngine(t) + pph.grid.root = tc.root + pph.rootChecked = tc.rootChecked + + probe := pbinTestPathFromBits(t, pbinTestBitSpec(t, tc.probe)) + require.Equal(t, tc.want, pph.needUnfolding(&probe)) + }) + } +} + +// TestPBinNeedUnfoldingSelectsCellByBranchBit checks the row case picks the cell +// with the bit the row branches on, the arity-2 stand-in for the hex engine's +// nibble. +func TestPBinNeedUnfoldingSelectsCellByBranchBit(t *testing.T) { + t.Parallel() + + pph, ms := pbinTestEngine(t) + pbinTestPutRecord(t, ms, pbinBitpath{}, [2]pbinCell{ + pbinTestSpecCell(t, pbinNodeLeaf, "000"), + pbinTestSpecCell(t, pbinNodeBranch, "111"), + }) + + probe := pbinTestPathFromBits(t, pbinTestBitSpec(t, "0000")) + require.NoError(t, pph.unfold(&probe, pph.needUnfolding(&probe))) + require.Equal(t, 1, pph.grid.activeRows) + require.Equal(t, int16(1), pph.grid.depths[0]) + + for _, tc := range []struct { + name string + probe string + want pbinUnfolding + }{ + {"left cell, leaf fully matched", "0000", pbinUnfolding{}}, + {"left cell, leaf left at its last bit", "0001", pbinUnfolding{action: pbinUnfoldSplit, matched: 2}}, + {"right cell, branch fully matched", "1111", pbinUnfolding{action: pbinUnfoldDescend, matched: 3}}, + {"right cell, branch left inside its prefix", "1101", pbinUnfolding{action: pbinUnfoldSplit, matched: 1}}, + } { + t.Run(tc.name, func(t *testing.T) { + probe := pbinTestPathFromBits(t, pbinTestBitSpec(t, tc.probe)) + require.Equal(t, tc.want, pph.needUnfolding(&probe)) + }) + } +} + +// TestPBinUnfoldEmptyPrefixBranchRecord guards H7: EIP-8297 admits a branch node +// with no prefix, so a zero-length prefix cannot double as "this cell is not a +// stored branch". The engine must read the record below and descend into it. +func TestPBinUnfoldEmptyPrefixBranchRecord(t *testing.T) { + t.Parallel() + + childCells := [2]pbinCell{ + pbinTestSpecCell(t, pbinNodeLeaf, "0101"), + pbinTestSpecCell(t, pbinNodeLeaf, "1100"), + } + rootCells := [2]pbinCell{ + pbinTestSpecCell(t, pbinNodeLeaf, "010"), + pbinTestSpecCell(t, pbinNodeBranch, ""), + } + childPath := pbinTestPathFromBits(t, pbinTestBitSpec(t, "1")) + + pph, ms := pbinTestEngine(t) + pbinTestPutRecord(t, ms, pbinBitpath{}, rootCells) + pbinTestPutRecord(t, ms, childPath, childCells) + + probe := pbinTestPathFromBits(t, pbinTestBitSpec(t, "1101")) + require.NoError(t, pph.unfold(&probe, pph.needUnfolding(&probe))) + require.Equal(t, 1, pph.grid.activeRows) + + u := pph.needUnfolding(&probe) + require.Equal(t, pbinUnfolding{action: pbinUnfoldRecord}, u, + "a branch cell with a zero-length prefix is still a stored node") + + require.NoError(t, pph.unfold(&probe, u)) + require.Equal(t, 2, pph.grid.activeRows) + require.Equal(t, int16(2), pph.grid.depths[1]) + require.Equal(t, childPath, pph.currentKey) + require.True(t, pph.grid.branchBefore[1]) + require.Equal(t, childCells, pph.grid.rows[1]) + require.Equal(t, uint16(0b11), pph.grid.afterMap[1]) + require.Equal(t, uint16(0), pph.grid.touchMap[1]) +} + +// A missing record below such a cell is an inconsistency, not an empty subtree — +// the other half of H7's failure mode. +func TestPBinUnfoldEmptyPrefixBranchRecordMissing(t *testing.T) { + t.Parallel() + + pph, ms := pbinTestEngine(t) + pbinTestPutRecord(t, ms, pbinBitpath{}, [2]pbinCell{ + pbinTestSpecCell(t, pbinNodeLeaf, "010"), + pbinTestSpecCell(t, pbinNodeBranch, ""), + }) + + probe := pbinTestPathFromBits(t, pbinTestBitSpec(t, "1101")) + require.NoError(t, pph.unfold(&probe, pph.needUnfolding(&probe))) + require.ErrorIs(t, pph.unfold(&probe, pph.needUnfolding(&probe)), errPBinMissingBranch) +} + +func TestPBinUnfoldEmptyRoot(t *testing.T) { + t.Parallel() + + pph, _ := pbinTestEngine(t) + probe := pbinPathFromBytes(pbinTreeKeyAccount(pbinOracleAddr(1), pbinBasicDataLeafKey)) + + u := pph.needUnfolding(&probe) + require.Equal(t, pbinUnfolding{action: pbinUnfoldRecord}, u) + require.NoError(t, pph.unfold(&probe, u)) + + require.Equal(t, 0, pph.grid.activeRows) + require.True(t, pph.rootChecked) + require.Equal(t, pbinUnfolding{}, pph.needUnfolding(&probe), "a checked empty root does not unfold again") +} + +// TestPBinUnfoldSplitsInsidePrefix walks the divergence bit across both word +// boundaries of the [9]uint64 path and both zone lengths. A split moves the node +// below one level down and re-cuts its prefix, dropping the bit the new row +// branches on (eip:174-176). +func TestPBinUnfoldSplitsInsidePrefix(t *testing.T) { + t.Parallel() + + full := pbinTestPathFromBits(t, pbinTestBitPattern(pbinMaxPathBits)) + + for _, divergence := range []int16{0, 63, 64, 65, 271, 527} { + t.Run(fmt.Sprintf("bit %d", divergence), func(t *testing.T) { + t.Parallel() + + pph, _ := pbinTestEngine(t) + pph.grid.root = pbinTestEmptyCell() + pph.grid.root.kind = pbinNodeLeaf + pph.grid.root.prefix = full + pph.rootPresent = true + + probe := full + probe.setBitAt(divergence, full.bit(divergence)^1) + + u := pph.needUnfolding(&probe) + require.Equal(t, pbinUnfolding{action: pbinUnfoldSplit, matched: divergence}, u) + require.NoError(t, pph.unfold(&probe, u)) + + require.Equal(t, 1, pph.grid.activeRows) + require.Equal(t, divergence+1, pph.grid.depths[0]) + require.Equal(t, full.slice(0, divergence), pph.currentKey) + + survivorBit := full.bit(divergence) + survivor := &pph.grid.rows[0][survivorBit] + require.Equal(t, pbinNodeLeaf, survivor.kind) + require.Equal(t, full.slice(divergence+1, full.bitLen), survivor.prefix, + "the survivor drops the bit the new row branches on") + require.Equal(t, pbinNodeEmpty, pph.grid.rows[0][1-survivorBit].kind, + "the probe's own side is left for updateCell to fill") + + require.Equal(t, uint16(0), pph.grid.touchMap[0]) + require.Equal(t, uint16(1)< Date: Wed, 29 Jul 2026 18:36:00 +0700 Subject: [PATCH 09/56] feat: EIP-8297 fold primitives fold dispatches a row by how many cells survive it: branch (two), propagate (one), delete (none). A branch fold hashes both cells, stores the row as one self-contained record keyed by the encoded bit path, and hands the parent a branch cell. A propagate collapses the row into the cell above, prepending the bits the row consumed and writing no record. EIP-8297 puts a node's prefix inside its hash, so any prefix change invalidates it. The plan covered the split direction; the ordinary descent has the same problem in reverse, since unfold moves a prefix out of a cell and the propagate that follows moves it back. Cells this run built now carry their two child hashes in memory, so a prefix change re-derives; cells that arrived from a record still materialize from their own record, which is unchanged by a split. Counters measure how often that fallback fires. A record carries plain keys, not values, so an untouched sibling loaded from one is read back from state before it is hashed. --- docs/plans/20260729-pbin-patricia-hashed.md | 22 +- execution/commitment/pbin_cell.go | 9 + execution/commitment/pbin_fold_test.go | 487 +++++++++++++++++++ execution/commitment/pbin_hash.go | 3 + execution/commitment/pbin_patricia_hashed.go | 293 ++++++++++- execution/commitment/pbin_unfold_test.go | 3 +- 6 files changed, 801 insertions(+), 16 deletions(-) create mode 100644 execution/commitment/pbin_fold_test.go diff --git a/docs/plans/20260729-pbin-patricia-hashed.md b/docs/plans/20260729-pbin-patricia-hashed.md index 4aa44ee1a6d..6d285742a90 100644 --- a/docs/plans/20260729-pbin-patricia-hashed.md +++ b/docs/plans/20260729-pbin-patricia-hashed.md @@ -212,15 +212,17 @@ func pbinCommonPrefixBits(a, b *pbinBitpath) int16 // XOR + LeadingZeros64, clam - Modify: `execution/commitment/pbin_patricia_hashed.go` - Create: `execution/commitment/pbin_fold_test.go` -- [ ] write failing grid-seeded unit tests: hand-build one row, fold it, assert the emitted hash equals the oracle's `merkelize` of that node and that the record bytes round-trip -- [ ] write a failing test for a split whose survivor is a **leaf**, asserting no branch record is read -- [ ] write failing tests forcing splits inside prefixes at several depths, asserting each rehashed node matches the oracle -- [ ] implement `pbinFold` dispatching the three kinds — delete / propagate / branch — mirroring `hex_patricia_hashed.go:2031-2038` -- [ ] implement `pbinFoldBranch` writing records keyed by the encoded bit path, asserting `(touchMap|afterMap) &^ 0b11 == 0` at entry and `popcount(afterMap) == 2` (guards H12) -- [ ] implement `pbinFoldPropagate` accumulating the child's prefix bits into the parent cell and writing **no** record, asserting `prefixBits == depth - upDepth - 1` (guards H12) -- [ ] implement materialize-on-split with the leaf-survivor short circuit, plus a debug assert that a cell whose prefix bit length changed has `hashLen == 0` (guards H1) -- [ ] add instrumentation counters for splits-inside-prefix and extra `ctx.Branch` reads -- [ ] run tests - must pass before task 9 +- [x] write failing grid-seeded unit tests: hand-build one row, fold it, assert the emitted hash equals the oracle's `merkelize` of that node and that the record bytes round-trip +- [x] write a failing test for a split whose survivor is a **leaf**, asserting no branch record is read +- [x] write failing tests forcing splits inside prefixes at several depths, asserting each rehashed node matches the oracle +- [x] implement `pbinFold` dispatching the three kinds — delete / propagate / branch — mirroring `hex_patricia_hashed.go:2031-2038` — landed as the methods `fold`/`foldBranch`/`foldPropagate`/`foldDelete`, same reasoning as Task 7's `unfold` +- [x] implement `pbinFoldBranch` writing records keyed by the encoded bit path, asserting `(touchMap|afterMap) &^ 0b11 == 0` at entry and `popcount(afterMap) == 2` (guards H12) +- [x] implement `pbinFoldPropagate` accumulating the child's prefix bits into the parent cell and writing **no** record, asserting `prefixBits == depth - upDepth - 1` (guards H12) — landed as the equivalent post-condition on the assembled prefix, which also catches a dropped branch bit +- [x] implement materialize-on-split with the leaf-survivor short circuit, plus a debug assert that a cell whose prefix bit length changed has `hashLen == 0` (guards H1) — landed as `rehashAfterPrefixChange`, which enforces the invariant rather than asserting it: a cell that knows its children re-derives, one that does not is marked stale and materializes on demand +- [x] add instrumentation counters for splits-inside-prefix and extra `ctx.Branch` reads +- [x] run tests - must pass before task 9 + +⚠️ **Scope note (discovered here, resolved here).** Decision 8 covered only `needUnfolding`-reported splits, but a cell's node prefix also changes on the *normal* descent: `unfold` consuming a branch cell's prefix leaves the cell holding none of it, and the propagate that follows hands it back. Both directions invalidate a hash the prefix sits inside, and the propagate direction cannot be fixed by a record read at fold time without re-reading every descended node. Resolved by carrying the two child hashes in memory on cells this run built (`pbinCell.children`/`childrenSet`, not serialised), so a prefix change re-derives instead of re-reading; materialize-on-split stays the fallback for cells that arrived from a record. One Task 7 assertion (`pbin_unfold_test.go`, descended cell keeps the parent's hash) encoded the wrong behaviour and now pins `hashLen == 0`. ### Task 9: drive loop, Process and RootHash @@ -286,7 +288,7 @@ func pbinCommonPrefixBits(a, b *pbinBitpath) int16 // XOR + LeadingZeros64, clam *Items requiring manual intervention, measurement, or follow-on milestones — no checkboxes* **Decisions deferred to data:** -- Split-rehash strategy. M0 ships materialize-on-split. If the Task 8 counters show split-inside-prefix reads dominating, revisit storing `left||right` (64 B) per cell instead of a 32-byte child hash — a wire-format change needing its own migration story. +- Split-rehash strategy. M0 ships materialize-on-split, narrowed by Task 8's in-memory child hashes to cells that arrived from a record — a node this run folded re-derives for free. `pbinCounters.materializeReads` measures what is left. If it stays non-trivial, promote the two child hashes into the record itself and the hazard disappears, at 32 B per branch cell and a migration story. - Record the one-prefix-per-cell rationale (Task 5) here and in the commit body rather than as a source comment. **Out of scope, in rough dependency order:** diff --git a/execution/commitment/pbin_cell.go b/execution/commitment/pbin_cell.go index 3076c37bea1..a801ead6308 100644 --- a/execution/commitment/pbin_cell.go +++ b/execution/commitment/pbin_cell.go @@ -39,9 +39,15 @@ const ( // the one prefix is always tree-key-space bits. There is no memoized leaf hash // either — H(0x00||key||value) commits the complete key and has nothing worth // caching. +// +// A branch cell's prefix is inside its hash, so re-cutting the prefix +// invalidates it. Two invariants keep that from going unnoticed: a non-zero +// hashLen means hash covers the prefix the cell holds now, and childrenSet means +// the cell can re-derive the hash for any prefix without touching the database. type pbinCell struct { prefix pbinBitpath hash common.Hash + children [2]common.Hash accountAddr common.Address storageAddr [length.Addr + length.Hash]byte @@ -49,10 +55,13 @@ type pbinCell struct { storageAddrLen int16 hashLen int16 kind pbinNodeKind + childrenSet bool loaded loadFlags Update } +func (c *pbinCell) setFromUpdate(u *Update) { c.Update.Merge(u) } + func (c *pbinCell) reset() { *c = pbinCell{} c.Update.Reset() diff --git a/execution/commitment/pbin_fold_test.go b/execution/commitment/pbin_fold_test.go new file mode 100644 index 00000000000..1c0707cc0ea --- /dev/null +++ b/execution/commitment/pbin_fold_test.go @@ -0,0 +1,487 @@ +// Copyright 2026 The Erigon Authors +// This file is part of Erigon. +// +// Erigon is free software: you can redistribute it and/or modify +// it under the terms of the GNU Lesser General Public License as published by +// the Free Software Foundation, either version 3 of the License, or +// (at your option) any later version. +// +// Erigon is distributed in the hope that it will be useful, +// but WITHOUT ANY WARRANTY; without even the implied warranty of +// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +// GNU Lesser General Public License for more details. +// +// You should have received a copy of the GNU Lesser General Public License +// along with Erigon. If not, see . + +package commitment + +import ( + "bytes" + "fmt" + "testing" + + "github.com/stretchr/testify/require" + + "github.com/erigontech/erigon/common" + "github.com/erigontech/erigon/common/length" + "github.com/erigontech/erigon/db/kv" +) + +// pbinTestCountingCtx counts branch reads, so a test can pin how many records a +// fold needed beyond the ones the descent itself read. +type pbinTestCountingCtx struct { + PatriciaContext + branchReads int +} + +func (c *pbinTestCountingCtx) Branch(prefix []byte) ([]byte, kv.Step, error) { + c.branchReads++ + return c.PatriciaContext.Branch(prefix) +} + +// pbinTestLeaf is one storage entry in every form a fold needs it: the plain key +// its state is read by, the tree key its path is cut from, and the encoded value +// both the engine and the oracle hash. +type pbinTestLeaf struct { + plainKey []byte + treeKey []byte + storage []byte + value [pbinValueLength]byte +} + +func pbinTestStorageLeaf(treeKey []byte, seed byte) pbinTestLeaf { + storage := []byte{seed, seed ^ 0xFF} + return pbinTestLeaf{ + plainKey: bytes.Repeat([]byte{seed}, length.Addr+length.Hash), + treeKey: treeKey, + storage: storage, + value: pbinEncodeStorageValue(storage), + } +} + +func (l pbinTestLeaf) update() Update { + u := Update{Flags: StorageUpdate, StorageLen: int8(len(l.storage))} + copy(u.Storage[:], l.storage) + return u +} + +// cell cuts the leaf's tree key at depth, the way a row at that depth holds it. +func (l pbinTestLeaf) cell(t *testing.T, depth int16) pbinCell { + t.Helper() + full := pbinPathFromBytes(l.treeKey) + c := pbinTestEmptyCell() + c.kind = pbinNodeLeaf + c.prefix = full.slice(depth, full.bitLen) + copy(c.storageAddr[:], l.plainKey) + c.storageAddrLen = length.Addr + length.Hash + c.Update = l.update() + c.loaded = cellLoadStorage + return c +} + +func (l pbinTestLeaf) entry() pbinOracleEntry { + return pbinOracleEntry{key: l.treeKey, value: l.value[:]} +} + +func pbinTestPutState(t *testing.T, ms *MockState, leaves ...pbinTestLeaf) { + t.Helper() + keys := make([][]byte, 0, len(leaves)) + updates := make([]Update, 0, len(leaves)) + for _, l := range leaves { + keys = append(keys, l.plainKey) + updates = append(updates, l.update()) + } + require.NoError(t, ms.applyPlainUpdates(keys, updates)) +} + +// pbinTestTreeKeyFlipped derives a key diverging from the original at exactly +// one named bit. The zone byte is off limits: it selects the value encoding. +func pbinTestTreeKeyFlipped(t *testing.T, key []byte, d int16) []byte { + t.Helper() + require.GreaterOrEqual(t, d, int16(8), "bit %d is inside the zone byte", d) + require.Less(t, int(d), len(key)*8) + out := bytes.Clone(key) + out[d/8] ^= 1 << (7 - uint(d%8)) + return out +} + +func pbinTestBaseStorageKey() []byte { + return pbinTreeKeyStorage(pbinOracleAddr(7), pbinOracleSlot(1000)) +} + +// pbinTestKeyPrefix is the first bitLen bits of a tree key, the shape both a +// descent key and a node prefix take. +func pbinTestKeyPrefix(key []byte, bitLen int16) pbinBitpath { + full := pbinPathFromBytes(key) + return full.slice(0, bitLen) +} + +func pbinTestSeedRow(pph *PBinPatriciaHashed, currentKey pbinBitpath, depth int16, cells [2]pbinCell, touchMap, afterMap uint16) { + pph.currentKey = currentKey + pph.grid.rows[0] = cells + pph.grid.depths[0] = depth + pph.grid.touchMap[0], pph.grid.afterMap[0] = touchMap, afterMap + pph.grid.activeRows = 1 +} + +// pbinTestFillCell drops a cell into a live row the way updateCell will, marking +// it both touched and present. +func pbinTestFillCell(pph *PBinPatriciaHashed, row int, bit uint64, c pbinCell) { + pph.grid.rows[row][bit] = c + pph.grid.touchMap[row] |= uint16(1) << bit + pph.grid.afterMap[row] |= uint16(1) << bit +} + +func pbinTestBranchOrder(t *testing.T, a, b pbinTestLeaf, divergence int16) (left, right pbinTestLeaf) { + t.Helper() + path := pbinPathFromBytes(a.treeKey) + if path.bit(divergence) == 1 { + return b, a + } + return a, b +} + +// TestPBinFoldBranchMatchesOracle folds a hand-built row and checks the node it +// emits against the reference tree, at divergence points spanning both word +// boundaries of the path. The record it writes must also survive a decode and +// re-encode unchanged, since nothing merges it with a predecessor. +func TestPBinFoldBranchMatchesOracle(t *testing.T) { + t.Parallel() + + base := pbinTestBaseStorageKey() + for _, divergence := range []int16{8, 63, 64, 65, 271, 527} { + t.Run(fmt.Sprintf("bit %d", divergence), func(t *testing.T) { + t.Parallel() + + a := pbinTestStorageLeaf(base, 0x11) + b := pbinTestStorageLeaf(pbinTestTreeKeyFlipped(t, base, divergence), 0x22) + left, right := pbinTestBranchOrder(t, a, b, divergence) + + ms := NewMockState(t) + ctx := &pbinTestCountingCtx{PatriciaContext: ms} + pph := NewPBinPatriciaHashed(ctx) + + currentKey := pbinTestKeyPrefix(a.treeKey, divergence) + cells := [2]pbinCell{left.cell(t, divergence+1), right.cell(t, divergence+1)} + pbinTestSeedRow(pph, currentKey, divergence+1, cells, 0b11, 0b11) + + require.NoError(t, pph.fold()) + require.Equal(t, 0, pph.grid.activeRows) + require.Equal(t, int16(0), pph.currentKey.bitLen) + require.True(t, pph.rootTouched) + require.True(t, pph.rootPresent) + require.Zero(t, ctx.branchReads, "a fold of loaded cells reads nothing") + + require.Equal(t, pbinNodeBranch, pph.grid.root.kind) + require.Equal(t, currentKey, pph.grid.root.prefix) + want := pbinOracleRoot([]pbinOracleEntry{a.entry(), b.entry()}) + require.Equal(t, common.Hash(want), pph.grid.root.hash) + + data, _, err := ms.Branch(pbinEncodeBitPath(¤tKey)) + require.NoError(t, err) + require.NotEmpty(t, data, "a branch fold stores its row") + + var stored [2]pbinCell + touchMap, afterMap, err := pbinDecodeBranch(data, &stored) + require.NoError(t, err) + require.Equal(t, uint16(0b11), touchMap) + require.Equal(t, uint16(0b11), afterMap) + require.Equal(t, cells[0].prefix, stored[0].prefix) + require.Equal(t, cells[1].prefix, stored[1].prefix) + + var enc pbinBranchEncoder + again, err := enc.encode(touchMap, afterMap, &stored) + require.NoError(t, err) + require.Equal(t, data, []byte(again)) + }) + } +} + +// A binary node has two children. Folding a row as a branch with any other count +// is a lost or duplicated sibling, which at arity 2 is half the subtree +// (guards H12). +func TestPBinFoldBranchRejectsWrongArity(t *testing.T) { + t.Parallel() + + base := pbinTestBaseStorageKey() + a := pbinTestStorageLeaf(base, 0x11) + + pph, _ := pbinTestEngine(t) + cells := [2]pbinCell{a.cell(t, 9), pbinTestEmptyCell()} + pbinTestSeedRow(pph, pbinTestKeyPrefix(a.treeKey, 8), 9, cells, 0b01, 0b01) + + require.Error(t, pph.foldBranch(0, 0, 0, 9, &pph.grid.root)) +} + +func TestPBinFoldRejectsInconsistentGrid(t *testing.T) { + t.Parallel() + + t.Run("no active rows", func(t *testing.T) { + t.Parallel() + pph, _ := pbinTestEngine(t) + require.Error(t, pph.fold()) + }) + t.Run("cell bit outside the arity", func(t *testing.T) { + t.Parallel() + pph, _ := pbinTestEngine(t) + pbinTestSeedRow(pph, pbinBitpath{}, 1, [2]pbinCell{}, 0b100, 0b100) + require.ErrorIs(t, pph.fold(), errPBinCellMaps) + }) + t.Run("key shorter than the row depth", func(t *testing.T) { + t.Parallel() + pph, _ := pbinTestEngine(t) + pbinTestSeedRow(pph, pbinBitpath{}, 5, [2]pbinCell{}, 0, 0b11) + require.Error(t, pph.fold()) + }) +} + +// TestPBinFoldPropagateRestoresDescendedNode is the round trip a shared prefix +// forces: unfold consumes the prefix into the descent key, so the branch fold +// below sees none of it, and the propagate that follows has to hand the node +// back its full prefix — which is inside its hash. +func TestPBinFoldPropagateRestoresDescendedNode(t *testing.T) { + t.Parallel() + + base := pbinTestBaseStorageKey() + for _, divergence := range []int16{8, 63, 64, 65, 271, 527} { + t.Run(fmt.Sprintf("prefix of %d bits", divergence), func(t *testing.T) { + t.Parallel() + + a := pbinTestStorageLeaf(base, 0x33) + b := pbinTestStorageLeaf(pbinTestTreeKeyFlipped(t, base, divergence), 0x44) + left, right := pbinTestBranchOrder(t, a, b, divergence) + prefix := pbinTestKeyPrefix(a.treeKey, divergence) + + ms := NewMockState(t) + pbinTestPutState(t, ms, a, b) + + // Store the node the descent will walk into, then meet it again through a + // cell that only knows its prefix and hash, as a reloaded one would. + builder := NewPBinPatriciaHashed(ms) + cells := [2]pbinCell{left.cell(t, divergence+1), right.cell(t, divergence+1)} + pbinTestSeedRow(builder, prefix, divergence+1, cells, 0b11, 0b11) + require.NoError(t, builder.fold()) + nodeHash := builder.grid.root.hash + + ctx := &pbinTestCountingCtx{PatriciaContext: ms} + pph := NewPBinPatriciaHashed(ctx) + pph.grid.root = pbinTestEmptyCell() + pph.grid.root.kind = pbinNodeBranch + pph.grid.root.prefix = prefix + pph.grid.root.hash = nodeHash + pph.grid.root.hashLen = length.Hash + pph.rootPresent = true + + probe := pbinPathFromBytes(a.treeKey) + u := pph.needUnfolding(&probe) + require.Equal(t, pbinUnfolding{action: pbinUnfoldDescend, matched: divergence}, u) + require.NoError(t, pph.unfold(&probe, u)) + require.Equal(t, int16(0), pph.grid.rows[0][probe.bit(divergence-1)].hashLen, + "re-cutting a prefix invalidates the hash it is inside") + + u = pph.needUnfolding(&probe) + require.Equal(t, pbinUnfolding{action: pbinUnfoldRecord}, u) + require.NoError(t, pph.unfold(&probe, u)) + require.Equal(t, 2, pph.grid.activeRows) + + require.NoError(t, pph.fold()) + require.NoError(t, pph.fold()) + + require.Equal(t, 0, pph.grid.activeRows) + require.Equal(t, pbinNodeBranch, pph.grid.root.kind) + require.Equal(t, prefix, pph.grid.root.prefix, "the propagate hands back every consumed bit") + require.Equal(t, nodeHash, pph.grid.root.hash) + + want := pbinOracleRoot([]pbinOracleEntry{a.entry(), b.entry()}) + require.Equal(t, common.Hash(want), pph.grid.root.hash) + require.Equal(t, 1, ctx.branchReads, "the descent reads the node once") + require.Zero(t, pph.counters.materializeReads, "a descended node keeps its children") + }) + } +} + +// TestPBinFoldSplitLeafSurvivorReadsNoBranch pins the short circuit: a leaf +// commits its complete key, so shortening the prefix it sits behind cannot +// invalidate anything and no record has to be read to rebuild it. +func TestPBinFoldSplitLeafSurvivorReadsNoBranch(t *testing.T) { + t.Parallel() + + base := pbinTestBaseStorageKey() + for _, divergence := range []int16{8, 63, 64, 65, 271, 527} { + t.Run(fmt.Sprintf("bit %d", divergence), func(t *testing.T) { + t.Parallel() + + a := pbinTestStorageLeaf(base, 0x55) + c := pbinTestStorageLeaf(pbinTestTreeKeyFlipped(t, base, divergence), 0x66) + + ms := NewMockState(t) + pbinTestPutState(t, ms, a, c) + ctx := &pbinTestCountingCtx{PatriciaContext: ms} + pph := NewPBinPatriciaHashed(ctx) + pph.grid.root = a.cell(t, 0) + pph.rootPresent = true + + probe := pbinPathFromBytes(c.treeKey) + u := pph.needUnfolding(&probe) + require.Equal(t, pbinUnfolding{action: pbinUnfoldSplit, matched: divergence}, u) + require.NoError(t, pph.unfold(&probe, u)) + require.Equal(t, uint64(1), pph.counters.splitsInsidePrefix) + + pbinTestFillCell(pph, 0, probe.bit(divergence), c.cell(t, divergence+1)) + require.NoError(t, pph.fold()) + + want := pbinOracleRoot([]pbinOracleEntry{a.entry(), c.entry()}) + require.Equal(t, common.Hash(want), pph.grid.root.hash) + require.Zero(t, ctx.branchReads, "a leaf survivor needs no record") + require.Zero(t, pph.counters.materializeReads) + }) + } +} + +// TestPBinFoldSplitInsidePrefixMatchesOracle guards H1: the survivor of a split +// keeps prefix[matched+1:], and the prefix is inside its hash, so the cached one +// is stale. The engine has to rebuild it from the survivor's own children before +// the fold above can use it. +func TestPBinFoldSplitInsidePrefixMatchesOracle(t *testing.T) { + t.Parallel() + + base := pbinTestBaseStorageKey() + const nodePrefixBits = 527 + + for _, divergence := range []int16{8, 63, 64, 65, 271, 526} { + t.Run(fmt.Sprintf("bit %d of %d", divergence, nodePrefixBits), func(t *testing.T) { + t.Parallel() + + a := pbinTestStorageLeaf(base, 0x77) + b := pbinTestStorageLeaf(pbinTestTreeKeyFlipped(t, base, nodePrefixBits), 0x88) + c := pbinTestStorageLeaf(pbinTestTreeKeyFlipped(t, base, divergence), 0x99) + left, right := pbinTestBranchOrder(t, a, b, nodePrefixBits) + nodePrefix := pbinTestKeyPrefix(a.treeKey, nodePrefixBits) + + ms := NewMockState(t) + pbinTestPutState(t, ms, a, b, c) + + builder := NewPBinPatriciaHashed(ms) + cells := [2]pbinCell{left.cell(t, nodePrefixBits+1), right.cell(t, nodePrefixBits+1)} + pbinTestSeedRow(builder, nodePrefix, nodePrefixBits+1, cells, 0b11, 0b11) + require.NoError(t, builder.fold()) + + ctx := &pbinTestCountingCtx{PatriciaContext: ms} + pph := NewPBinPatriciaHashed(ctx) + pph.grid.root = pbinTestEmptyCell() + pph.grid.root.kind = pbinNodeBranch + pph.grid.root.prefix = nodePrefix + pph.grid.root.hash = builder.grid.root.hash + pph.grid.root.hashLen = length.Hash + pph.rootPresent = true + + probe := pbinPathFromBytes(c.treeKey) + u := pph.needUnfolding(&probe) + require.Equal(t, pbinUnfolding{action: pbinUnfoldSplit, matched: divergence}, u) + require.NoError(t, pph.unfold(&probe, u)) + require.Equal(t, uint64(1), pph.counters.splitsInsidePrefix) + + survivorBit := 1 - probe.bit(divergence) + survivor := &pph.grid.rows[0][survivorBit] + require.Equal(t, pbinNodeBranch, survivor.kind) + require.Equal(t, nodePrefix.slice(divergence+1, nodePrefixBits), survivor.prefix) + require.Equal(t, int16(0), survivor.hashLen, "a shortened prefix voids the cached hash") + + pbinTestFillCell(pph, 0, probe.bit(divergence), c.cell(t, divergence+1)) + require.NoError(t, pph.fold()) + + want := pbinOracleRoot([]pbinOracleEntry{a.entry(), b.entry(), c.entry()}) + require.Equal(t, common.Hash(want), pph.grid.root.hash) + require.Equal(t, nodePrefix.slice(0, divergence), pph.grid.root.prefix) + require.Equal(t, uint64(1), pph.counters.materializeReads, "the survivor is rebuilt from one record") + }) + } +} + +// A cell whose subtree is stored but missing cannot be rebuilt, and passing the +// stale hash off as current would commit a wrong root. +func TestPBinFoldSplitInsidePrefixMissingRecord(t *testing.T) { + t.Parallel() + + base := pbinTestBaseStorageKey() + const nodePrefixBits = 271 + const divergence = 64 + + a := pbinTestStorageLeaf(base, 0xA1) + c := pbinTestStorageLeaf(pbinTestTreeKeyFlipped(t, base, divergence), 0xA2) + nodePrefix := pbinTestKeyPrefix(a.treeKey, nodePrefixBits) + + ms := NewMockState(t) + pbinTestPutState(t, ms, a, c) + pph := NewPBinPatriciaHashed(ms) + pph.grid.root = pbinTestEmptyCell() + pph.grid.root.kind = pbinNodeBranch + pph.grid.root.prefix = nodePrefix + pph.grid.root.hash = common.Hash{0xDE, 0xAD} + pph.grid.root.hashLen = length.Hash + pph.rootPresent = true + + probe := pbinPathFromBytes(c.treeKey) + require.NoError(t, pph.unfold(&probe, pph.needUnfolding(&probe))) + pbinTestFillCell(pph, 0, probe.bit(divergence), c.cell(t, divergence+1)) + + require.ErrorIs(t, pph.fold(), errPBinMissingBranch) +} + +// TestPBinFoldLoadsSiblingState covers the untouched half of a branch: a record +// carries plain keys, not values, so a sibling that nothing in this run touched +// has to be read back from state before it can be hashed. +func TestPBinFoldLoadsSiblingState(t *testing.T) { + t.Parallel() + + base := pbinTestBaseStorageKey() + const divergence = 271 + + a := pbinTestStorageLeaf(base, 0xB1) + b := pbinTestStorageLeaf(pbinTestTreeKeyFlipped(t, base, divergence), 0xB2) + left, right := pbinTestBranchOrder(t, a, b, divergence) + prefix := pbinTestKeyPrefix(a.treeKey, divergence) + + ms := NewMockState(t) + pbinTestPutState(t, ms, a, b) + pph := NewPBinPatriciaHashed(ms) + + stateless := [2]pbinCell{left.cell(t, divergence+1), right.cell(t, divergence+1)} + for i := range stateless { + stateless[i].Update.Reset() + stateless[i].loaded = cellLoadNone + } + pbinTestSeedRow(pph, prefix, divergence+1, stateless, 0b11, 0b11) + require.NoError(t, pph.fold()) + + want := pbinOracleRoot([]pbinOracleEntry{a.entry(), b.entry()}) + require.Equal(t, common.Hash(want), pph.grid.root.hash) +} + +// TestPBinFoldDeleteDropsRecord pins the third dispatch arm: a row that keeps +// nothing takes its stored record with it and reports the absence upwards. +func TestPBinFoldDeleteDropsRecord(t *testing.T) { + t.Parallel() + + pph, ms := pbinTestEngine(t) + key := pbinBitpath{} + pbinTestPutRecord(t, ms, key, [2]pbinCell{ + pbinTestSpecCell(t, pbinNodeLeaf, "010"), + pbinTestSpecCell(t, pbinNodeLeaf, "110"), + }) + + probe := pbinTestPathFromBits(t, pbinTestBitSpec(t, "0101")) + require.NoError(t, pph.unfold(&probe, pph.needUnfolding(&probe))) + require.True(t, pph.grid.branchBefore[0]) + pph.grid.touchMap[0], pph.grid.afterMap[0] = 0b11, 0 + + require.NoError(t, pph.fold()) + require.Equal(t, pbinTestEmptyCell(), pph.grid.root) + require.True(t, pph.rootTouched) + require.False(t, pph.rootPresent) + + data, _, err := ms.Branch(pbinEncodeBitPath(&key)) + require.NoError(t, err) + require.Empty(t, data) +} diff --git a/execution/commitment/pbin_hash.go b/execution/commitment/pbin_hash.go index 59c7bd5cb48..26aaa4aeb66 100644 --- a/execution/commitment/pbin_hash.go +++ b/execution/commitment/pbin_hash.go @@ -94,6 +94,9 @@ func (h *pbinHasher) cellHash(c *pbinCell, path *pbinBitpath) (common.Hash, erro case pbinNodeEmpty: return pbinEmptyTreeHash, nil case pbinNodeBranch: + if c.childrenSet { + return h.branchHash(&c.prefix, &c.children[0], &c.children[1]), nil + } if c.hashLen != length.Hash { return common.Hash{}, fmt.Errorf("%w: branch cell holds %d hash bytes", errPBinCellHash, c.hashLen) } diff --git a/execution/commitment/pbin_patricia_hashed.go b/execution/commitment/pbin_patricia_hashed.go index faa051f097d..f000389c30a 100644 --- a/execution/commitment/pbin_patricia_hashed.go +++ b/execution/commitment/pbin_patricia_hashed.go @@ -17,8 +17,13 @@ package commitment import ( + "bytes" "errors" "fmt" + "math/bits" + + "github.com/erigontech/erigon/common" + "github.com/erigontech/erigon/common/length" ) // PBinPatriciaHashed computes commitment over EIP-8297's partitioned binary @@ -26,16 +31,28 @@ import ( // node model: arity is 2, there is no extension node and no storage root, and a // leaf commits its complete tree key. type PBinPatriciaHashed struct { - grid pbinGrid - currentKey pbinBitpath // path from the root to the deepest active row, one bit per level - ctx PatriciaContext - hasher pbinHasher + grid pbinGrid + currentKey pbinBitpath // path from the root to the deepest active row, one bit per level + ctx PatriciaContext + hasher pbinHasher + branchEncoder pbinBranchEncoder + counters pbinCounters rootChecked bool // whether the root record is known to be absent rootTouched bool rootPresent bool } +// pbinCounters measures what keeping a single hash per branch cell costs. A +// probe diverging inside a stored prefix invalidates that hash, and rebuilding +// it needs a branch read the descent itself would not have made. Storing both +// child hashes per cell instead is a wire-format change, so it waits on these +// numbers. +type pbinCounters struct { + splitsInsidePrefix uint64 + materializeReads uint64 +} + func NewPBinPatriciaHashed(ctx PatriciaContext) *PBinPatriciaHashed { return &PBinPatriciaHashed{ctx: ctx} } @@ -153,6 +170,9 @@ func (pph *PBinPatriciaHashed) unfold(probe *pbinBitpath, u pbinUnfolding) error if err != nil { return err } + if u.action == pbinUnfoldSplit { + pph.counters.splitsInsidePrefix++ + } bit := upCell.prefix.bit(consumed - 1) if touched { g.touchMap[row] = uint16(1) << bit @@ -161,6 +181,7 @@ func (pph *PBinPatriciaHashed) unfold(probe *pbinBitpath, u pbinUnfolding) error g.afterMap[row] = uint16(1) << bit } g.rows[row][bit].fillFromUpperCell(upCell, consumed) + pph.rehashAfterPrefixChange(&g.rows[row][bit]) if consumed > 1 { head := upCell.prefix.slice(0, consumed-1) @@ -227,7 +248,8 @@ func (pph *PBinPatriciaHashed) unfoldBranchNode(row int, depth int16, deleted bo // fillFromUpperCell moves a cell one level down, dropping the prefix bits the // descent has taken over. skip counts those bits and includes the one the new -// row branches on. +// row branches on. It re-cuts the prefix, so the caller owes the cell a +// rehashAfterPrefixChange. func (c *pbinCell) fillFromUpperCell(up *pbinCell, skip int16) { c.reset() if skip < up.prefix.bitLen { @@ -246,6 +268,267 @@ func (c *pbinCell) fillFromUpperCell(up *pbinCell, skip int16) { if up.hashLen > 0 { c.hash = up.hash } + c.children, c.childrenSet = up.children, up.childrenSet c.loaded = up.loaded c.Update = up.Update } + +// fillFromLowerCell moves the sole survivor of a collapsed row into the cell +// above, prepending the bits the row consumed: the ones the parent already +// descended plus the one the row branched on. +func (c *pbinCell) fillFromLowerCell(low *pbinCell, head *pbinBitpath, bit uint64) { + prefix := *head + prefix.appendBit(bit) + prefix.append(&low.prefix) + *c = *low + c.prefix = prefix +} + +// rehashAfterPrefixChange restores the invariant that a set hashLen means the +// hash covers the prefix the cell holds now. A cell that knows its children +// re-derives; one that does not is marked stale for materializeBranch. +func (pph *PBinPatriciaHashed) rehashAfterPrefixChange(c *pbinCell) { + if c.kind != pbinNodeBranch { + return + } + if c.childrenSet { + c.hash = pph.hasher.branchHash(&c.prefix, &c.children[0], &c.children[1]) + c.hashLen = length.Hash + return + } + c.hash, c.hashLen = common.Hash{}, 0 +} + +// fold reduces currentKey by one row: it hashes what the row holds into the cell +// above and, when the row stays a branch, writes the row's record. +func (pph *PBinPatriciaHashed) fold() error { + g := &pph.grid + if g.activeRows == 0 { + return errors.New("pbin: cannot fold with no active rows") + } + row := g.activeRows - 1 + if err := pbinCheckCellMaps(g.touchMap[row], g.afterMap[row]); err != nil { + return err + } + depth := g.depths[row] + if pph.currentKey.bitLen != depth-1 { + return fmt.Errorf("pbin: row %d at depth %d folds under a %d-bit key", row, depth, pph.currentKey.bitLen) + } + + var upCell *pbinCell + var bit uint64 + var upDepth int16 + if row == 0 { + upCell = &g.root + } else { + upDepth = g.depths[row-1] + bit = pph.currentKey.bit(upDepth - 1) + upCell = &g.rows[row-1][bit] + } + + var err error + switch kind, _ := afterMapUpdateKind(g.afterMap[row]); kind { + case updateKindDelete: + err = pph.foldDelete(row, bit, upCell) + case updateKindPropagate: + err = pph.foldPropagate(row, bit, upDepth, depth, upCell) + case updateKindBranch: + err = pph.foldBranch(row, bit, upDepth, depth, upCell) + } + if err != nil { + return err + } + g.activeRows-- + pph.currentKey.truncate(max(upDepth-1, 0)) + return nil +} + +// foldBranch hashes a row that keeps both cells and stores it as one record, +// keyed by the bit path down to the branch bit. +func (pph *PBinPatriciaHashed) foldBranch(row int, bit uint64, upDepth, depth int16, upCell *pbinCell) error { + g := &pph.grid + if n := bits.OnesCount16(g.afterMap[row]); n != 2 { + return fmt.Errorf("pbin: branch fold at row %d keeps %d cells, want 2", row, n) + } + pph.propagateTouch(row, bit) + + childPath := pph.currentKey + childPath.appendBit(0) + left, err := pph.hashRowCell(&g.rows[row][0], &childPath) + if err != nil { + return err + } + childPath.setBitAt(depth-1, 1) + right, err := pph.hashRowCell(&g.rows[row][1], &childPath) + if err != nil { + return err + } + + key := pbinEncodeBitPath(&pph.currentKey) + record, err := pph.branchEncoder.encode(g.touchMap[row], g.afterMap[row], &g.rows[row]) + if err != nil { + return err + } + if err = pph.ctx.PutBranch(key, bytes.Clone(record), nil); err != nil { + return fmt.Errorf("pbin: write branch at %x: %w", key, err) + } + + prefix := pph.currentKey.slice(upDepth, depth-1) + upCell.reset() + upCell.kind = pbinNodeBranch + upCell.prefix = prefix + upCell.children, upCell.childrenSet = [2]common.Hash{left, right}, true + upCell.hash = pph.hasher.branchHash(&prefix, &left, &right) + upCell.hashLen = length.Hash + return nil +} + +// foldPropagate collapses a row down to its sole survivor. The node moves up +// rather than being rewritten, so no record is written and the bits the row +// consumed are prepended to the survivor's own prefix. +func (pph *PBinPatriciaHashed) foldPropagate(row int, bit uint64, upDepth, depth int16, upCell *pbinCell) error { + g := &pph.grid + pph.propagateTouch(row, bit) + + childBit := bits.TrailingZeros16(g.afterMap[row]) + child := &g.rows[row][childBit] + + head := pph.currentKey.slice(upDepth, depth-1) + upCell.fillFromLowerCell(child, &head, uint64(childBit)) + // The row's own bit is part of what moves up: dropping it still hashes, and + // still gives the wrong root. + if want := depth - upDepth + child.prefix.bitLen; upCell.prefix.bitLen != want { + return fmt.Errorf("pbin: propagate at row %d formed a %d-bit prefix, want %d", row, upCell.prefix.bitLen, want) + } + pph.rehashAfterPrefixChange(upCell) + return nil +} + +// foldDelete drops a row that kept nothing, taking the record it came from with +// it. +func (pph *PBinPatriciaHashed) foldDelete(row int, bit uint64, upCell *pbinCell) error { + g := &pph.grid + if g.touchMap[row] != 0 { + if row == 0 { + pph.rootTouched, pph.rootPresent = true, false + } else { + g.touchMap[row-1] |= uint16(1) << bit + g.afterMap[row-1] &^= uint16(1) << bit + } + } + upCell.reset() + if !g.branchBefore[row] { + return nil + } + key := pbinEncodeBitPath(&pph.currentKey) + if err := pph.ctx.PutBranch(key, nil, nil); err != nil { + return fmt.Errorf("pbin: delete branch at %x: %w", key, err) + } + return nil +} + +// propagateTouch carries a modification to the row above. A fold that leaves a +// node behind also marks the root present: without it the next unfold reads +// touched and absent, and drops the whole subtree. +func (pph *PBinPatriciaHashed) propagateTouch(row int, bit uint64) { + if pph.grid.touchMap[row] == 0 { + return + } + if row == 0 { + pph.rootTouched, pph.rootPresent = true, true + return + } + pph.grid.touchMap[row-1] |= uint16(1) << bit +} + +// hashRowCell hashes one cell of a folding row and writes the result back, so +// the record the row produces carries every child hash a later read needs. +func (pph *PBinPatriciaHashed) hashRowCell(c *pbinCell, path *pbinBitpath) (common.Hash, error) { + h, err := pph.cellHash(c, path) + if err != nil { + return common.Hash{}, err + } + if c.kind == pbinNodeBranch { + c.hash, c.hashLen = h, length.Hash + } + return h, nil +} + +// cellHash resolves whatever a cell is missing — a leaf's state, a branch's +// stale hash — and hands it to the one hasher. +func (pph *PBinPatriciaHashed) cellHash(c *pbinCell, path *pbinBitpath) (common.Hash, error) { + switch c.kind { + case pbinNodeLeaf: + if err := pph.loadCellState(c); err != nil { + return common.Hash{}, err + } + case pbinNodeBranch: + if !c.childrenSet && c.hashLen == 0 { + if err := pph.materializeBranch(c, path); err != nil { + return common.Hash{}, err + } + } + } + return pph.hasher.cellHash(c, path) +} + +// loadCellState fills a leaf cell whose plain key arrived from a record and +// whose value therefore did not. +func (pph *PBinPatriciaHashed) loadCellState(c *pbinCell) error { + if c.accountAddrLen > 0 && !c.loaded.account() { + update, err := pph.ctx.Account(c.accountAddr[:c.accountAddrLen]) + if err != nil { + return fmt.Errorf("pbin: read account %x: %w", c.accountAddr[:c.accountAddrLen], err) + } + c.setFromUpdate(update) + c.loaded = c.loaded.addFlag(cellLoadAccount) + } + if c.storageAddrLen > 0 && !c.loaded.storage() { + update, err := pph.ctx.Storage(c.storageAddr[:c.storageAddrLen]) + if err != nil { + return fmt.Errorf("pbin: read storage %x: %w", c.storageAddr[:c.storageAddrLen], err) + } + c.setFromUpdate(update) + c.loaded = c.loaded.addFlag(cellLoadStorage) + } + return nil +} + +// materializeBranch rebuilds a branch cell's hash under the prefix it holds now +// by reading its own record. A split shortens a survivor's prefix without moving +// its record, so the key is the cell's path followed by that prefix. +func (pph *PBinPatriciaHashed) materializeBranch(c *pbinCell, path *pbinBitpath) error { + nodeKey := *path + nodeKey.append(&c.prefix) + key := pbinEncodeBitPath(&nodeKey) + + data, _, err := pph.ctx.Branch(key) + if err != nil { + return fmt.Errorf("pbin: read branch at %x: %w", key, err) + } + if len(data) == 0 { + return fmt.Errorf("%w at %x (%d bits)", errPBinMissingBranch, key, nodeKey.bitLen) + } + pph.counters.materializeReads++ + + var cells [2]pbinCell + if _, _, err = pbinDecodeBranch(data, &cells); err != nil { + return fmt.Errorf("pbin: decode branch at %x: %w", key, err) + } + childPath := nodeKey + childPath.appendBit(0) + left, err := pph.cellHash(&cells[0], &childPath) + if err != nil { + return err + } + childPath.setBitAt(nodeKey.bitLen, 1) + right, err := pph.cellHash(&cells[1], &childPath) + if err != nil { + return err + } + + c.children, c.childrenSet = [2]common.Hash{left, right}, true + c.hash = pph.hasher.branchHash(&c.prefix, &left, &right) + c.hashLen = length.Hash + return nil +} diff --git a/execution/commitment/pbin_unfold_test.go b/execution/commitment/pbin_unfold_test.go index 5f487307161..44c7b8a6e91 100644 --- a/execution/commitment/pbin_unfold_test.go +++ b/execution/commitment/pbin_unfold_test.go @@ -318,7 +318,8 @@ func TestPBinUnfoldDescendsThroughPrefix(t *testing.T) { cell := &pph.grid.rows[0][branchBit] require.Equal(t, pbinNodeBranch, cell.kind) require.Equal(t, int16(0), cell.prefix.bitLen, "the whole prefix moved into the descent key") - require.Equal(t, pph.grid.root.hash, cell.hash) + require.Equal(t, int16(0), cell.hashLen, + "the prefix is inside the branch hash, so moving it out of the cell invalidates it") u = pph.needUnfolding(&full) require.Equal(t, pbinUnfolding{action: pbinUnfoldRecord}, u) From 1151d532c550b4a31a11e34d4e431db6ca3b1374 Mon Sep 17 00:00:00 2001 From: awskii Date: Wed, 29 Jul 2026 18:48:19 +0700 Subject: [PATCH 10/56] feat: EIP-8297 drive loop, Process and RootHash --- docs/plans/20260729-pbin-patricia-hashed.md | 16 +- execution/commitment/pbin_patricia_hashed.go | 175 +++++++++- execution/commitment/pbin_process_test.go | 339 +++++++++++++++++++ 3 files changed, 521 insertions(+), 9 deletions(-) create mode 100644 execution/commitment/pbin_process_test.go diff --git a/docs/plans/20260729-pbin-patricia-hashed.md b/docs/plans/20260729-pbin-patricia-hashed.md index 6d285742a90..f05125065b4 100644 --- a/docs/plans/20260729-pbin-patricia-hashed.md +++ b/docs/plans/20260729-pbin-patricia-hashed.md @@ -230,14 +230,14 @@ func pbinCommonPrefixBits(a, b *pbinBitpath) int16 // XOR + LeadingZeros64, clam - Modify: `execution/commitment/pbin_patricia_hashed.go` - Create: `execution/commitment/pbin_process_test.go` -- [ ] write a failing test asserting `RootHash()` on a fresh engine is 32 zero bytes, not `empty.RootHash` (guards H11) -- [ ] write a failing test for a one-key tree asserting the root **is** the leaf hash `H(0x00||key||value)` (`eip:133-135`), and for a two-key tree asserting it is the branch hash -- [ ] write failing `Process` tests over `MockState` for account-only, storage-only and mixed corpora, asserting root equality with the oracle -- [ ] implement `pbinUpdateCell`, the key-path descent, and the `Process` drive loop -- [ ] implement `RootHash` including the root-as-leaf case -- [ ] implement the account fan-out: write the `CODE_HASH` leaf at `sub_index+1` during the same stem visit, leaving `Updates`/`HashSort`/`TouchPlainKey` untouched -- [ ] reject deletes originating from the **update stream** only; a missing-key `ctx.Account`/`ctx.Storage` read returns `DeleteUpdate` (`patricia_state_mock_test.go:92-95`, `:129-134`) and MUST be treated as absent, not as a delete (guards H13) -- [ ] run tests - must pass before task 10 +- [x] write a failing test asserting `RootHash()` on a fresh engine is 32 zero bytes, not `empty.RootHash` (guards H11) +- [x] write a failing test for a one-key tree asserting the root **is** the leaf hash `H(0x00||key||value)` (`eip:133-135`), and for a two-key tree asserting it is the branch hash +- [x] write failing `Process` tests over `MockState` for account-only, storage-only and mixed corpora, asserting root equality with the oracle +- [x] implement `pbinUpdateCell`, the key-path descent, and the `Process` drive loop — landed as the methods `updateCell`/`followAndUpdate`/`processKey`, same reasoning as Task 7's `unfold` +- [x] implement `RootHash` including the root-as-leaf case +- [x] implement the account fan-out: write the `CODE_HASH` leaf at `sub_index+1` during the same stem visit, leaving `Updates`/`HashSort`/`TouchPlainKey` untouched +- [x] reject deletes originating from the **update stream** only; a missing-key `ctx.Account`/`ctx.Storage` read returns `DeleteUpdate` (`patricia_state_mock_test.go:92-95`, `:129-134`) and MUST be treated as absent, not as a delete (guards H13) +- [x] run tests - must pass before task 10 ### Task 10: variant registration diff --git a/execution/commitment/pbin_patricia_hashed.go b/execution/commitment/pbin_patricia_hashed.go index f000389c30a..523be50b38e 100644 --- a/execution/commitment/pbin_patricia_hashed.go +++ b/execution/commitment/pbin_patricia_hashed.go @@ -18,6 +18,7 @@ package commitment import ( "bytes" + "context" "errors" "fmt" "math/bits" @@ -38,6 +39,8 @@ type PBinPatriciaHashed struct { branchEncoder pbinBranchEncoder counters pbinCounters + siblingKey [pbinAccountKeyLength]byte // scratch for the CODE_HASH key of the account being visited + rootChecked bool // whether the root record is known to be absent rootTouched bool rootPresent bool @@ -57,7 +60,177 @@ func NewPBinPatriciaHashed(ctx PatriciaContext) *PBinPatriciaHashed { return &PBinPatriciaHashed{ctx: ctx} } -var errPBinMissingBranch = errors.New("pbin: branch record missing") +var ( + errPBinMissingBranch = errors.New("pbin: branch record missing") + errPBinDeleteUnsupported = errors.New("pbin: EIP-8297 defines no deletion") +) + +// Process folds the update stream into the tree and returns the new root. +// HashSort hands keys over in tree-key order, which is descent order, so the +// grid only ever walks the path between two consecutive keys. +// +// M0 ignores warmup: the engine runs against an in-memory context, so there is +// no page cache to pre-warm. +func (pph *PBinPatriciaHashed) Process(ctx context.Context, updates *Updates, logPrefix string, onProgress func(*CommitProgress), warmup WarmupConfig) ([]byte, error) { + var processed uint64 + err := updates.HashSort(ctx, nil, func(treeKey, plainKey []byte, stateUpdate *Update) error { + if err := pph.processKey(treeKey, plainKey, stateUpdate); err != nil { + return err + } + processed++ + return nil + }) + if err != nil { + return nil, fmt.Errorf("pbin: process %s: %w", logPrefix, err) + } + for pph.grid.activeRows > 0 { + if err = pph.fold(); err != nil { + return nil, fmt.Errorf("pbin: final fold: %w", err) + } + } + if onProgress != nil { + onProgress(&CommitProgress{KeyIndex: processed, UpdateCount: processed}) + } + return pph.RootHash() +} + +// processKey routes one update into the tree. An account fans out to two leaves +// visited back to back — BASIC_DATA and the CODE_HASH sibling at the next +// sub-index — which is what lets the shared keyHasher stay a one-key function. +func (pph *PBinPatriciaHashed) processKey(treeKey, plainKey []byte, stateUpdate *Update) error { + if stateUpdate != nil && stateUpdate.Deleted() { + return fmt.Errorf("%w: update for %x", errPBinDeleteUnsupported, plainKey) + } + update := stateUpdate + if update == nil { + var err error + if update, err = pph.stateOf(plainKey); err != nil { + return err + } + // A key with no state reads back as a delete; under EIP-8297 that means + // there is no leaf here, not that one has to be removed. + if update.Deleted() { + return nil + } + } + if err := pph.followAndUpdate(treeKey, plainKey, update); err != nil { + return err + } + if len(plainKey) != length.Addr { + return nil + } + codeKey, err := pph.codeHashKey(treeKey) + if err != nil { + return err + } + return pph.followAndUpdate(codeKey, plainKey, update) +} + +func (pph *PBinPatriciaHashed) stateOf(plainKey []byte) (*Update, error) { + if len(plainKey) == length.Addr { + update, err := pph.ctx.Account(plainKey) + if err != nil { + return nil, fmt.Errorf("pbin: read account %x: %w", plainKey, err) + } + return update, nil + } + update, err := pph.ctx.Storage(plainKey) + if err != nil { + return nil, fmt.Errorf("pbin: read storage %x: %w", plainKey, err) + } + return update, nil +} + +// codeHashKey is the CODE_HASH leaf beside a BASIC_DATA key: same stem, next +// sub-index (eip:311-320). The two sit adjacent in key order, so visiting them +// together never walks the descent backwards. +func (pph *PBinPatriciaHashed) codeHashKey(basicDataKey []byte) ([]byte, error) { + if len(basicDataKey) != pbinAccountKeyLength || basicDataKey[pbinAccountKeyLength-1] != pbinBasicDataLeafKey { + return nil, fmt.Errorf("pbin: %x is not a BASIC_DATA key", basicDataKey) + } + copy(pph.siblingKey[:], basicDataKey) + pph.siblingKey[pbinAccountKeyLength-1] = pbinCodeHashLeafKey + return pph.siblingKey[:], nil +} + +// followAndUpdate moves the grid onto treeKey and writes the update into the +// cell that lands there. +func (pph *PBinPatriciaHashed) followAndUpdate(treeKey, plainKey []byte, update *Update) error { + probe := pbinPathFromBytes(treeKey) + for !probe.hasPrefix(&pph.currentKey) { + if err := pph.fold(); err != nil { + return err + } + } + for u := pph.needUnfolding(&probe); u.action != pbinUnfoldNone; u = pph.needUnfolding(&probe) { + if err := pph.unfold(&probe, u); err != nil { + return err + } + } + return pph.updateCell(plainKey, &probe, update) +} + +// updateCell writes one leaf into the deepest open row. Unfolding has already +// made the target either empty — a new leaf, whose prefix is the rest of the +// key — or the same leaf touched again. +func (pph *PBinPatriciaHashed) updateCell(plainKey []byte, probe *pbinBitpath, update *Update) error { + g := &pph.grid + var c *pbinCell + var depth int16 + if g.activeRows == 0 { + c = &g.root + pph.rootTouched, pph.rootPresent = true, true + } else { + row := g.activeRows - 1 + depth = g.depths[row] + if probe.bitLen < depth { + return fmt.Errorf("pbin: a %d-bit key cannot be updated in a row at depth %d", probe.bitLen, depth) + } + bit := probe.bit(depth - 1) + c = &g.rows[row][bit] + g.touchMap[row] |= uint16(1) << bit + g.afterMap[row] |= uint16(1) << bit + } + + switch c.kind { + case pbinNodeEmpty: + c.kind = pbinNodeLeaf + c.prefix = probe.slice(depth, probe.bitLen) + case pbinNodeLeaf: + default: + return fmt.Errorf("pbin: update for a %d-bit key lands on a branch cell", probe.bitLen) + } + + switch len(plainKey) { + case length.Addr: + c.accountAddrLen = int16(len(plainKey)) + copy(c.accountAddr[:], plainKey) + c.loaded = c.loaded.addFlag(cellLoadAccount) + case length.Addr + length.Hash: + c.storageAddrLen = int16(len(plainKey)) + copy(c.storageAddr[:], plainKey) + c.loaded = c.loaded.addFlag(cellLoadStorage) + default: + return fmt.Errorf("pbin: plain key of %d bytes is neither an account nor a storage key", len(plainKey)) + } + c.setFromUpdate(update) + return nil +} + +// RootHash hashes whatever the root cell holds. A one-key tree's root is the +// leaf itself (eip:133-135) and an empty tree is 32 zero bytes (eip:208), both +// of which fall out of hashing the cell rather than special-casing the shape. +func (pph *PBinPatriciaHashed) RootHash() ([]byte, error) { + if pph.grid.activeRows != 0 { + return nil, fmt.Errorf("pbin: root hash requested with %d rows still open", pph.grid.activeRows) + } + var path pbinBitpath + hash, err := pph.cellHash(&pph.grid.root, &path) + if err != nil { + return nil, err + } + return hash[:], nil +} // pbinUnfoldAction is what needUnfolding tells unfold to do about one cell. type pbinUnfoldAction uint8 diff --git a/execution/commitment/pbin_process_test.go b/execution/commitment/pbin_process_test.go new file mode 100644 index 00000000000..0d55e1ca379 --- /dev/null +++ b/execution/commitment/pbin_process_test.go @@ -0,0 +1,339 @@ +// Copyright 2026 The Erigon Authors +// This file is part of Erigon. +// +// Erigon is free software: you can redistribute it and/or modify +// it under the terms of the GNU Lesser General Public License as published by +// the Free Software Foundation, either version 3 of the License, or +// (at your option) any later version. +// +// Erigon is distributed in the hope that it will be useful, +// but WITHOUT ANY WARRANTY; without even the implied warranty of +// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +// GNU Lesser General Public License for more details. +// +// You should have received a copy of the GNU Lesser General Public License +// along with Erigon. If not, see . + +package commitment + +import ( + "bytes" + "context" + "testing" + + "github.com/stretchr/testify/require" + + "github.com/erigontech/erigon/common" + "github.com/erigontech/erigon/common/empty" + "github.com/erigontech/erigon/common/length" +) + +// pbinTestCorpus collects plain-key updates in the two shapes the engine +// accepts and derives the leaf set they must produce, so a Process run can be +// diffed against the reference tree over the same entries. +type pbinTestCorpus struct { + plainKeys [][]byte + updates []Update +} + +func (c *pbinTestCorpus) account(addr []byte, nonce, balance uint64, codeHash common.Hash) *pbinTestCorpus { + u := Update{Flags: NonceUpdate | BalanceUpdate | CodeUpdate, Nonce: nonce, CodeHash: codeHash} + u.Balance.SetUint64(balance) + c.plainKeys = append(c.plainKeys, bytes.Clone(addr)) + c.updates = append(c.updates, u) + return c +} + +func (c *pbinTestCorpus) storage(addr, slot []byte, value ...byte) *pbinTestCorpus { + u := Update{Flags: StorageUpdate, StorageLen: int8(len(value))} + copy(u.Storage[:], value) + c.plainKeys = append(c.plainKeys, append(bytes.Clone(addr), slot...)) + c.updates = append(c.updates, u) + return c +} + +// entries is the leaf set the corpus stands for. An account is two leaves, so +// this is also where the fan-out is stated independently of the engine. +func (c *pbinTestCorpus) entries(t *testing.T) []pbinOracleEntry { + t.Helper() + entries := make([]pbinOracleEntry, 0, len(c.plainKeys)) + for i, plainKey := range c.plainKeys { + u := &c.updates[i] + switch len(plainKey) { + case length.Addr: + basic, err := pbinEncodeBasicData(u.Nonce, &u.Balance, 0) + require.NoError(t, err) + code := pbinCodeHashValue(u.CodeHash) + entries = append(entries, + pbinOracleEntry{key: pbinTreeKeyAccount(plainKey, pbinBasicDataLeafKey), value: basic[:]}, + pbinOracleEntry{key: pbinTreeKeyAccount(plainKey, pbinCodeHashLeafKey), value: code[:]}) + case length.Addr + length.Hash: + value := pbinEncodeStorageValue(u.Storage[:u.StorageLen]) + entries = append(entries, pbinOracleEntry{ + key: pbinTreeKeyStorage(plainKey[:length.Addr], plainKey[length.Addr:]), + value: value[:], + }) + default: + t.Fatalf("plain key of %d bytes is neither an account nor a storage key", len(plainKey)) + } + } + return entries +} + +func (c *pbinTestCorpus) oracleRoot(t *testing.T) []byte { + t.Helper() + root := pbinOracleRoot(c.entries(t)) + return root[:] +} + +// process applies the corpus to state, then runs it through the engine the way +// the domain layer would: ModeDirect, so every value comes back through the +// context rather than the update stream. +func (c *pbinTestCorpus) process(t *testing.T) (*PBinPatriciaHashed, []byte) { + t.Helper() + pph, ms := pbinTestEngine(t) + require.NoError(t, ms.applyPlainUpdates(c.plainKeys, c.updates)) + return pph, pbinTestProcess(t, pph, c.plainKeys, c.updates) +} + +func pbinTestProcess(t *testing.T, pph *PBinPatriciaHashed, plainKeys [][]byte, updates []Update) []byte { + t.Helper() + upd := WrapKeyUpdates(t, ModeDirect, pbinKeyHasher(), plainKeys, updates) + root, err := pph.Process(context.Background(), upd, "", nil, WarmupConfig{}) + require.NoError(t, err) + return root +} + +// TestPBinRootHashEmptyEngine guards H11 at the engine boundary: an empty +// EIP-8297 tree is 32 zero bytes (eip:208), not the empty-MPT root the rest of +// erigon reaches for. +func TestPBinRootHashEmptyEngine(t *testing.T) { + t.Parallel() + + pph, _ := pbinTestEngine(t) + root, err := pph.RootHash() + require.NoError(t, err) + require.Equal(t, make([]byte, length.Hash), root) + require.NotEqual(t, empty.RootHash[:], root) +} + +// TestPBinProcessSingleKeyRootIsLeaf pins eip:133-135: with one entry the root +// is the leaf itself, not a branch wrapping it. +func TestPBinProcessSingleKeyRootIsLeaf(t *testing.T) { + t.Parallel() + + addr, slot := pbinOracleAddr(1), pbinOracleSlot(1000) + corpus := new(pbinTestCorpus).storage(addr, slot, 0x01, 0x02) + + pph, root := corpus.process(t) + require.Equal(t, pbinNodeLeaf, pph.grid.root.kind) + + value := pbinEncodeStorageValue([]byte{0x01, 0x02}) + want := pbinTestKeccak(t, []byte{0x00}, pbinTreeKeyStorage(addr, slot), value[:]) + require.Equal(t, want, root) + require.Equal(t, corpus.oracleRoot(t), root) +} + +// TestPBinProcessTwoKeysRootIsBranch is the other half: a second entry turns the +// root into a branch over the two leaf hashes. +func TestPBinProcessTwoKeysRootIsBranch(t *testing.T) { + t.Parallel() + + addr := pbinOracleAddr(2) + a, b := pbinOracleSlot(256), pbinOracleSlot(257) + corpus := new(pbinTestCorpus).storage(addr, a, 0xAA).storage(addr, b, 0xBB) + + pph, root := corpus.process(t) + require.Equal(t, pbinNodeBranch, pph.grid.root.kind) + + // The two sub-indices differ only in their low bit, so the branch prefix is + // every bit of the key but the last and slot 256 takes the left side. + left := pbinEncodeStorageValue([]byte{0xAA}) + right := pbinEncodeStorageValue([]byte{0xBB}) + leftHash := pbinTestKeccak(t, []byte{0x00}, pbinTreeKeyStorage(addr, a), left[:]) + rightHash := pbinTestKeccak(t, []byte{0x00}, pbinTreeKeyStorage(addr, b), right[:]) + prefix := pbinOracleBytesToBits(pbinTreeKeyStorage(addr, a))[:pbinMaxPathBits-1] + want := pbinTestKeccak(t, []byte{0x01}, pbinOracleEncodeBitPrefix(prefix), leftHash, rightHash) + + require.Equal(t, want, root) + require.Equal(t, corpus.oracleRoot(t), root) +} + +// TestPBinProcessMatchesOracle is the M0 gate: for every corpus shape the engine +// must reproduce the reference tree's root. +func TestPBinProcessMatchesOracle(t *testing.T) { + t.Parallel() + + for _, tc := range []struct { + name string + corpus *pbinTestCorpus + }{ + { + name: "one account", + corpus: new(pbinTestCorpus).account(pbinOracleAddr(1), 3, 7, common.Hash{0xC0, 0xDE}), + }, + { + name: "accounts only", + corpus: new(pbinTestCorpus). + account(pbinOracleAddr(1), 1, 100, common.Hash{0x01}). + account(pbinOracleAddr(2), 0, 0, common.Hash{}). + account(pbinOracleAddr(3), 1<<40, 1<<62, empty.CodeHash), + }, + { + name: "storage zone only", + corpus: new(pbinTestCorpus). + storage(pbinOracleAddr(4), pbinOracleSlot(64), 0x01). + storage(pbinOracleAddr(4), pbinOracleSlot(255), 0x02, 0x03). + storage(pbinOracleAddr(4), pbinOracleSlot(256), 0x04). + storage(pbinOracleAddr(4), pbinOracleSlot(1000), bytes.Repeat([]byte{0xEE}, 32)...), + }, + { + name: "header zone slots", + corpus: new(pbinTestCorpus). + storage(pbinOracleAddr(5), pbinOracleSlot(0), 0x01). + storage(pbinOracleAddr(5), pbinOracleSlot(1), 0x02). + storage(pbinOracleAddr(5), pbinOracleSlot(63), 0x03), + }, + { + name: "one account across both zones", + corpus: new(pbinTestCorpus). + account(pbinOracleAddr(6), 9, 1234, common.Hash{0xAB}). + storage(pbinOracleAddr(6), pbinOracleSlot(0), 0x01). + storage(pbinOracleAddr(6), pbinOracleSlot(63), 0x02). + storage(pbinOracleAddr(6), pbinOracleSlot(64), 0x03). + storage(pbinOracleAddr(6), pbinOracleSlot(65), 0x04). + storage(pbinOracleAddr(6), pbinOracleSlot(1000), 0x05), + }, + { + name: "mixed accounts and storage", + corpus: pbinTestMixedCorpus(), + }, + { + name: "deep shared prefix", + corpus: pbinTestDeepSharedPrefixCorpus(), + }, + } { + t.Run(tc.name, func(t *testing.T) { + t.Parallel() + _, root := tc.corpus.process(t) + require.Equal(t, tc.corpus.oracleRoot(t), root) + }) + } +} + +func pbinTestMixedCorpus() *pbinTestCorpus { + c := new(pbinTestCorpus) + for i := uint64(1); i <= 6; i++ { + addr := pbinOracleAddr(i) + c.account(addr, i, i*1000, common.Hash{byte(i)}) + for _, slot := range []uint64{0, 5, 63, 64, 255, 256, 1000, 1 << 20} { + c.storage(addr, pbinOracleSlot(slot), byte(i), byte(slot)) + } + } + return c +} + +// pbinTestDeepSharedPrefixCorpus reuses the mined cluster, so the descent walks +// far past the root before diverging (guards H1's corpus side). +func pbinTestDeepSharedPrefixCorpus() *pbinTestCorpus { + c := new(pbinTestCorpus) + for i, addr := range pbinOracleMinedAddrs() { + c.account(addr, uint64(i), uint64(i)*7, common.Hash{byte(i)}) + } + return c +} + +// TestPBinProcessAccountFansOutToCodeHash pins the sibling leaf: one account +// update produces both BASIC_DATA and CODE_HASH, written during the same stem +// visit so the shared keyHasher stays a one-key function. +func TestPBinProcessAccountFansOutToCodeHash(t *testing.T) { + t.Parallel() + + addr := pbinOracleAddr(11) + codeHash := common.Hash{0xC0, 0xDE, 0xFF} + corpus := new(pbinTestCorpus).account(addr, 5, 999, codeHash) + + pph, root := corpus.process(t) + require.Equal(t, pbinNodeBranch, pph.grid.root.kind, "two leaves under one stem make a branch") + require.Equal(t, corpus.oracleRoot(t), root) + + basic, err := pbinEncodeBasicData(5, &corpus.updates[0].Balance, 0) + require.NoError(t, err) + basicOnly := pbinOracleRoot([]pbinOracleEntry{ + {key: pbinTreeKeyAccount(addr, pbinBasicDataLeafKey), value: basic[:]}, + }) + require.NotEqual(t, basicOnly[:], root, "dropping the CODE_HASH leaf must change the root") + + code := pbinCodeHashValue(codeHash) + require.Equal(t, codeHash[:], code[:]) +} + +// TestPBinProcessRejectsStreamDelete guards H13: EIP-8297 never removes an +// entry, so a delete arriving on the update stream is an error rather than a +// silently applied removal. +func TestPBinProcessRejectsStreamDelete(t *testing.T) { + t.Parallel() + + pph, ms := pbinTestEngine(t) + plainKeys := [][]byte{pbinOracleAddr(1)} + updates := []Update{{Flags: DeleteUpdate}} + require.NoError(t, ms.applyPlainUpdates(plainKeys, updates)) + + upd := WrapKeyUpdates(t, ModeUpdate, pbinKeyHasher(), plainKeys, updates) + _, err := pph.Process(context.Background(), upd, "", nil, WarmupConfig{}) + require.ErrorIs(t, err, errPBinDeleteUnsupported) +} + +// TestPBinProcessMissingStateIsAbsent is H13's other half: a context read for a +// key with no state reports DeleteUpdate, which means "no leaf here" and must +// not be mistaken for a removal. +func TestPBinProcessMissingStateIsAbsent(t *testing.T) { + t.Parallel() + + addr := pbinOracleAddr(21) + present := new(pbinTestCorpus). + storage(addr, pbinOracleSlot(256), 0x01). + storage(addr, pbinOracleSlot(257), 0x02) + + touched := new(pbinTestCorpus). + storage(addr, pbinOracleSlot(256), 0x01). + storage(addr, pbinOracleSlot(257), 0x02). + storage(addr, pbinOracleSlot(258), 0x03). + account(pbinOracleAddr(22), 1, 2, common.Hash{0x03}) + + pph, ms := pbinTestEngine(t) + require.NoError(t, ms.applyPlainUpdates(present.plainKeys, present.updates)) + + root := pbinTestProcess(t, pph, touched.plainKeys, touched.updates) + require.Equal(t, present.oracleRoot(t), root, "keys with no state contribute no leaf") +} + +// TestPBinProcessRepeatedKeyKeepsOneLeaf checks a stem touched twice in one run +// still holds a single leaf, so the second visit updates rather than splits. +func TestPBinProcessRepeatedKeyKeepsOneLeaf(t *testing.T) { + t.Parallel() + + addr, slot := pbinOracleAddr(31), pbinOracleSlot(1000) + pph, ms := pbinTestEngine(t) + + first := new(pbinTestCorpus).storage(addr, slot, 0x01) + require.NoError(t, ms.applyPlainUpdates(first.plainKeys, first.updates)) + require.Equal(t, first.oracleRoot(t), pbinTestProcess(t, pph, first.plainKeys, first.updates)) + + second := new(pbinTestCorpus).storage(addr, slot, 0x02) + require.NoError(t, ms.applyPlainUpdates(second.plainKeys, second.updates)) + root := pbinTestProcess(t, pph, second.plainKeys, second.updates) + + require.Equal(t, pbinNodeLeaf, pph.grid.root.kind) + require.Equal(t, second.oracleRoot(t), root) +} + +// TestPBinProcessEmptyUpdatesKeepsEmptyRoot checks the drive loop over nothing: +// the root stays the empty-tree constant instead of picking up a shape. +func TestPBinProcessEmptyUpdatesKeepsEmptyRoot(t *testing.T) { + t.Parallel() + + pph, _ := pbinTestEngine(t) + root := pbinTestProcess(t, pph, nil, nil) + require.Equal(t, make([]byte, length.Hash), root) +} From 34191505739877098e80346187a78d7ba56daf79 Mon Sep 17 00:00:00 2001 From: awskii Date: Wed, 29 Jul 2026 18:56:22 +0700 Subject: [PATCH 11/56] feat: EIP-8297 variant registration and Trie interface methods --- docs/plans/20260729-pbin-patricia-hashed.md | 12 +- execution/commitment/commitment.go | 11 ++ execution/commitment/pbin_patricia_hashed.go | 48 +++++- execution/commitment/pbin_variant_test.go | 155 +++++++++++++++++++ 4 files changed, 220 insertions(+), 6 deletions(-) create mode 100644 execution/commitment/pbin_variant_test.go diff --git a/docs/plans/20260729-pbin-patricia-hashed.md b/docs/plans/20260729-pbin-patricia-hashed.md index f05125065b4..32849a11eda 100644 --- a/docs/plans/20260729-pbin-patricia-hashed.md +++ b/docs/plans/20260729-pbin-patricia-hashed.md @@ -245,11 +245,13 @@ func pbinCommonPrefixBits(a, b *pbinBitpath) int16 // XOR + LeadingZeros64, clam - Modify: `execution/commitment/commitment.go` - Create: `execution/commitment/pbin_variant_test.go` -- [ ] write `TestInitializeTrieAndUpdates_BinVariant` first as the red test, asserting the constructed type, `Variant()`, and `Mode() == ModeDirect` -- [ ] add `VariantBinPatriciaTrie` plus a case in `ParseTrieVariant`/`InitializeTrieAndUpdates` — **additive only** -- [ ] implement the remaining `Trie` methods to satisfy the interface unchanged: `Reset`, `ResetContext`, `Release`, `Variant`, `SetTraceWriter`, `EnableCsvMetrics` -- [ ] write a test asserting `Reset` then reuse produces the same root as a fresh engine -- [ ] run tests - must pass before task 11 +- [x] write `TestInitializeTrieAndUpdates_BinVariant` first as the red test, asserting the constructed type, `Variant()`, and `Mode() == ModeDirect` +- [x] add `VariantBinPatriciaTrie` plus a case in `ParseTrieVariant`/`InitializeTrieAndUpdates` — **additive only** +- [x] implement the remaining `Trie` methods to satisfy the interface unchanged: `Reset`, `ResetContext`, `Release`, `Variant`, `SetTraceWriter`, `EnableCsvMetrics` +- [x] write a test asserting `Reset` then reuse produces the same root as a fresh engine +- [x] run tests - must pass before task 11 + +**Registration notes.** `InitializeTrieAndUpdates` pins `ModeDirect` for this variant whatever mode the caller passes, mirroring how the parallel variant pins `ModeParallel`: `ModeParallel` allocates a hex-nibble prefix trie that has no meaning at arity 2. `SetTraceWriter` traces one line per run — the Task 8 counters — which is also how Task 12 reads them. `EnableCsvMetrics` is a no-op: M0 collects no metrics. `Release` pools the engine as the hex one does, since the grid is ~439 KB. ### Task 11: hazard guards and differential fuzzing diff --git a/execution/commitment/commitment.go b/execution/commitment/commitment.go index e878ce24564..28e1531c9bf 100644 --- a/execution/commitment/commitment.go +++ b/execution/commitment/commitment.go @@ -142,6 +142,9 @@ const ( VariantHexPatriciaTrie TrieVariant = "hex-patricia-hashed" VariantParallelHexPatricia TrieVariant = "hex-parallel-patricia-hashed" VariantStreamingHexPatricia TrieVariant = "hex-streaming-patricia-hashed" + // VariantBinPatriciaTrie is EIP-8297's binary tree. It is not wired to the + // domain layer: commitment state save/restore has no binary implementation. + VariantBinPatriciaTrie TrieVariant = "bin-patricia-hashed" ) // InitializeTrieAndUpdates constructs the trie + updates buffer from cfg. @@ -159,6 +162,12 @@ func InitializeTrieAndUpdates(mode Mode, tmpdir string, cfg TrieConfig) (Trie, * tree := NewUpdates(ModeParallel, tmpdir, KeyToHexNibbleHash) tree.SetStreamingCommitter(sc) return trie, tree + case VariantBinPatriciaTrie: + // ModeDirect regardless of the argument: the parallel prefix trie is a + // hex-nibble structure and the binary key space has no nibbles. + trie := NewPBinPatriciaHashed(nil) + tree := NewUpdates(ModeDirect, tmpdir, pbinKeyHasher()) + return trie, tree case VariantHexPatriciaTrie: fallthrough default: @@ -1205,6 +1214,8 @@ func ParseTrieVariant(s string) TrieVariant { switch s { case "parallel": trieVariant = VariantParallelHexPatricia + case "bin": + trieVariant = VariantBinPatriciaTrie case "hex": fallthrough default: diff --git a/execution/commitment/pbin_patricia_hashed.go b/execution/commitment/pbin_patricia_hashed.go index 523be50b38e..306f584cd73 100644 --- a/execution/commitment/pbin_patricia_hashed.go +++ b/execution/commitment/pbin_patricia_hashed.go @@ -21,7 +21,9 @@ import ( "context" "errors" "fmt" + "io" "math/bits" + "sync" "github.com/erigontech/erigon/common" "github.com/erigontech/erigon/common/length" @@ -41,6 +43,8 @@ type PBinPatriciaHashed struct { siblingKey [pbinAccountKeyLength]byte // scratch for the CODE_HASH key of the account being visited + traceW io.Writer // nil = disabled + rootChecked bool // whether the root record is known to be absent rootTouched bool rootPresent bool @@ -56,8 +60,46 @@ type pbinCounters struct { materializeReads uint64 } +// pbinPool recycles engines: the grid is the better part of a megabyte, and +// Release leaves a pooled engine in the state a fresh one starts in. +var pbinPool sync.Pool + func NewPBinPatriciaHashed(ctx PatriciaContext) *PBinPatriciaHashed { - return &PBinPatriciaHashed{ctx: ctx} + pph, ok := pbinPool.Get().(*PBinPatriciaHashed) + if !ok { + pph = &PBinPatriciaHashed{} + } + pph.ctx = ctx + return pph +} + +func (pph *PBinPatriciaHashed) Variant() TrieVariant { return VariantBinPatriciaTrie } + +func (pph *PBinPatriciaHashed) ResetContext(ctx PatriciaContext) { pph.ctx = ctx } + +// SetTraceWriter enables tracing. M0 traces one line per run: the counters the +// split-rehash decision is waiting on. +func (pph *PBinPatriciaHashed) SetTraceWriter(w io.Writer) { pph.traceW = w } + +// EnableCsvMetrics is a no-op: the binary engine collects no metrics in M0. +func (pph *PBinPatriciaHashed) EnableCsvMetrics(string) {} + +// Reset drops the tree, keeping the context. What survives is in the context, so +// the next run rebuilds whatever it descends into from stored records. +func (pph *PBinPatriciaHashed) Reset() { + pph.grid.resetForReuse() + pph.currentKey = pbinBitpath{} + pph.rootChecked, pph.rootTouched, pph.rootPresent = false, false, false +} + +// Release returns the engine to the pool. The caller must not use it afterwards. +func (pph *PBinPatriciaHashed) Release() { + pph.Reset() + pph.ctx = nil + pph.traceW = nil + pph.counters = pbinCounters{} + pph.branchEncoder.buf = pph.branchEncoder.buf[:0] + pbinPool.Put(pph) } var ( @@ -91,6 +133,10 @@ func (pph *PBinPatriciaHashed) Process(ctx context.Context, updates *Updates, lo if onProgress != nil { onProgress(&CommitProgress{KeyIndex: processed, UpdateCount: processed}) } + if pph.traceW != nil { + fmt.Fprintf(pph.traceW, "pbin: keys=%d splitsInsidePrefix=%d materializeReads=%d\n", + processed, pph.counters.splitsInsidePrefix, pph.counters.materializeReads) + } return pph.RootHash() } diff --git a/execution/commitment/pbin_variant_test.go b/execution/commitment/pbin_variant_test.go new file mode 100644 index 00000000000..6038ff48b67 --- /dev/null +++ b/execution/commitment/pbin_variant_test.go @@ -0,0 +1,155 @@ +// Copyright 2026 The Erigon Authors +// This file is part of Erigon. +// +// Erigon is free software: you can redistribute it and/or modify +// it under the terms of the GNU Lesser General Public License as published by +// the Free Software Foundation, either version 3 of the License, or +// (at your option) any later version. +// +// Erigon is distributed in the hope that it will be useful, +// but WITHOUT ANY WARRANTY; without even the implied warranty of +// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +// GNU Lesser General Public License for more details. +// +// You should have received a copy of the GNU Lesser General Public License +// along with Erigon. If not, see . + +package commitment + +import ( + "bytes" + "testing" + + "github.com/stretchr/testify/require" + + "github.com/erigontech/erigon/common" +) + +// TestInitializeTrieAndUpdates_BinVariant pins the registration. M0 runs the +// binary engine in ModeDirect whatever mode the caller asks for: ModeParallel's +// prefix trie is a hex-nibble structure with no meaning at arity 2. +func TestInitializeTrieAndUpdates_BinVariant(t *testing.T) { + t.Parallel() + + cfg := DefaultTrieConfig() + cfg.Variant = VariantBinPatriciaTrie + trie, upd := InitializeTrieAndUpdates(ModeParallel, t.TempDir(), cfg) + defer upd.Close() + defer trie.Release() + + require.IsType(t, (*PBinPatriciaHashed)(nil), trie) + require.Equal(t, VariantBinPatriciaTrie, trie.Variant()) + require.Equal(t, ModeDirect, upd.Mode()) + require.Nil(t, upd.parallel) + require.False(t, upd.IsConcurrentCommitment()) +} + +func TestParseTrieVariantBin(t *testing.T) { + t.Parallel() + + require.Equal(t, VariantBinPatriciaTrie, ParseTrieVariant("bin")) + require.Equal(t, VariantHexPatriciaTrie, ParseTrieVariant("hex")) + require.Equal(t, VariantParallelHexPatricia, ParseTrieVariant("parallel")) +} + +// TestPBinResetReuse checks that a run over a populated state depends only on +// what the context holds: an engine that dropped its in-memory root, and one +// that never had it, must both reproduce the root of the run that built it. +func TestPBinResetReuse(t *testing.T) { + t.Parallel() + + corpus := pbinTestMixedCorpus() + pph, ms := pbinTestEngine(t) + require.NoError(t, ms.applyPlainUpdates(corpus.plainKeys, corpus.updates)) + + want := corpus.oracleRoot(t) + require.Equal(t, want, pbinTestProcess(t, pph, corpus.plainKeys, corpus.updates)) + + pph.Reset() + require.Equal(t, want, pbinTestProcess(t, pph, corpus.plainKeys, corpus.updates), "reset engine re-reads the tree from the context") + + fresh := NewPBinPatriciaHashed(ms) + require.Equal(t, want, pbinTestProcess(t, fresh, corpus.plainKeys, corpus.updates), "fresh engine over the same state agrees") +} + +// TestPBinResetClearsTrieState is the state-level half of the reuse contract: +// Reset leaves the engine indistinguishable from a new one but keeps the +// context, which the Trie interface hands over separately. +func TestPBinResetClearsTrieState(t *testing.T) { + t.Parallel() + + corpus := new(pbinTestCorpus). + storage(pbinOracleAddr(1), pbinOracleSlot(256), 0x01). + storage(pbinOracleAddr(1), pbinOracleSlot(257), 0x02) + + pph, ms := pbinTestEngine(t) + require.NoError(t, ms.applyPlainUpdates(corpus.plainKeys, corpus.updates)) + pbinTestProcess(t, pph, corpus.plainKeys, corpus.updates) + require.Equal(t, pbinNodeBranch, pph.grid.root.kind) + + pph.Reset() + require.Equal(t, pbinNodeEmpty, pph.grid.root.kind) + require.Zero(t, pph.currentKey.bitLen) + require.Zero(t, pph.grid.activeRows) + require.False(t, pph.rootChecked) + require.False(t, pph.rootTouched) + require.False(t, pph.rootPresent) + require.Same(t, ms, pph.ctx) + + root, err := pph.RootHash() + require.NoError(t, err) + require.Equal(t, make([]byte, 32), root) +} + +// TestPBinResetContext swaps the state under a released-and-reused engine. +func TestPBinResetContext(t *testing.T) { + t.Parallel() + + corpus := new(pbinTestCorpus).account(pbinOracleAddr(7), 1, 2, common.Hash{0x07}) + + pph, _ := pbinTestEngine(t) + other := NewMockState(t) + require.NoError(t, other.applyPlainUpdates(corpus.plainKeys, corpus.updates)) + + pph.ResetContext(other) + require.Same(t, other, pph.ctx) + require.Equal(t, corpus.oracleRoot(t), pbinTestProcess(t, pph, corpus.plainKeys, corpus.updates)) +} + +// TestPBinReleaseReuse guards the pool: a released engine carries no state into +// its next life, so the next run over a different context matches a fresh one. +func TestPBinReleaseReuse(t *testing.T) { + t.Parallel() + + corpus := pbinTestMixedCorpus() + pph, ms := pbinTestEngine(t) + require.NoError(t, ms.applyPlainUpdates(corpus.plainKeys, corpus.updates)) + pbinTestProcess(t, pph, corpus.plainKeys, corpus.updates) + pph.Release() + + next := NewMockState(t) + require.NoError(t, next.applyPlainUpdates(corpus.plainKeys, corpus.updates)) + reused := NewPBinPatriciaHashed(next) + require.Equal(t, corpus.oracleRoot(t), pbinTestProcess(t, reused, corpus.plainKeys, corpus.updates)) +} + +// TestPBinSetTraceWriter pins what the engine traces: the two counters the +// split-rehash decision is waiting on. +func TestPBinSetTraceWriter(t *testing.T) { + t.Parallel() + + corpus := pbinTestDeepSharedPrefixCorpus() + pph, ms := pbinTestEngine(t) + require.NoError(t, ms.applyPlainUpdates(corpus.plainKeys, corpus.updates)) + + var trace bytes.Buffer + pph.SetTraceWriter(&trace) + pbinTestProcess(t, pph, corpus.plainKeys, corpus.updates) + require.Contains(t, trace.String(), "splitsInsidePrefix=") + require.Contains(t, trace.String(), "materializeReads=") + + trace.Reset() + pph.SetTraceWriter(nil) + pbinTestProcess(t, pph, corpus.plainKeys, corpus.updates) + require.Empty(t, trace.String()) +} From b2c4062dce1574a4408e71442a7cc734385410ea Mon Sep 17 00:00:00 2001 From: awskii Date: Wed, 29 Jul 2026 19:10:07 +0700 Subject: [PATCH 12/56] feat: EIP-8297 hazard guards and differential fuzzing --- docs/plans/20260729-pbin-patricia-hashed.md | 16 +- execution/commitment/pbin_fuzz_test.go | 111 ++++++ execution/commitment/pbin_hazard_test.go | 338 ++++++++++++++++ execution/commitment/pbin_verify_test.go | 403 ++++++++++++++++++++ 4 files changed, 860 insertions(+), 8 deletions(-) create mode 100644 execution/commitment/pbin_fuzz_test.go create mode 100644 execution/commitment/pbin_hazard_test.go create mode 100644 execution/commitment/pbin_verify_test.go diff --git a/docs/plans/20260729-pbin-patricia-hashed.md b/docs/plans/20260729-pbin-patricia-hashed.md index 32849a11eda..2362dd16aed 100644 --- a/docs/plans/20260729-pbin-patricia-hashed.md +++ b/docs/plans/20260729-pbin-patricia-hashed.md @@ -260,13 +260,13 @@ func pbinCommonPrefixBits(a, b *pbinBitpath) int16 // XOR + LeadingZeros64, clam - Create: `execution/commitment/pbin_hazard_test.go` - Create: `execution/commitment/pbin_fuzz_test.go` -- [ ] implement an independent branch-record recompute oracle: walk every written record, decode, recompute bottom-up, assert it reproduces the root -- [ ] implement a bit-space plain-key validator asserting `treeKey(plainKey) == branchPath || cellPrefix` for every written record (guards H8) -- [ ] write the two-phase sibling test: `Process` batch A writing both children, then batch B touching one child, asserting the root equals the oracle over A∪B (guards H2) -- [ ] write the mined deep-shared-prefix corpus test and assert oracle equality (guards H1) -- [ ] write permutation-independence tests porting `Test_HexPatriciaHashed_UniqueRepresentation`/`2`/`BrokenUniqueRepr` (`hex_patricia_hashed_test.go:157-249`) -- [ ] write a differential fuzzer over `Process` against the oracle with a **low-entropy slot generator** — random 32-byte slots essentially never share a stem, so a default corpus never exercises sub-index sharing -- [ ] run tests - must pass before task 12 +- [x] implement an independent branch-record recompute oracle: walk every written record, decode, recompute bottom-up, assert it reproduces the root — landed as `pbinVerifier`, which finds the root record as the one no other record is a bit-prefix of +- [x] implement a bit-space plain-key validator asserting `treeKey(plainKey) == branchPath || cellPrefix` for every written record (guards H8) +- [x] write the two-phase sibling test: `Process` batch A writing both children, then batch B touching one child, asserting the root equals the oracle over A∪B (guards H2) +- [x] write the mined deep-shared-prefix corpus test and assert oracle equality (guards H1) +- [x] write permutation-independence tests porting `Test_HexPatriciaHashed_UniqueRepresentation`/`2`/`BrokenUniqueRepr` (`hex_patricia_hashed_test.go:157-249`) +- [x] write a differential fuzzer over `Process` against the oracle with a **low-entropy slot generator** — random 32-byte slots essentially never share a stem, so a default corpus never exercises sub-index sharing +- [x] run tests - must pass before task 12 ### Task 12: Verify acceptance criteria @@ -275,7 +275,7 @@ func pbinCommonPrefixBits(a, b *pbinBitpath) int16 // XOR + LeadingZeros64, clam - [ ] verify every hazard in the register except H6 has a named passing test - [ ] verify every new package-level identifier carries the `pbin` prefix and the package compiles without collision - [ ] run the package test suite: `go test ./execution/commitment/...` -- [ ] run fuzzers briefly: `go test ./execution/commitment/ -run=Fuzz -fuzz=FuzzPBin -fuzztime=60s` +- [ ] run fuzzers briefly, one target per invocation — `-fuzz` refuses a regex matching several: `go test ./execution/commitment/ -run=Fuzz -fuzz=FuzzPBinBitPathCodec -fuzztime=60s` then the same for `FuzzPBinProcessMatchesOracle` - [ ] verify `go build ./...` and `go vet ./execution/commitment/...` are clean - [ ] record the Task 8 instrumentation counters under Post-Completion diff --git a/execution/commitment/pbin_fuzz_test.go b/execution/commitment/pbin_fuzz_test.go new file mode 100644 index 00000000000..951b43e67e2 --- /dev/null +++ b/execution/commitment/pbin_fuzz_test.go @@ -0,0 +1,111 @@ +// Copyright 2026 The Erigon Authors +// This file is part of Erigon. +// +// Erigon is free software: you can redistribute it and/or modify +// it under the terms of the GNU Lesser General Public License as published by +// the Free Software Foundation, either version 3 of the License, or +// (at your option) any later version. +// +// Erigon is distributed in the hope that it will be useful, +// but WITHOUT ANY WARRANTY; without even the implied warranty of +// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +// GNU Lesser General Public License for more details. +// +// You should have received a copy of the GNU Lesser General Public License +// along with Erigon. If not, see . + +package commitment + +import ( + "testing" + + "github.com/stretchr/testify/require" + + "github.com/erigontech/erigon/common" + "github.com/erigontech/erigon/common/length" +) + +// pbinFuzzSlots is the slot pool the generator draws from. Entropy is the enemy +// here: 32-byte slots picked at random essentially never share a stem, so a +// fuzzer free to choose them would only ever build shallow trees and would never +// reach sub-index sharing, group boundaries or the account/storage zone split. +var pbinFuzzSlots = []uint64{0, 1, 2, 63, 64, 65, 66, 127, 128, 255, 256, 257, 258, 511, 512, 1000, 1 << 20, 1<<20 + 1} + +// pbinFuzzAccountBit is the selector bit choosing an account write over a slot. +const pbinFuzzAccountBit = 0x04 + +// pbinFuzzCorpus reads the input three bytes at a time — what to write, where, +// and with what value — drawing addresses and slots from small pools so keys +// collide by construction. +func pbinFuzzCorpus(data []byte) *pbinTestCorpus { + c := new(pbinTestCorpus) + for i := 0; i+2 < len(data); i += 3 { + where, slot, value := data[i], data[i+1], data[i+2] + addr := pbinOracleAddr(uint64(where & 0x03)) + if where&pbinFuzzAccountBit != 0 { + c.account(addr, uint64(value), uint64(value)*1_000_000_007, common.Hash{value, 0xC0}) + continue + } + c.storage(addr, pbinOracleSlot(pbinFuzzSlots[int(slot)%len(pbinFuzzSlots)]), value, value^0xFF) + } + return c +} + +// pbinFuzzBatches cuts the corpus in two, so a run also covers what one Process +// call leaves for the next to read back. +func pbinFuzzBatches(data []byte, cut byte) []*pbinTestCorpus { + c := pbinFuzzCorpus(data) + if len(c.plainKeys) == 0 { + return nil + } + at := int(cut) % (len(c.plainKeys) + 1) + batches := make([]*pbinTestCorpus, 0, 2) + for _, b := range []*pbinTestCorpus{ + {plainKeys: c.plainKeys[:at], updates: c.updates[:at]}, + {plainKeys: c.plainKeys[at:], updates: c.updates[at:]}, + } { + if len(b.plainKeys) > 0 { + batches = append(batches, b) + } + } + return batches +} + +// FuzzPBinProcessMatchesOracle is the differential gate: whatever the generator +// produces, the engine's root must equal the reference tree's over the same +// leaves, and the records it left behind must rebuild that root on their own. +// +// go test ./execution/commitment/ -run=Fuzz -fuzz=FuzzPBinProcessMatchesOracle -fuzztime=60s +func FuzzPBinProcessMatchesOracle(f *testing.F) { + // Seeds spell the generator's (selector, slot, value) triples: bit 2 of the + // selector asks for an account, its low bits pick the address, and the slot + // byte indexes the pool. + f.Add([]byte{0x04, 0, 1, 0x05, 0, 2}, byte(0)) // two accounts + f.Add([]byte{0x00, 10, 1, 0x00, 11, 2, 0x00, 12, 3}, byte(2)) // three slots of one group + f.Add([]byte{0x00, 3, 1, 0x00, 4, 2, 0x04, 0, 3}, byte(1)) // the 63/64 zone boundary plus a header + f.Add([]byte{0x04, 0, 1, 0x00, 15, 2, 0x01, 15, 3, 0x02, 16, 4}, byte(3)) // one slot per address + f.Add([]byte{0x00, 10, 1, 0x00, 10, 2, 0x00, 10, 3}, byte(1)) // the same slot rewritten + + f.Fuzz(func(t *testing.T, data []byte, cut byte) { + batches := pbinFuzzBatches(data, cut) + if len(batches) == 0 { + return + } + + pph, ms := pbinTestEngine(t) + var root []byte + for _, b := range batches { + require.NoError(t, ms.applyPlainUpdates(b.plainKeys, b.updates)) + root = pbinTestProcess(t, pph, b.plainKeys, b.updates) + } + require.Len(t, root, length.Hash) + + union := pbinTestUnion(batches...) + require.Equal(t, union.oracleRoot(t), root) + + // A tree of one leaf is that leaf and writes no record. + if leaves := union.leafCount(t); leaves > 1 { + pbinTestVerifyRecords(t, ms, root, leaves) + } + }) +} diff --git a/execution/commitment/pbin_hazard_test.go b/execution/commitment/pbin_hazard_test.go new file mode 100644 index 00000000000..236bdcf8f59 --- /dev/null +++ b/execution/commitment/pbin_hazard_test.go @@ -0,0 +1,338 @@ +// Copyright 2026 The Erigon Authors +// This file is part of Erigon. +// +// Erigon is free software: you can redistribute it and/or modify +// it under the terms of the GNU Lesser General Public License as published by +// the Free Software Foundation, either version 3 of the License, or +// (at your option) any later version. +// +// Erigon is distributed in the hope that it will be useful, +// but WITHOUT ANY WARRANTY; without even the implied warranty of +// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +// GNU Lesser General Public License for more details. +// +// You should have received a copy of the GNU Lesser General Public License +// along with Erigon. If not, see . + +package commitment + +import ( + "bytes" + "math/rand" + "slices" + "testing" + + "github.com/stretchr/testify/require" + + "github.com/erigontech/erigon/common" +) + +// pbinTestBatches runs each corpus through one engine and one state in order, +// the way consecutive blocks reach the trie, and returns the root after the last. +func pbinTestBatches(t *testing.T, batches ...*pbinTestCorpus) (*PBinPatriciaHashed, *MockState, []byte) { + t.Helper() + pph, ms := pbinTestEngine(t) + var root []byte + for _, b := range batches { + require.NoError(t, ms.applyPlainUpdates(b.plainKeys, b.updates)) + root = bytes.Clone(pbinTestProcess(t, pph, b.plainKeys, b.updates)) + } + return pph, ms, root +} + +// pbinTestUnion is the leaf set the batches leave behind. A key touched twice +// keeps its last value, which is what the oracle's duplicate-key insert does and +// what MockState's update merge does. +func pbinTestUnion(batches ...*pbinTestCorpus) *pbinTestCorpus { + u := new(pbinTestCorpus) + for _, b := range batches { + u.plainKeys = append(u.plainKeys, b.plainKeys...) + u.updates = append(u.updates, b.updates...) + } + return u +} + +// leafCount is how many leaves the corpus stands for once repeated keys collapse. +func (c *pbinTestCorpus) leafCount(t *testing.T) int { + t.Helper() + seen := make(map[string]struct{}) + for _, e := range c.entries(t) { + seen[string(e.key)] = struct{}{} + } + return len(seen) +} + +func (c *pbinTestCorpus) permute(order []int) *pbinTestCorpus { + out := new(pbinTestCorpus) + for _, i := range order { + out.plainKeys = append(out.plainKeys, c.plainKeys[i]) + out.updates = append(out.updates, c.updates[i]) + } + return out +} + +// TestPBinUntouchedSiblingSurvivesBatch guards H2. At arity 2 a cell's sibling is +// the whole other half of the subtree, so a batch that rewrites a node from the +// touched child alone loses everything under the other one. +func TestPBinUntouchedSiblingSurvivesBatch(t *testing.T) { + t.Parallel() + + for _, tc := range []struct { + name string + batchA, batchB *pbinTestCorpus + }{ + { + name: "sibling slot in the same storage group", + batchA: new(pbinTestCorpus). + storage(pbinOracleAddr(41), pbinOracleSlot(256), 0x01). + storage(pbinOracleAddr(41), pbinOracleSlot(257), 0x02), + batchB: new(pbinTestCorpus). + storage(pbinOracleAddr(41), pbinOracleSlot(257), 0x03), + }, + { + name: "sibling under another account", + batchA: new(pbinTestCorpus). + account(pbinOracleAddr(42), 1, 10, common.Hash{0x01}). + account(pbinOracleAddr(43), 2, 20, common.Hash{0x02}), + batchB: new(pbinTestCorpus). + account(pbinOracleAddr(43), 3, 30, common.Hash{0x03}), + }, + { + name: "a third key joins a shared branch", + batchA: new(pbinTestCorpus). + storage(pbinOracleAddr(44), pbinOracleSlot(256), 0x01). + storage(pbinOracleAddr(44), pbinOracleSlot(257), 0x02), + batchB: new(pbinTestCorpus). + storage(pbinOracleAddr(44), pbinOracleSlot(258), 0x03), + }, + { + name: "one header slot of an account spanning both zones", + batchA: new(pbinTestCorpus). + account(pbinOracleAddr(45), 1, 10, common.Hash{0x01}). + storage(pbinOracleAddr(45), pbinOracleSlot(0), 0x01). + storage(pbinOracleAddr(45), pbinOracleSlot(63), 0x02). + storage(pbinOracleAddr(45), pbinOracleSlot(64), 0x03). + storage(pbinOracleAddr(45), pbinOracleSlot(1000), 0x04), + batchB: new(pbinTestCorpus). + storage(pbinOracleAddr(45), pbinOracleSlot(63), 0x09), + }, + { + name: "one of a deep-shared-prefix cluster", + batchA: pbinTestDeepSharedPrefixCorpus(), + batchB: new(pbinTestCorpus). + account(pbinOracleMinedAddrs()[1], 99, 999, common.Hash{0x99}), + }, + } { + t.Run(tc.name, func(t *testing.T) { + t.Parallel() + _, ms, root := pbinTestBatches(t, tc.batchA, tc.batchB) + + union := pbinTestUnion(tc.batchA, tc.batchB) + require.Equal(t, union.oracleRoot(t), root) + pbinTestVerifyRecords(t, ms, root, union.leafCount(t)) + }) + } +} + +// TestPBinSplitInsideStoredPrefix guards H1. A probe diverging inside a stored +// branch's prefix shortens that prefix, and the prefix is inside the node's hash, +// so a hash carried over from the record is stale. The counters are what pin that +// this run actually took that path rather than passing by luck. +func TestPBinSplitInsideStoredPrefix(t *testing.T) { + t.Parallel() + + addr := pbinOracleAddr(61) + batchA := new(pbinTestCorpus). + account(addr, 1, 2, common.Hash{0x01}). + storage(addr, pbinOracleSlot(256), 0x01). + storage(addr, pbinOracleSlot(257), 0x02) + // Sub-indices 0, 1 and 2 differ only in their last two bits, so the third slot + // leaves the stored branch's prefix one bit before its end. + batchB := new(pbinTestCorpus).storage(addr, pbinOracleSlot(258), 0x03) + + pph, ms := pbinTestEngine(t) + require.NoError(t, ms.applyPlainUpdates(batchA.plainKeys, batchA.updates)) + pbinTestProcess(t, pph, batchA.plainKeys, batchA.updates) + afterA := pph.counters + + require.NoError(t, ms.applyPlainUpdates(batchB.plainKeys, batchB.updates)) + root := pbinTestProcess(t, pph, batchB.plainKeys, batchB.updates) + + require.Greater(t, pph.counters.splitsInsidePrefix, afterA.splitsInsidePrefix) + require.Greater(t, pph.counters.materializeReads, afterA.materializeReads, + "a branch cell read back from a record must rehash under its shortened prefix") + + union := pbinTestUnion(batchA, batchB) + require.Equal(t, union.oracleRoot(t), root) + pbinTestVerifyRecords(t, ms, root, union.leafCount(t)) +} + +// TestPBinDeepSharedPrefixCorpus is H1's other half: a mined cluster whose keys +// agree far past the root, so the splits happen deep instead of at the first +// bits, spread over batches so the survivors come back from records. +func TestPBinDeepSharedPrefixCorpus(t *testing.T) { + t.Parallel() + + addrs := pbinOracleMinedAddrs() + require.GreaterOrEqual(t, len(addrs), 4) + + batches := make([]*pbinTestCorpus, 0, len(addrs)) + for i, addr := range addrs { + batches = append(batches, new(pbinTestCorpus). + account(addr, uint64(i), uint64(i)*7, common.Hash{byte(i)})) + } + + pph, ms, root := pbinTestBatches(t, batches...) + require.Positive(t, pph.counters.splitsInsidePrefix) + + union := pbinTestUnion(batches...) + require.Equal(t, union.oracleRoot(t), root) + pbinTestVerifyRecords(t, ms, root, union.leafCount(t)) +} + +// pbinTestOrderings returns the corpus in every arrival order worth trying: as +// written, reversed, both tree-key directions, and one shuffle. +func pbinTestOrderings(t *testing.T, c *pbinTestCorpus) map[string]*pbinTestCorpus { + t.Helper() + + hasher := pbinKeyHasher() + treeKeys := make([][]byte, len(c.plainKeys)) + order := make([]int, len(c.plainKeys)) + for i, plainKey := range c.plainKeys { + treeKeys[i] = hasher(plainKey) + order[i] = i + } + + ascending := slices.Clone(order) + slices.SortFunc(ascending, func(a, b int) int { return bytes.Compare(treeKeys[a], treeKeys[b]) }) + descending := slices.Clone(ascending) + slices.Reverse(descending) + reversed := slices.Clone(order) + slices.Reverse(reversed) + + shuffled := slices.Clone(order) + rnd := rand.New(rand.NewSource(0x8297)) + rnd.Shuffle(len(shuffled), func(i, j int) { shuffled[i], shuffled[j] = shuffled[j], shuffled[i] }) + + return map[string]*pbinTestCorpus{ + "as given": c, + "reversed": c.permute(reversed), + "tree key ascending": c.permute(ascending), + "tree key descending": c.permute(descending), + "shuffled": c.permute(shuffled), + } +} + +// pbinTestProcessSeq feeds the corpus one key per Process call, the way +// per-block processing arrives, and returns the root after the last key. +func pbinTestProcessSeq(t *testing.T, c *pbinTestCorpus) (*MockState, []byte) { + t.Helper() + pph, ms := pbinTestEngine(t) + var root []byte + for i := range c.plainKeys { + require.NoError(t, ms.applyPlainUpdates(c.plainKeys[i:i+1], c.updates[i:i+1])) + root = bytes.Clone(pbinTestProcess(t, pph, c.plainKeys[i:i+1], c.updates[i:i+1])) + } + return ms, root +} + +func pbinTestUniqueReprCorpora() []struct { + name string + corpus *pbinTestCorpus +} { + return []struct { + name string + corpus *pbinTestCorpus + }{ + { + name: "accounts", + corpus: new(pbinTestCorpus). + account(pbinOracleAddr(71), 1, 999860099, common.Hash{0x01}). + account(pbinOracleAddr(72), 3, 900234, common.Hash{0x02}). + account(pbinOracleAddr(73), 0, 0, common.Hash{}). + account(pbinOracleAddr(74), 7, 2000000000000138901, common.Hash{0x04}), + }, + { + name: "storage across both zones", + corpus: new(pbinTestCorpus). + storage(pbinOracleAddr(75), pbinOracleSlot(0), 0x01). + storage(pbinOracleAddr(75), pbinOracleSlot(63), 0x02). + storage(pbinOracleAddr(75), pbinOracleSlot(64), 0x03). + storage(pbinOracleAddr(75), pbinOracleSlot(256), 0x04). + storage(pbinOracleAddr(76), pbinOracleSlot(256), 0x05). + storage(pbinOracleAddr(76), pbinOracleSlot(257), 0x06), + }, + {name: "mixed accounts and storage", corpus: pbinTestMixedCorpus()}, + {name: "deep shared prefix", corpus: pbinTestDeepSharedPrefixCorpus()}, + } +} + +// TestPBinUniqueRepresentation ports Test_HexPatriciaHashed_UniqueRepresentation +// and its variants: the root follows the state the keys leave behind, not the +// order they arrive in nor how many Process calls they are split across. +func TestPBinUniqueRepresentation(t *testing.T) { + t.Parallel() + + for _, tc := range pbinTestUniqueReprCorpora() { + t.Run(tc.name, func(t *testing.T) { + t.Parallel() + want := tc.corpus.oracleRoot(t) + leaves := tc.corpus.leafCount(t) + + for name, ordered := range pbinTestOrderings(t, tc.corpus) { + _, batchState, batchRoot := pbinTestBatches(t, ordered) + require.Equal(t, want, batchRoot, "batch, ordering %s", name) + pbinTestVerifyRecords(t, batchState, batchRoot, leaves) + + seqState, seqRoot := pbinTestProcessSeq(t, ordered) + require.Equal(t, want, seqRoot, "sequential, ordering %s", name) + pbinTestVerifyRecords(t, seqState, seqRoot, leaves) + } + }) + } +} + +// TestPBinUniqueRepresentationAcrossRounds ports +// Test_HexPatriciaHashed_UniqueRepresentation2: a second round of updates lands +// on trees built two different ways, and both must still agree. +func TestPBinUniqueRepresentationAcrossRounds(t *testing.T) { + t.Parallel() + + addrs := [][]byte{pbinOracleAddr(81), pbinOracleAddr(82), pbinOracleAddr(83)} + round1 := new(pbinTestCorpus). + account(addrs[0], 1, 999860099, common.Hash{0x01}). + account(addrs[1], 3, 900234, common.Hash{0x02}). + storage(addrs[1], pbinOracleSlot(64), 0x01). + account(addrs[2], 0, 2000000000000138901, common.Hash{0x03}) + round2 := new(pbinTestCorpus). + account(addrs[0], 2, 2345234560099, common.Hash{0x11}). + storage(addrs[1], pbinOracleSlot(64), 0x02). + storage(addrs[1], pbinOracleSlot(1000), 0x03) + + pphBatch, batchState := pbinTestEngine(t) + pphSeq, seqState := pbinTestEngine(t) + + batchRoot := func(c *pbinTestCorpus) []byte { + require.NoError(t, batchState.applyPlainUpdates(c.plainKeys, c.updates)) + return bytes.Clone(pbinTestProcess(t, pphBatch, c.plainKeys, c.updates)) + } + seqRoot := func(c *pbinTestCorpus) []byte { + var root []byte + for i := range c.plainKeys { + require.NoError(t, seqState.applyPlainUpdates(c.plainKeys[i:i+1], c.updates[i:i+1])) + root = bytes.Clone(pbinTestProcess(t, pphSeq, c.plainKeys[i:i+1], c.updates[i:i+1])) + } + return root + } + + require.Equal(t, round1.oracleRoot(t), batchRoot(round1)) + require.Equal(t, round1.oracleRoot(t), seqRoot(round1)) + + union := pbinTestUnion(round1, round2) + root := batchRoot(round2) + require.Equal(t, union.oracleRoot(t), root) + require.Equal(t, root, seqRoot(round2)) + + pbinTestVerifyRecords(t, batchState, root, union.leafCount(t)) + pbinTestVerifyRecords(t, seqState, root, union.leafCount(t)) +} diff --git a/execution/commitment/pbin_verify_test.go b/execution/commitment/pbin_verify_test.go new file mode 100644 index 00000000000..4276054a239 --- /dev/null +++ b/execution/commitment/pbin_verify_test.go @@ -0,0 +1,403 @@ +// Copyright 2026 The Erigon Authors +// This file is part of Erigon. +// +// Erigon is free software: you can redistribute it and/or modify +// it under the terms of the GNU Lesser General Public License as published by +// the Free Software Foundation, either version 3 of the License, or +// (at your option) any later version. +// +// Erigon is distributed in the hope that it will be useful, +// but WITHOUT ANY WARRANTY; without even the implied warranty of +// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +// GNU Lesser General Public License for more details. +// +// You should have received a copy of the GNU Lesser General Public License +// along with Erigon. If not, see . + +package commitment + +import ( + "bytes" + "errors" + "fmt" + "testing" + + "github.com/stretchr/testify/require" + + "github.com/erigontech/erigon/common" + "github.com/erigontech/erigon/common/length" +) + +// pbinVerifier rebuilds the tree from the records the engine wrote, reading +// nothing out of the engine's own cells. The Task 4 oracle answers "is this the +// right root for these leaves"; this answers the other half — "is what landed in +// the database the tree that root came from". It walks records top down, resolves +// every child, and hashes back up with the independent Keccak the tests use. +// +// It reports errors rather than failing the test directly, so a test can also +// pin that a corrupted record is caught. +type pbinVerifier struct { + t *testing.T + ms *MockState +} + +var ( + errPBinVerifyNoRecords = errors.New("pbin verify: no branch records") + errPBinVerifyPosition = errors.New("pbin verify: leaf sits where its key does not") +) + +// recordPaths decodes the key of every live record. A record put with no data is +// a deletion and names no node. +func (v *pbinVerifier) recordPaths() ([]pbinBitpath, error) { + paths := make([]pbinBitpath, 0, len(v.ms.cm)) + for key, data := range v.ms.cm { + if len(data) == 0 { + continue + } + p, err := pbinDecodeBitPath([]byte(key)) + if err != nil { + return nil, fmt.Errorf("pbin verify: record key %x: %w", key, err) + } + paths = append(paths, p) + } + return paths, nil +} + +// rootPath is the record with no record above it. Records are keyed by the full +// descent path, so one record's path being a bit-prefix of another's is exactly +// the ancestor relation. +func (v *pbinVerifier) rootPath() (pbinBitpath, error) { + paths, err := v.recordPaths() + if err != nil { + return pbinBitpath{}, err + } + if len(paths) == 0 { + return pbinBitpath{}, errPBinVerifyNoRecords + } + var roots []pbinBitpath + for _, p := range paths { + covered := false + for _, q := range paths { + if q.bitLen < p.bitLen && p.hasPrefix(&q) { + covered = true + break + } + } + if !covered { + roots = append(roots, p) + } + } + if len(roots) != 1 { + return pbinBitpath{}, fmt.Errorf("pbin verify: %d of %d records have no ancestor, want 1", len(roots), len(paths)) + } + return roots[0], nil +} + +// recomputeRoot hashes the whole record set bottom up. The root node starts at +// depth 0, so its prefix is its entire path. +func (v *pbinVerifier) recomputeRoot() ([]byte, error) { + root, err := v.rootPath() + if err != nil { + return nil, err + } + return v.nodeHash(&root, &root) +} + +func (v *pbinVerifier) nodeHash(nodePath, prefix *pbinBitpath) ([]byte, error) { + cells, err := v.recordAt(nodePath) + if err != nil { + return nil, err + } + var children [2][]byte + for bit := range children { + start := *nodePath + start.appendBit(uint64(bit)) + if children[bit], err = v.cellHash(&start, &cells[bit]); err != nil { + return nil, err + } + } + return pbinTestKeccak(v.t, []byte{pbinBranchTag}, + pbinOracleEncodeBitPrefix(pbinVerifyBits(prefix)), children[0], children[1]), nil +} + +func (v *pbinVerifier) cellHash(start *pbinBitpath, c *pbinCell) ([]byte, error) { + switch c.kind { + case pbinNodeLeaf: + key, value, err := v.leaf(start, c) + if err != nil { + return nil, err + } + return pbinTestKeccak(v.t, []byte{pbinLeafTag}, key, value), nil + case pbinNodeBranch: + nodePath := *start + nodePath.append(&c.prefix) + return v.nodeHash(&nodePath, &c.prefix) + default: + return nil, fmt.Errorf("pbin verify: cell at %d bits has no node kind", start.bitLen) + } +} + +// leaf resolves a leaf cell to the key its position spells and the value its +// plain key holds in state. +func (v *pbinVerifier) leaf(start *pbinBitpath, c *pbinCell) (key, value []byte, err error) { + full := *start + full.append(&c.prefix) + if full.bitLen != pbinAccountKeyLength*8 && full.bitLen != pbinStorageKeyLength*8 { + return nil, nil, fmt.Errorf("pbin verify: leaf key of %d bits is neither zone length", full.bitLen) + } + key = pbinVerifyPackBits(pbinVerifyBits(&full)) + + update, err := v.plainState(c) + if err != nil { + return nil, nil, err + } + encoded, err := pbinLeafValue(key, update) + if err != nil { + return nil, nil, err + } + return key, encoded[:], nil +} + +func (v *pbinVerifier) plainState(c *pbinCell) (*Update, error) { + switch { + case c.accountAddrLen > 0 && c.storageAddrLen > 0: + return nil, errors.New("pbin verify: leaf carries both an account and a storage plain key") + case c.accountAddrLen > 0: + return v.ms.Account(c.accountAddr[:c.accountAddrLen]) + case c.storageAddrLen > 0: + return v.ms.Storage(c.storageAddr[:c.storageAddrLen]) + default: + return nil, errors.New("pbin verify: leaf carries no plain key") + } +} + +func (v *pbinVerifier) recordAt(nodePath *pbinBitpath) ([2]pbinCell, error) { + var cells [2]pbinCell + key := pbinEncodeBitPath(nodePath) + data, _, err := v.ms.Branch(key) + if err != nil { + return cells, err + } + if len(data) == 0 { + return cells, fmt.Errorf("pbin verify: no record for the %d-bit node at %x", nodePath.bitLen, key) + } + _, afterMap, err := pbinDecodeBranch(data, &cells) + if err != nil { + return cells, fmt.Errorf("pbin verify: record at %x: %w", key, err) + } + if afterMap != pbinCellBits { + return cells, fmt.Errorf("pbin verify: record at %x keeps %02b of its children, want both", key, afterMap) + } + return cells, nil +} + +// checkPlainKeys asserts every stored leaf sits where its own key derivation puts +// it: the record's path, the child bit and the cell's prefix must spell exactly +// treeKey(plainKey). A slot routed into the wrong zone still builds a tree that +// hashes consistently, so position against derivation is what catches it (H8). +func (v *pbinVerifier) checkPlainKeys() (int, error) { + paths, err := v.recordPaths() + if err != nil { + return 0, err + } + leaves := 0 + for _, path := range paths { + cells, err := v.recordAt(&path) + if err != nil { + return 0, err + } + for bit := range cells { + c := &cells[bit] + if c.kind != pbinNodeLeaf { + continue + } + start := path + start.appendBit(uint64(bit)) + key, _, err := v.leaf(&start, c) + if err != nil { + return 0, err + } + want, err := pbinVerifyDerivedKey(c, key) + if err != nil { + return 0, err + } + if !bytes.Equal(want, key) { + return 0, fmt.Errorf("%w: stored at %x, derives %x", errPBinVerifyPosition, key, want) + } + leaves++ + } + } + return leaves, nil +} + +// pbinVerifyDerivedKey re-derives a leaf's tree key from its plain key. The +// sub-index comes from the stored key because the two account-header leaves share +// one address; which of the two it is, the record does not say. +func pbinVerifyDerivedKey(c *pbinCell, key []byte) ([]byte, error) { + switch { + case c.accountAddrLen > 0: + if len(key) != pbinAccountKeyLength { + return nil, fmt.Errorf("pbin verify: account leaf key of %d bytes, want %d", len(key), pbinAccountKeyLength) + } + subIndex := key[pbinAccountKeyLength-1] + if subIndex != pbinBasicDataLeafKey && subIndex != pbinCodeHashLeafKey { + return nil, fmt.Errorf("pbin verify: account leaf at sub-index %d is neither header leaf", subIndex) + } + return pbinTreeKeyAccount(c.accountAddr[:c.accountAddrLen], subIndex), nil + case c.storageAddrLen > 0: + addr, slot := c.storageAddr[:length.Addr], c.storageAddr[length.Addr:c.storageAddrLen] + return pbinTreeKeyStorage(addr, slot), nil + default: + return nil, errors.New("pbin verify: leaf carries no plain key") + } +} + +// pbinVerifyBits spells a path one bit per byte, the shape the oracle's +// encode_bit_prefix takes. +func pbinVerifyBits(p *pbinBitpath) []byte { + out := make([]byte, p.bitLen) + for i := range out { + out[i] = byte(p.bit(int16(i))) + } + return out +} + +func pbinVerifyPackBits(bits []byte) []byte { + out := make([]byte, (len(bits)+7)/8) + for i, b := range bits { + out[i/8] |= b << (7 - i%8) + } + return out +} + +// pbinTestVerifyRecords is the check every multi-leaf corpus gets: the records +// rebuild the root the engine returned, and every leaf they hold sits at its own +// key. +func pbinTestVerifyRecords(t *testing.T, ms *MockState, root []byte, wantLeaves int) { + t.Helper() + v := &pbinVerifier{t: t, ms: ms} + + recomputed, err := v.recomputeRoot() + require.NoError(t, err) + require.Equal(t, root, recomputed, "records do not rebuild the engine's root") + + leaves, err := v.checkPlainKeys() + require.NoError(t, err) + require.Equal(t, wantLeaves, leaves) +} + +func pbinTestVerifyCorpora(t *testing.T) []struct { + name string + corpus *pbinTestCorpus +} { + t.Helper() + return []struct { + name string + corpus *pbinTestCorpus + }{ + { + name: "two accounts", + corpus: new(pbinTestCorpus). + account(pbinOracleAddr(51), 1, 2, common.Hash{0x01}). + account(pbinOracleAddr(52), 3, 4, common.Hash{0x02}), + }, + { + name: "zone boundary slots", + corpus: new(pbinTestCorpus). + storage(pbinOracleAddr(53), pbinOracleSlot(63), 0x01). + storage(pbinOracleAddr(53), pbinOracleSlot(64), 0x02). + storage(pbinOracleAddr(53), pbinOracleSlot(255), 0x03). + storage(pbinOracleAddr(53), pbinOracleSlot(256), 0x04), + }, + {name: "mixed accounts and storage", corpus: pbinTestMixedCorpus()}, + {name: "deep shared prefix", corpus: pbinTestDeepSharedPrefixCorpus()}, + } +} + +// TestPBinVerifyRecordsRebuildRoot is the independent recompute: what the engine +// wrote must hash back to what it returned, with no cell of the live grid +// involved. +func TestPBinVerifyRecordsRebuildRoot(t *testing.T) { + t.Parallel() + + for _, tc := range pbinTestVerifyCorpora(t) { + t.Run(tc.name, func(t *testing.T) { + t.Parallel() + pph, ms := pbinTestEngine(t) + require.NoError(t, ms.applyPlainUpdates(tc.corpus.plainKeys, tc.corpus.updates)) + root := pbinTestProcess(t, pph, tc.corpus.plainKeys, tc.corpus.updates) + + require.Equal(t, tc.corpus.oracleRoot(t), root) + pbinTestVerifyRecords(t, ms, root, len(tc.corpus.entries(t))) + }) + } +} + +// TestPBinVerifyRootRecordIsUnique pins the shape the recompute relies on: one +// record has no ancestor, and its path is the root node's prefix. +func TestPBinVerifyRootRecordIsUnique(t *testing.T) { + t.Parallel() + + corpus := new(pbinTestCorpus). + storage(pbinOracleAddr(54), pbinOracleSlot(256), 0x01). + storage(pbinOracleAddr(54), pbinOracleSlot(257), 0x02) + + pph, ms := pbinTestEngine(t) + require.NoError(t, ms.applyPlainUpdates(corpus.plainKeys, corpus.updates)) + pbinTestProcess(t, pph, corpus.plainKeys, corpus.updates) + + v := &pbinVerifier{t: t, ms: ms} + root, err := v.rootPath() + require.NoError(t, err) + require.Equal(t, pph.grid.root.prefix, root, "the root record's path is the root node's prefix") +} + +// TestPBinVerifyEmptyStateHasNoRecords checks the recompute refuses to invent a +// tree: a root that is a bare leaf writes no record at all. +func TestPBinVerifyEmptyStateHasNoRecords(t *testing.T) { + t.Parallel() + + corpus := new(pbinTestCorpus).storage(pbinOracleAddr(55), pbinOracleSlot(1000), 0x01) + pph, ms := pbinTestEngine(t) + require.NoError(t, ms.applyPlainUpdates(corpus.plainKeys, corpus.updates)) + pbinTestProcess(t, pph, corpus.plainKeys, corpus.updates) + + v := &pbinVerifier{t: t, ms: ms} + _, err := v.recomputeRoot() + require.ErrorIs(t, err, errPBinVerifyNoRecords) +} + +// TestPBinVerifyCatchesSwappedCells gives both checks teeth: swapping a record's +// two children moves each leaf to a position its key does not spell, which the +// plain-key check must reject and the recompute must no longer reproduce. +func TestPBinVerifyCatchesSwappedCells(t *testing.T) { + t.Parallel() + + corpus := new(pbinTestCorpus). + storage(pbinOracleAddr(56), pbinOracleSlot(256), 0x01). + storage(pbinOracleAddr(56), pbinOracleSlot(257), 0x02) + + pph, ms := pbinTestEngine(t) + require.NoError(t, ms.applyPlainUpdates(corpus.plainKeys, corpus.updates)) + root := pbinTestProcess(t, pph, corpus.plainKeys, corpus.updates) + pbinTestVerifyRecords(t, ms, root, len(corpus.entries(t))) + + v := &pbinVerifier{t: t, ms: ms} + path, err := v.rootPath() + require.NoError(t, err) + cells, err := v.recordAt(&path) + require.NoError(t, err) + + cells[0], cells[1] = cells[1], cells[0] + var enc pbinBranchEncoder + swapped, err := enc.encode(pbinCellBits, pbinCellBits, &cells) + require.NoError(t, err) + require.NoError(t, ms.PutBranch(pbinEncodeBitPath(&path), bytes.Clone(swapped), nil)) + + _, err = v.checkPlainKeys() + require.ErrorIs(t, err, errPBinVerifyPosition) + + recomputed, err := v.recomputeRoot() + require.NoError(t, err) + require.NotEqual(t, root, recomputed) +} From 4103ecbe84e13661ef074965d49ed60f5c8866e9 Mon Sep 17 00:00:00 2001 From: awskii Date: Wed, 29 Jul 2026 19:21:25 +0700 Subject: [PATCH 13/56] feat: verify EIP-8297 acceptance criteria --- docs/plans/20260729-pbin-patricia-hashed.md | 63 ++++++++++++++++++--- 1 file changed, 54 insertions(+), 9 deletions(-) diff --git a/docs/plans/20260729-pbin-patricia-hashed.md b/docs/plans/20260729-pbin-patricia-hashed.md index 2362dd16aed..5d9be13314e 100644 --- a/docs/plans/20260729-pbin-patricia-hashed.md +++ b/docs/plans/20260729-pbin-patricia-hashed.md @@ -270,14 +270,44 @@ func pbinCommonPrefixBits(a, b *pbinBitpath) int16 // XOR + LeadingZeros64, clam ### Task 12: Verify acceptance criteria -- [ ] verify all requirements from Overview are implemented and M0 scope boundaries were respected -- [ ] verify no shared type, interface or signature was modified: `git diff --stat` shows `commitment.go` as the only pre-existing non-test file, additive only -- [ ] verify every hazard in the register except H6 has a named passing test -- [ ] verify every new package-level identifier carries the `pbin` prefix and the package compiles without collision -- [ ] run the package test suite: `go test ./execution/commitment/...` -- [ ] run fuzzers briefly, one target per invocation — `-fuzz` refuses a regex matching several: `go test ./execution/commitment/ -run=Fuzz -fuzz=FuzzPBinBitPathCodec -fuzztime=60s` then the same for `FuzzPBinProcessMatchesOracle` -- [ ] verify `go build ./...` and `go vet ./execution/commitment/...` are clean -- [ ] record the Task 8 instrumentation counters under Post-Completion +- [x] verify all requirements from Overview are implemented and M0 scope boundaries were respected — `VariantBinPatriciaTrie`/`PBinPatriciaHashed`/`pbinKeyHasher` appear outside the `pbin_*` files only at the three additive `commitment.go` sites, so nothing reaches the domain layer +- [x] verify no shared type, interface or signature was modified: `git diff --stat` shows `commitment.go` as the only pre-existing non-test file, additive only — 22 files, 6,289 insertions, 0 deletions; `commitment.go` +11/-0 +- [x] verify every hazard in the register except H6 has a named passing test +- [x] verify every new package-level identifier carries the `pbin` prefix and the package compiles without collision — 274 identifiers checked by AST walk; the only ones not starting at position 0 are `errPBin*` and `NewPBinPatriciaHashed`, where Go's `err`/`New` convention precedes the marker +- [x] run the package test suite: `go test ./execution/commitment/...` +- [x] run fuzzers briefly, one target per invocation — `-fuzz` refuses a regex matching several: `go test ./execution/commitment/ -run=Fuzz -fuzz=FuzzPBinBitPathCodec -fuzztime=60s` then the same for `FuzzPBinProcessMatchesOracle` +- [x] verify `go build ./...` and `go vet ./execution/commitment/...` are clean +- [x] record the Task 8 instrumentation counters under Post-Completion + +**Verification results.** + +| Check | Result | +|-------|--------| +| `go test ./execution/commitment/...` | ok, 9.6s | +| `go vet ./execution/commitment/...` | clean | +| `go build ./...` | clean | +| `FuzzPBinBitPathCodec -fuzztime=60s` | pass, 8.2M execs, 0 new interesting | +| `FuzzPBinProcessMatchesOracle -fuzztime=60s` | pass, 231k execs, 97 new interesting | + +Hazard → guard, all passing (H6 is N/A in M0, H14 is a review item): + +| Hazard | Guard | +|--------|-------| +| H1 | `TestPBinFoldSplitInsidePrefixMatchesOracle`, `TestPBinSplitInsideStoredPrefix`, `TestPBinDeepSharedPrefixCorpus` | +| H2 | `TestPBinUntouchedSiblingSurvivesBatch` | +| H3 | `TestPBinBranchDecodeRejects` (`pbin_cell_test.go:172`) | +| H4 | `TestPBinBranchCodecRoundTripPrefixBitLengths` (`pbin_cell_test.go:63`) | +| H5 | `FuzzPBinBitPathCodec`, `TestPBinBitPathNeverEncodesToStateKey` | +| H7 | `TestPBinUnfoldEmptyPrefixBranchRecord` | +| H8 | `TestPBinStorageZoneRouting`, `TestPBinAddr`, `pbinVerifier.checkPlainKeys` | +| H9 | `TestPBinNeedUnfolding` | +| H10 | `TestPBinCommonPrefixBits_IgnoresBitsBeyondBitLen`, `TestPBinCommonPrefixBits_ShorterPathIsPrefix` | +| H11 | `TestPBinEmptyTreeHash`, `TestPBinRootHashEmptyEngine`, `TestPBinOracleEmptyTreeHash` | +| H12 | `TestPBinFoldRejectsInconsistentGrid`, `TestPBinFoldBranchRejectsWrongArity` | +| H13 | `TestPBinProcessRejectsStreamDelete`, `TestPBinProcessMissingStateIsAbsent` | +| H14 | `pbinHasher.cellHash` is the only cell hasher; `PBinPatriciaHashed.cellHash` delegates to it and `leafCellHash` is reachable only through it | + +⚠️ **Fuzz-harness note.** `FuzzPBinProcessMatchesOracle` at the documented invocation can end in `context deadline exceeded`. It is the harness, not the engine: Go's default `-fuzzminimizetime` is 60s, so a newly interesting input found late in a 60s run keeps minimizing past the coordinator's shutdown deadline. Symptom is `execs` falling to 0/sec near the end. Adding `-fuzzminimizetime=2s` holds ~4,900 exec/s throughout and exits clean. Separately checked that no input is slow: 20,000 generated corpora ran with a worst case of 23ms. ### Task 13: [Final] Update documentation @@ -289,8 +319,23 @@ func pbinCommonPrefixBits(a, b *pbinBitpath) int16 // XOR + LeadingZeros64, clam *Items requiring manual intervention, measurement, or follow-on milestones — no checkboxes* +**Task 8 instrumentation counters (measured in Task 12).** + +`splitsInsidePrefix` counts probes diverging inside a cell's prefix; `materializeReads` counts the `ctx.Branch` reads that follow, i.e. the ones the descent alone would not have made. + +| Corpus | keys | leaves | splitsInsidePrefix | materializeReads | +|--------|-----:|-------:|-------------------:|-----------------:| +| mixed, one batch | 54 | 60 | 59 | 0 | +| deep shared prefix, one batch | 4 | 8 | 7 | 0 | +| mixed, two batches | 54 | 60 | 59 | 6 | +| deep shared prefix, two batches | 4 | 8 | 7 | 0 | +| mixed, one key per batch | 54 | 60 | 59 | 33 | +| fuzz generator space, 2,000 runs | 200,329 | — | 79,437 | 751 | + +Splits inside a prefix are the common case, not the exception — roughly one per key. What makes them cheap is Task 8's in-memory child hashes: **within a single `Process` call `materializeReads` is 0**, because every cell that splits was built by that same run and re-derives. A read costs only when a cell arrives from a record an earlier batch wrote, so the counter tracks batch granularity rather than tree shape — 6 reads at two batches, 33 at one key per batch (the drive loop's worst case), and 751 over 200,329 keys in the fuzz space (0.37% of keys, 0.95% of splits). + **Decisions deferred to data:** -- Split-rehash strategy. M0 ships materialize-on-split, narrowed by Task 8's in-memory child hashes to cells that arrived from a record — a node this run folded re-derives for free. `pbinCounters.materializeReads` measures what is left. If it stays non-trivial, promote the two child hashes into the record itself and the hazard disappears, at 32 B per branch cell and a migration story. +- Split-rehash strategy. M0 ships materialize-on-split, narrowed by Task 8's in-memory child hashes to cells that arrived from a record — a node this run folded re-derives for free. The numbers above say the residual is small and, crucially, driven by how work is batched rather than by the corpus. Promoting the two child hashes into the record (32 B per branch cell, plus a migration story) would remove the hazard outright, but on M0 evidence it buys under 1% of splits; revisit against production batch sizes, where a batch spans one block and the cross-batch fraction will be higher than these tests show. - Record the one-prefix-per-cell rationale (Task 5) here and in the commit body rather than as a source comment. **Out of scope, in rough dependency order:** From 58e39f5e74b3e8b6378ec186ecf555057d24b1af Mon Sep 17 00:00:00 2001 From: awskii Date: Wed, 29 Jul 2026 19:25:07 +0700 Subject: [PATCH 14/56] feat: EIP-8297 engine doc header, plan completed Name the EIP, the Keccak-256 choice for both node hashing and tree-key derivation, and the M0 scope boundaries at the top of the engine file. Kept off the package clause: the comment describes this engine, not the whole commitment package. --- .../{ => completed}/20260729-pbin-patricia-hashed.md | 6 +++--- execution/commitment/pbin_patricia_hashed.go | 11 +++++++++++ 2 files changed, 14 insertions(+), 3 deletions(-) rename docs/plans/{ => completed}/20260729-pbin-patricia-hashed.md (98%) diff --git a/docs/plans/20260729-pbin-patricia-hashed.md b/docs/plans/completed/20260729-pbin-patricia-hashed.md similarity index 98% rename from docs/plans/20260729-pbin-patricia-hashed.md rename to docs/plans/completed/20260729-pbin-patricia-hashed.md index 5d9be13314e..eeabe93f596 100644 --- a/docs/plans/20260729-pbin-patricia-hashed.md +++ b/docs/plans/completed/20260729-pbin-patricia-hashed.md @@ -311,9 +311,9 @@ Hazard → guard, all passing (H6 is N/A in M0, H14 is a review item): ### Task 13: [Final] Update documentation -- [ ] add a package-level doc comment on `pbin_patricia_hashed.go` naming the EIP, the Keccak suite choice and the M0 scope boundaries -- [ ] update `CLAUDE.md` if new patterns were discovered -- [ ] move this plan to `docs/plans/completed/` +- [x] add a package-level doc comment on `pbin_patricia_hashed.go` naming the EIP, the Keccak suite choice and the M0 scope boundaries — landed as a file-header comment separated from `package commitment` by a blank line, matching the package's own convention; attaching it would have made it the doc comment for all of `commitment`, which this engine does not own +- [x] update `CLAUDE.md` if new patterns were discovered — no change: the `pbin` prefix rule is plan-local, and nothing repo-wide came out of M0 +- [x] move this plan to `docs/plans/completed/` ## Post-Completion diff --git a/execution/commitment/pbin_patricia_hashed.go b/execution/commitment/pbin_patricia_hashed.go index 306f584cd73..9d83308359d 100644 --- a/execution/commitment/pbin_patricia_hashed.go +++ b/execution/commitment/pbin_patricia_hashed.go @@ -14,6 +14,17 @@ // You should have received a copy of the GNU Lesser General Public License // along with Erigon. If not, see . +// PBinPatriciaHashed — commitment over EIP-8297's partitioned binary tree. +// +// The EIP leaves its hash function open and names Keccak-256 as a candidate; +// this engine uses Keccak-256 both for node hashing and for tree-key +// derivation, behind pbinHasher so the suite can be swapped. +// +// M0 scope: in-memory Process over the account and storage zones, ModeDirect +// only. Code chunking, deletion, commitment state save/restore and parallel +// mounting are out — BASIC_DATA carries code_size 0, and a delete arriving on +// the update stream is rejected rather than applied. + package commitment import ( From 88f4c84314fedd712031f6505aa3d1e59d575bc8 Mon Sep 17 00:00:00 2001 From: awskii Date: Wed, 29 Jul 2026 20:06:32 +0700 Subject: [PATCH 15/56] fix: address code review findings MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit execution/commitment: persist the root cell so a stored tree is reloadable. Records are keyed by the path that reaches the node, which leaves the root itself unnamed: its prefix is empty only when the tree spans both zones, so a single-zone tree (and a bare-leaf root, which writes no record at all) was rebuilt from the touched keys alone after Reset — a wrong root with no error. The root cell now goes to a reserved key no bit path encodes. execution/commitment: reject a delete that lands on a live leaf. ModeDirect carries no Update, so the stream-delete guard was unreachable and every absent state read returned nil, leaving the stale leaf in the tree. execution/commitment: reject a record leaf with no plain key — it hashed a zero-valued state instead of failing. Also: one divergence primitive instead of two, with the word scan on the descent path; drop pbinMaxDepths, pbinGrid.reset and the pbinBranchData alias; move the test-only leafHash into the test file; cover the context error paths. .github, docs: register the two pbin fuzzers, cap fuzz minimization so a late crash cannot minimize past the coordinator deadline. cmd/integration: commitment visualize decodes hex records only — reject --trie=bin instead of reporting hex-shaped stats for it. --- .github/workflows/test-fuzz.yml | 7 +- cmd/integration/commands/commitment.go | 5 +- docs/fuzzing.md | 1 + .../20260729-pbin-patricia-hashed.md | 2 +- execution/commitment/pbin_bitpath.go | 35 +++-- execution/commitment/pbin_bitpath_test.go | 41 +++++- execution/commitment/pbin_branch.go | 22 ++-- execution/commitment/pbin_cell.go | 23 +--- execution/commitment/pbin_cell_test.go | 24 +--- execution/commitment/pbin_fold_test.go | 4 +- execution/commitment/pbin_hash.go | 15 --- execution/commitment/pbin_hash_test.go | 21 ++- execution/commitment/pbin_patricia_hashed.go | 107 ++++++++++++--- execution/commitment/pbin_process_test.go | 122 ++++++++++++++++++ execution/commitment/pbin_unfold_test.go | 60 +++++++-- execution/commitment/pbin_variant_test.go | 50 +++++++ execution/commitment/pbin_verify_test.go | 97 +++++++++++--- 17 files changed, 493 insertions(+), 143 deletions(-) diff --git a/.github/workflows/test-fuzz.yml b/.github/workflows/test-fuzz.yml index ef4632a587c..b9e21f9372a 100644 --- a/.github/workflows/test-fuzz.yml +++ b/.github/workflows/test-fuzz.yml @@ -70,6 +70,8 @@ jobs: - { name: patricia, pkg: db/seg/patricia, fn: FuzzPatricia } - { name: patricia-longest-match, pkg: db/seg/patricia, fn: FuzzLongestMatch } - { name: abi, pkg: execution/abi, fn: FuzzABI } + - { name: pbin-bitpath-codec, pkg: execution/commitment, fn: FuzzPBinBitPathCodec } + - { name: pbin-process-oracle, pkg: execution/commitment, fn: FuzzPBinProcessMatchesOracle } - { name: nibbles-hexcompact, pkg: execution/commitment/nibbles, fn: FuzzHexCompactRoundtrip } - { name: rlp, pkg: execution/types, fn: FuzzRLP } - { name: precompiles, pkg: execution/vm, fn: FuzzPrecompiledContracts } @@ -108,7 +110,10 @@ jobs: run: | mkdir -p "$ERIGON_BUILD/fuzz" echo "::group::go test -fuzz ${{ matrix.fn }} (./${{ matrix.pkg }}, ${FUZZTIME})" - go test "./${{ matrix.pkg }}/" -run '^$' -fuzz "^${{ matrix.fn }}$" -fuzztime "${FUZZTIME}" + # Minimization is capped: Go's 60s default lets an input found late in + # the run keep minimizing past the coordinator's deadline, which + # surfaces as "context deadline exceeded" rather than as the crash. + go test "./${{ matrix.pkg }}/" -run '^$' -fuzz "^${{ matrix.fn }}$" -fuzztime "${FUZZTIME}" -fuzzminimizetime 10s echo "::endgroup::" # On a crash, `go test` writes the minimized reproducing input to diff --git a/cmd/integration/commands/commitment.go b/cmd/integration/commands/commitment.go index 5293e37be1a..c2062466b59 100644 --- a/cmd/integration/commands/commitment.go +++ b/cmd/integration/commands/commitment.go @@ -138,7 +138,7 @@ func init() { // commitment visualize cmdCommitmentVisualize.Flags().StringVar(&visualizeOutputDir, "output", "", "existing directory to store output HTML. By default, same as commitment files") cmdCommitmentVisualize.Flags().IntVarP(&visualizeConcurrency, "concurrency", "j", 4, "amount of concurrently processed files") - cmdCommitmentVisualize.Flags().StringVar(&visualizeTrieVariant, "trie", "hex", "commitment trie variant (values are hex and parallel)") + cmdCommitmentVisualize.Flags().StringVar(&visualizeTrieVariant, "trie", "hex", "commitment trie variant (hex or parallel)") cmdCommitmentVisualize.Flags().StringVar(&visualizeCompression, "compression", "none", "compression type (none, k, v, kv)") cmdCommitmentVisualize.Flags().BoolVar(&visualizePrintState, "state", false, "print state of file") cmdCommitmentVisualize.Flags().IntVar(&visualizeDepth, "depth", 0, "depth of the prefixes to analyze") @@ -1371,6 +1371,9 @@ func extractKVPairFromCompressed(filename string, keysSink chan commitment.Branc } defer dec.Close() tv := commitment.ParseTrieVariant(visualizeTrieVariant) + if tv == commitment.VariantBinPatriciaTrie { + return fmt.Errorf("commitment visualize decodes hex records only, not %s", tv) + } fc, err := seg.ParseFileCompression(visualizeCompression) if err != nil { diff --git a/docs/fuzzing.md b/docs/fuzzing.md index da384217cf4..06d1497c838 100644 --- a/docs/fuzzing.md +++ b/docs/fuzzing.md @@ -32,6 +32,7 @@ Two things run these fuzzers: | `db/seg` | `FuzzCompress`, `FuzzDecompressMatch` | | `db/seg/patricia` | `FuzzPatricia`, `FuzzLongestMatch` | | `execution/abi` | `FuzzABI` | +| `execution/commitment` | `FuzzPBinBitPathCodec`, `FuzzPBinProcessMatchesOracle` | | `execution/commitment/nibbles` | `FuzzHexCompactRoundtrip` | | `execution/types` | `FuzzRLP` | | `execution/vm` | `FuzzPrecompiledContracts` | diff --git a/docs/plans/completed/20260729-pbin-patricia-hashed.md b/docs/plans/completed/20260729-pbin-patricia-hashed.md index eeabe93f596..7080badaea8 100644 --- a/docs/plans/completed/20260729-pbin-patricia-hashed.md +++ b/docs/plans/completed/20260729-pbin-patricia-hashed.md @@ -271,7 +271,7 @@ func pbinCommonPrefixBits(a, b *pbinBitpath) int16 // XOR + LeadingZeros64, clam ### Task 12: Verify acceptance criteria - [x] verify all requirements from Overview are implemented and M0 scope boundaries were respected — `VariantBinPatriciaTrie`/`PBinPatriciaHashed`/`pbinKeyHasher` appear outside the `pbin_*` files only at the three additive `commitment.go` sites, so nothing reaches the domain layer -- [x] verify no shared type, interface or signature was modified: `git diff --stat` shows `commitment.go` as the only pre-existing non-test file, additive only — 22 files, 6,289 insertions, 0 deletions; `commitment.go` +11/-0 +- [x] verify no shared type, interface or signature was modified: `git diff --stat` shows `commitment.go` as the only pre-existing non-test file, additive only — every other touched file is new; `commitment.go` +11/-0 - [x] verify every hazard in the register except H6 has a named passing test - [x] verify every new package-level identifier carries the `pbin` prefix and the package compiles without collision — 274 identifiers checked by AST walk; the only ones not starting at position 0 are `errPBin*` and `NewPBinPatriciaHashed`, where Go's `err`/`New` convention precedes the marker - [x] run the package test suite: `go test ./execution/commitment/...` diff --git a/execution/commitment/pbin_bitpath.go b/execution/commitment/pbin_bitpath.go index 029c9a46e11..de8c9289f2c 100644 --- a/execution/commitment/pbin_bitpath.go +++ b/execution/commitment/pbin_bitpath.go @@ -122,16 +122,26 @@ func (p *pbinBitpath) append(o *pbinBitpath) { } func (p *pbinBitpath) hasPrefix(o *pbinBitpath) bool { - return o.bitLen <= p.bitLen && pbinCommonPrefixBits(p, o) == o.bitLen + return o.bitLen <= p.bitLen && pbinCommonPrefixBitsAt(p, 0, o) == o.bitLen } -// pbinCommonPrefixBits reports how many leading bits a and b share, never more -// than the shorter path holds. -func pbinCommonPrefixBits(a, b *pbinBitpath) int16 { - limit := min(a.bitLen, b.bitLen) +// pbinCommonPrefixBitsAt reports how many leading bits of prefix agree with key +// read from bit `from`, never past the end of either operand. It is the one +// divergence primitive: bits past a path's length are masked to zero, so a whole +// word can be compared at a time and the answer clamped to what both hold. +func pbinCommonPrefixBitsAt(key *pbinBitpath, from int16, prefix *pbinBitpath) int16 { + limit := min(key.bitLen-from, prefix.bitLen) + if limit <= 0 { + return 0 + } + shift := uint(from % 64) n := int16(0) - for i := 0; i < pbinPathWords && n < limit; i++ { - if x := a.w[i] ^ b.w[i]; x != 0 { + for wi := int(from / 64); n < limit; wi++ { + w := key.w[wi] << shift + if shift != 0 && wi+1 < pbinPathWords { + w |= key.w[wi+1] >> (64 - shift) + } + if x := w ^ prefix.w[n/64]; x != 0 { n += int16(bits.LeadingZeros64(x)) break } @@ -140,17 +150,6 @@ func pbinCommonPrefixBits(a, b *pbinBitpath) int16 { return min(n, limit) } -// pbinCommonPrefixBitsAt reports how many leading bits of prefix agree with key -// read from bit `from`, never past the end of either operand. -func pbinCommonPrefixBitsAt(key *pbinBitpath, from int16, prefix *pbinBitpath) int16 { - limit := min(key.bitLen-from, prefix.bitLen) - n := int16(0) - for n < limit && key.bit(from+n) == prefix.bit(n) { - n++ - } - return n -} - // pbinAppendPackedBits appends the path's bits MSB-first, zero-padded to a byte // boundary. func (p *pbinBitpath) appendPackedBits(dst []byte) []byte { diff --git a/execution/commitment/pbin_bitpath_test.go b/execution/commitment/pbin_bitpath_test.go index 685f76e06d2..de1ed712879 100644 --- a/execution/commitment/pbin_bitpath_test.go +++ b/execution/commitment/pbin_bitpath_test.go @@ -65,8 +65,8 @@ func TestPBinCommonPrefixBits(t *testing.T) { if tc.flipAt >= 0 { b = pbinFlipBit(b, tc.flipAt) } - require.Equal(t, tc.want, pbinCommonPrefixBits(&a, &b)) - require.Equal(t, tc.want, pbinCommonPrefixBits(&b, &a)) + require.Equal(t, tc.want, pbinCommonPrefixBitsAt(&a, 0, &b)) + require.Equal(t, tc.want, pbinCommonPrefixBitsAt(&b, 0, &a)) }) } } @@ -80,8 +80,8 @@ func TestPBinCommonPrefixBits_ShorterPathIsPrefix(t *testing.T) { long := pbinTestPath(t, 0xAA, 528) short := pbinTestPath(t, 0xAA, 272) - require.Equal(t, int16(272), pbinCommonPrefixBits(&short, &long)) - require.Equal(t, int16(272), pbinCommonPrefixBits(&long, &short)) + require.Equal(t, int16(272), pbinCommonPrefixBitsAt(&short, 0, &long)) + require.Equal(t, int16(272), pbinCommonPrefixBitsAt(&long, 0, &short)) } // Words carrying set bits beyond bitLen must not be read as real path bits @@ -97,8 +97,8 @@ func TestPBinCommonPrefixBits_IgnoresBitsBeyondBitLen(t *testing.T) { dirty.w[i] = ^uint64(0) } - require.Equal(t, int16(272), pbinCommonPrefixBits(&dirty, &long)) - require.Equal(t, int16(272), pbinCommonPrefixBits(&long, &dirty)) + require.Equal(t, int16(272), pbinCommonPrefixBitsAt(&dirty, 0, &long)) + require.Equal(t, int16(272), pbinCommonPrefixBitsAt(&long, 0, &dirty)) clean := pbinTestPath(t, 0xAA, 272) dirty.maskTail() @@ -239,3 +239,32 @@ func FuzzPBinBitPathCodec(f *testing.F) { } }) } + +// The word-at-a-time divergence scan must agree with a bit-by-bit walk at every +// offset, including the ones that straddle a word boundary. +func TestPBinCommonPrefixBitsAt_MatchesNaiveScan(t *testing.T) { + t.Parallel() + + naive := func(key *pbinBitpath, from int16, prefix *pbinBitpath) int16 { + limit := min(key.bitLen-from, prefix.bitLen) + n := int16(0) + for n < limit && key.bit(from+n) == prefix.bit(n) { + n++ + } + return n + } + + key := pbinTestPath(t, 0x6D, pbinMaxPathBits) + for _, from := range []int16{0, 1, 7, 63, 64, 65, 127, 128, 271, 272, 511, 512, 527, 528} { + for _, want := range []int16{0, 1, 63, 64, 65, 128, 271} { + p := key.slice(from, min(from+want, key.bitLen)) + require.Equalf(t, naive(&key, from, &p), pbinCommonPrefixBitsAt(&key, from, &p), + "from %d, %d-bit prefix", from, p.bitLen) + for flip := int16(0); flip < p.bitLen; flip++ { + d := pbinFlipBit(p, flip) + require.Equalf(t, naive(&key, from, &d), pbinCommonPrefixBitsAt(&key, from, &d), + "from %d, %d-bit prefix flipped at %d", from, p.bitLen, flip) + } + } + } +} diff --git a/execution/commitment/pbin_branch.go b/execution/commitment/pbin_branch.go index f37c1945bf6..c45b2888e1e 100644 --- a/execution/commitment/pbin_branch.go +++ b/execution/commitment/pbin_branch.go @@ -46,20 +46,19 @@ var ( errPBinCellMaps = errors.New("pbin: branch maps address more than two cells") ) -// pbinBranchData is one serialised binary node. It is deliberately not +// pbinBranchEncoder serialises a binary node. The payload is deliberately not // BranchData: a 66-byte tree-key prefix does not fit the shared codec's cell // fields, and PatriciaContext moves branch payloads as opaque bytes. -type pbinBranchData []byte - -// pbinBranchEncoder serialises a binary node. Every record carries both child -// cells, so a record read back replaces its predecessor outright and no -// merge-with-previous path exists — at arity 2 the untouched sibling is the -// whole other half of the subtree, and merging is what loses it. +// +// Every record carries both child cells, so a record read back replaces its +// predecessor outright and no merge-with-previous path exists — at arity 2 the +// untouched sibling is the whole other half of the subtree, and merging is what +// loses it. type pbinBranchEncoder struct { buf []byte } -func (e *pbinBranchEncoder) encode(touchMap, afterMap uint16, cells *[2]pbinCell) (pbinBranchData, error) { +func (e *pbinBranchEncoder) encode(touchMap, afterMap uint16, cells *[2]pbinCell) ([]byte, error) { if err := pbinCheckCellMaps(touchMap, afterMap); err != nil { return nil, err } @@ -157,6 +156,13 @@ func pbinDecodeCell(data []byte, pos int, c *pbinCell) (int, error) { switch fields & pbinFieldKind { case pbinFieldLeaf: c.kind = pbinNodeLeaf + // A leaf without a plain key hashes a zero-valued state instead of failing, + // so the shape is rejected here rather than reaching the hasher. + switch fields & (pbinFieldAccountAddr | pbinFieldStorageAddr) { + case pbinFieldAccountAddr, pbinFieldStorageAddr: + default: + return 0, fmt.Errorf("%w: leaf cell fields %08b name no single plain key", errPBinMalformedBranch, fields) + } case pbinFieldBranch: c.kind = pbinNodeBranch default: diff --git a/execution/commitment/pbin_cell.go b/execution/commitment/pbin_cell.go index a801ead6308..a549e084ddb 100644 --- a/execution/commitment/pbin_cell.go +++ b/execution/commitment/pbin_cell.go @@ -67,14 +67,9 @@ func (c *pbinCell) reset() { c.Update.Reset() } -const ( - // pbinGridRows bounds the active rows: a row consumes at least the bit it - // splits on, so one row per path bit is enough. - pbinGridRows = pbinMaxPathBits - // pbinMaxDepths bounds anything indexed by bit depth, which is inclusive of a - // full-length path and so runs one past the row count. - pbinMaxDepths = pbinMaxPathBits + 1 -) +// pbinGridRows bounds the active rows: a row consumes at least the bit it splits +// on, so one row per path bit is enough. +const pbinGridRows = pbinMaxPathBits // pbinGrid is the unfolded part of the tree: one row per level of descent, two // cells per row. touchMap/afterMap are uint16 so the OnesCount16 / @@ -90,21 +85,12 @@ type pbinGrid struct { activeRows int } -func (g *pbinGrid) reset() { - g.resetRows(len(g.rows)) -} - // resetForReuse clears only the rows the finished run left live. Rows above // activeRows keep stale cells, which is safe because unfold initializes a row // before anything reads it. func (g *pbinGrid) resetForReuse() { - g.resetRows(g.activeRows) -} - -func (g *pbinGrid) resetRows(rows int) { g.root.reset() - g.activeRows = 0 - for row := range rows { + for row := range g.activeRows { g.rows[row][0].reset() g.rows[row][1].reset() g.depths[row] = 0 @@ -112,4 +98,5 @@ func (g *pbinGrid) resetRows(rows int) { g.touchMap[row] = 0 g.afterMap[row] = 0 } + g.activeRows = 0 } diff --git a/execution/commitment/pbin_cell_test.go b/execution/commitment/pbin_cell_test.go index 6d5d69dbbcb..143d35323d9 100644 --- a/execution/commitment/pbin_cell_test.go +++ b/execution/commitment/pbin_cell_test.go @@ -139,7 +139,7 @@ func TestPBinBranchCodecIsCanonical(t *testing.T) { again, err := enc.encode(0b11, 0b11, &got) require.NoError(t, err) - require.Equal(t, want, []byte(again)) + require.Equal(t, want, again) } // pbinTestRecord assembles a record by hand so decode can be probed with bytes @@ -196,6 +196,11 @@ func TestPBinBranchDecodeRejects(t *testing.T) { {"account address of the wrong length", pbinTestRecord(0b01, 0b01, pbinTestCellBody(pbinFieldLeaf|pbinFieldAccountAddr, 0, nil, pbinTestLenAndVal(bytes.Repeat([]byte{0xEE}, 21))...))}, {"storage address of the wrong length", pbinTestRecord(0b01, 0b01, pbinTestCellBody(pbinFieldLeaf|pbinFieldStorageAddr, 0, nil, pbinTestLenAndVal(bytes.Repeat([]byte{0xEE}, 51))...))}, {"trailing bytes", pbinTestRecord(0b01, 0b01, pbinTestCellBody(pbinFieldBranch, 0, nil), []byte{0x00})}, + // A leaf resolves its value through its plain key, so one without a plain + // key would hash a zero-valued state instead of failing. + {"leaf without a plain key", pbinTestRecord(0b01, 0b01, pbinTestCellBody(pbinFieldLeaf, 0, nil))}, + {"leaf naming both plain keys", pbinTestRecord(0b01, 0b01, pbinTestCellBody(pbinFieldLeaf|pbinFieldAccountAddr|pbinFieldStorageAddr, 0, nil, + append(pbinTestLenAndVal(bytes.Repeat([]byte{0xEE}, length.Addr)), pbinTestLenAndVal(bytes.Repeat([]byte{0xEE}, length.Addr+length.Hash))...)...))}, } { t.Run(tc.name, func(t *testing.T) { t.Parallel() @@ -305,20 +310,6 @@ func pbinTestRequireRowEmpty(t *testing.T, g *pbinGrid, row int) { require.Zero(t, g.afterMap[row]) } -func TestPBinGridReset(t *testing.T) { - t.Parallel() - - g := new(pbinGrid) - pbinTestFillGrid(g, 3) - g.reset() - - require.Zero(t, g.activeRows) - require.Equal(t, pbinTestEmptyCell(), g.root) - for row := range 3 { - pbinTestRequireRowEmpty(t, g, row) - } -} - // resetForReuse only has to clear what the finished run left live; rows above // activeRows are initialized by unfold before anything reads them. func TestPBinGridResetForReuse(t *testing.T) { @@ -338,7 +329,7 @@ func TestPBinGridResetForReuse(t *testing.T) { } // A row consumes at least the bit it splits on, so 528 rows cover the deepest -// path; depth is inclusive of a full-length path and needs one entry more. +// path. func TestPBinGridBounds(t *testing.T) { t.Parallel() @@ -346,5 +337,4 @@ func TestPBinGridBounds(t *testing.T) { require.Equal(t, pbinMaxPathBits, len(g.rows)) require.Equal(t, pbinGridRows, len(g.depths)) require.Equal(t, 2, len(g.rows[0])) - require.Equal(t, pbinGridRows+1, pbinMaxDepths) } diff --git a/execution/commitment/pbin_fold_test.go b/execution/commitment/pbin_fold_test.go index 1c0707cc0ea..9d3e7b10a05 100644 --- a/execution/commitment/pbin_fold_test.go +++ b/execution/commitment/pbin_fold_test.go @@ -466,13 +466,13 @@ func TestPBinFoldDeleteDropsRecord(t *testing.T) { pph, ms := pbinTestEngine(t) key := pbinBitpath{} - pbinTestPutRecord(t, ms, key, [2]pbinCell{ + pbinTestPutTopRecord(t, ms, [2]pbinCell{ pbinTestSpecCell(t, pbinNodeLeaf, "010"), pbinTestSpecCell(t, pbinNodeLeaf, "110"), }) probe := pbinTestPathFromBits(t, pbinTestBitSpec(t, "0101")) - require.NoError(t, pph.unfold(&probe, pph.needUnfolding(&probe))) + pbinTestUnfoldStep(t, pph, &probe) require.True(t, pph.grid.branchBefore[0]) pph.grid.touchMap[0], pph.grid.afterMap[0] = 0b11, 0 diff --git a/execution/commitment/pbin_hash.go b/execution/commitment/pbin_hash.go index 26aaa4aeb66..3bf488a092e 100644 --- a/execution/commitment/pbin_hash.go +++ b/execution/commitment/pbin_hash.go @@ -60,21 +60,6 @@ func pbinAppendBitPrefix(dst []byte, p *pbinBitpath) []byte { return p.appendPackedBits(binary.BigEndian.AppendUint16(dst, uint16(p.bitLen))) } -// leafHash is H(0x00 || key || value) over the complete tree key, so a leaf's -// hash does not depend on where in the tree it sits. -func (h *pbinHasher) leafHash(key, value []byte) common.Hash { - if len(key) != pbinAccountKeyLength && len(key) != pbinStorageKeyLength { - panic(fmt.Sprintf("pbin: leaf key of %d bytes is neither zone length", len(key))) - } - if len(value) != pbinValueLength { - panic(fmt.Sprintf("pbin: leaf value of %d bytes, want %d", len(value), pbinValueLength)) - } - buf := append(h.buf[:0], pbinLeafTag) - buf = append(buf, key...) - buf = append(buf, value...) - return keccak.Sum256(buf) -} - // branchHash is H(0x01 || encode_bit_prefix(prefix) || left || right). An absent // child passes pbinEmptyTreeHash rather than being omitted. func (h *pbinHasher) branchHash(prefix *pbinBitpath, left, right *common.Hash) common.Hash { diff --git a/execution/commitment/pbin_hash_test.go b/execution/commitment/pbin_hash_test.go index b67e3516359..4f25786d765 100644 --- a/execution/commitment/pbin_hash_test.go +++ b/execution/commitment/pbin_hash_test.go @@ -17,15 +17,34 @@ package commitment import ( + "fmt" "testing" "github.com/holiman/uint256" "github.com/stretchr/testify/require" + keccak "github.com/erigontech/fastkeccak" + "github.com/erigontech/erigon/common" "github.com/erigontech/erigon/common/empty" ) +// leafHash is H(0x00 || key || value) over the complete tree key. The engine +// builds this preimage from a cell in leafCellHash; spelling it out from a key +// and a value is what lets a test state the expected hash directly. +func (h *pbinHasher) leafHash(key, value []byte) common.Hash { + if len(key) != pbinAccountKeyLength && len(key) != pbinStorageKeyLength { + panic(fmt.Sprintf("pbin: leaf key of %d bytes is neither zone length", len(key))) + } + if len(value) != pbinValueLength { + panic(fmt.Sprintf("pbin: leaf value of %d bytes, want %d", len(value), pbinValueLength)) + } + buf := append(h.buf[:0], pbinLeafTag) + buf = append(buf, key...) + buf = append(buf, value...) + return keccak.Sum256(buf) +} + func pbinTestPathFromBits(t *testing.T, bits []byte) pbinBitpath { t.Helper() require.LessOrEqual(t, len(bits), pbinMaxPathBits) @@ -316,7 +335,7 @@ func TestPBinCellHashBuildsCorpusRoots(t *testing.T) { a, b := corpus.entries[0], corpus.entries[1] aPath, bPath := pbinPathFromBytes(a.key), pbinPathFromBytes(b.key) - shared := pbinCommonPrefixBits(&aPath, &bPath) + shared := pbinCommonPrefixBitsAt(&aPath, 0, &bPath) prefix := aPath.slice(0, shared) left, right := a, b diff --git a/execution/commitment/pbin_patricia_hashed.go b/execution/commitment/pbin_patricia_hashed.go index 9d83308359d..76b3c654b8c 100644 --- a/execution/commitment/pbin_patricia_hashed.go +++ b/execution/commitment/pbin_patricia_hashed.go @@ -21,9 +21,9 @@ // derivation, behind pbinHasher so the suite can be swapped. // // M0 scope: in-memory Process over the account and storage zones, ModeDirect -// only. Code chunking, deletion, commitment state save/restore and parallel -// mounting are out — BASIC_DATA carries code_size 0, and a delete arriving on -// the update stream is rejected rather than applied. +// only. Code chunking, deletion and parallel mounting are out — BASIC_DATA +// carries code_size 0, and a delete is rejected rather than applied, whether it +// arrives on the update stream or as an absent state read over a live leaf. package commitment @@ -95,8 +95,8 @@ func (pph *PBinPatriciaHashed) SetTraceWriter(w io.Writer) { pph.traceW = w } // EnableCsvMetrics is a no-op: the binary engine collects no metrics in M0. func (pph *PBinPatriciaHashed) EnableCsvMetrics(string) {} -// Reset drops the tree, keeping the context. What survives is in the context, so -// the next run rebuilds whatever it descends into from stored records. +// Reset drops the in-memory tree, keeping the context. The next run rebuilds +// what it descends into from stored records, starting at the root cell record. func (pph *PBinPatriciaHashed) Reset() { pph.grid.resetForReuse() pph.currentKey = pbinBitpath{} @@ -118,6 +118,13 @@ var ( errPBinDeleteUnsupported = errors.New("pbin: EIP-8297 defines no deletion") ) +// pbinRootKey names the record holding the root cell. It is the empty key, which +// pbinAppendBitPath never produces — every encoded path carries at least the +// trailing bit-count byte — so it cannot collide with a node record. The root is +// the one node no descent can name: every other node is found by the path that +// reaches it, while the root's own prefix is stored nowhere else. +var pbinRootKey = []byte{} + // Process folds the update stream into the tree and returns the new root. // HashSort hands keys over in tree-key order, which is descent order, so the // grid only ever walks the path between two consecutive keys. @@ -141,6 +148,9 @@ func (pph *PBinPatriciaHashed) Process(ctx context.Context, updates *Updates, lo return nil, fmt.Errorf("pbin: final fold: %w", err) } } + if err = pph.storeRoot(); err != nil { + return nil, err + } if onProgress != nil { onProgress(&CommitProgress{KeyIndex: processed, UpdateCount: processed}) } @@ -164,11 +174,6 @@ func (pph *PBinPatriciaHashed) processKey(treeKey, plainKey []byte, stateUpdate if update, err = pph.stateOf(plainKey); err != nil { return err } - // A key with no state reads back as a delete; under EIP-8297 that means - // there is no leaf here, not that one has to be removed. - if update.Deleted() { - return nil - } } if err := pph.followAndUpdate(treeKey, plainKey, update); err != nil { return err @@ -234,17 +239,33 @@ func (pph *PBinPatriciaHashed) updateCell(plainKey []byte, probe *pbinBitpath, u g := &pph.grid var c *pbinCell var depth int16 + var row int + var bit uint64 if g.activeRows == 0 { c = &g.root - pph.rootTouched, pph.rootPresent = true, true } else { - row := g.activeRows - 1 + row = g.activeRows - 1 depth = g.depths[row] if probe.bitLen < depth { return fmt.Errorf("pbin: a %d-bit key cannot be updated in a row at depth %d", probe.bitLen, depth) } - bit := probe.bit(depth - 1) + bit = probe.bit(depth - 1) c = &g.rows[row][bit] + } + + // A key with no state reads back as a delete. Landing on an empty slot means + // there simply is no leaf here; landing on one means the leaf has to go, which + // EIP-8297 does not define. + if update.Deleted() { + if c.kind == pbinNodeLeaf { + return fmt.Errorf("%w: %x", errPBinDeleteUnsupported, plainKey) + } + return nil + } + + if g.activeRows == 0 { + pph.rootTouched, pph.rootPresent = true, true + } else { g.touchMap[row] |= uint16(1) << bit g.afterMap[row] |= uint16(1) << bit } @@ -289,12 +310,60 @@ func (pph *PBinPatriciaHashed) RootHash() ([]byte, error) { return hash[:], nil } +// storeRoot persists the root cell so a later engine can find the tree. Without +// it a root sitting under a non-empty prefix — every tree confined to one zone — +// is unreachable, and a run that finds nothing rebuilds from the touched keys +// alone. +func (pph *PBinPatriciaHashed) storeRoot() error { + if !pph.rootTouched { + return nil + } + var record []byte + if pph.grid.root.kind != pbinNodeEmpty { + var err error + if record, err = pbinAppendCell(nil, &pph.grid.root); err != nil { + return err + } + } + if err := pph.ctx.PutBranch(pbinRootKey, record, nil); err != nil { + return fmt.Errorf("pbin: write root cell: %w", err) + } + return nil +} + +// loadRoot reads the stored root cell into the grid. An absent record means the +// tree is empty; anything below the root is reached from the root's own prefix. +func (pph *PBinPatriciaHashed) loadRoot() error { + pph.rootChecked = true + data, _, err := pph.ctx.Branch(pbinRootKey) + if err != nil { + return fmt.Errorf("pbin: read root cell: %w", err) + } + if len(data) == 0 { + return nil + } + pph.grid.root.reset() + pos, err := pbinDecodeCell(data, 0, &pph.grid.root) + if err != nil { + return fmt.Errorf("pbin: decode root cell: %w", err) + } + if pos != len(data) { + return fmt.Errorf("%w: %d trailing bytes after the root cell", errPBinMalformedBranch, len(data)-pos) + } + // Present but untouched: unfold reads these to decide whether the row it opens + // survives, and a loaded root that reads absent takes the tree with it. + pph.rootPresent = true + return nil +} + // pbinUnfoldAction is what needUnfolding tells unfold to do about one cell. type pbinUnfoldAction uint8 const ( // pbinUnfoldNone means the probe key's slot is already in the grid. pbinUnfoldNone pbinUnfoldAction = iota + // pbinUnfoldRoot means the grid has no root cell yet: read the root record. + pbinUnfoldRoot // pbinUnfoldRecord means the cell points straight at a stored node: read it. pbinUnfoldRecord // pbinUnfoldDescend means the probe key agrees with the cell's whole prefix, @@ -327,7 +396,7 @@ func (pph *PBinPatriciaHashed) needUnfolding(probe *pbinBitpath) pbinUnfolding { if pph.rootChecked { return pbinUnfolding{} } - return pbinUnfolding{action: pbinUnfoldRecord} + return pbinUnfolding{action: pbinUnfoldRoot} } cell = &pph.grid.root } else { @@ -365,6 +434,9 @@ func (pph *PBinPatriciaHashed) unfold(probe *pbinBitpath, u pbinUnfolding) error if u.action == pbinUnfoldNone { return nil } + if u.action == pbinUnfoldRoot { + return pph.loadRoot() + } g := &pph.grid var upCell *pbinCell @@ -372,9 +444,6 @@ func (pph *PBinPatriciaHashed) unfold(probe *pbinBitpath, u pbinUnfolding) error var upDepth int16 if g.activeRows == 0 { - if pph.rootChecked && g.root.kind == pbinNodeEmpty { - return nil - } upCell = &g.root touched, present = pph.rootTouched, pph.rootPresent } else { @@ -451,10 +520,6 @@ func (pph *PBinPatriciaHashed) unfoldBranchNode(row int, depth int16, deleted bo return fmt.Errorf("pbin: read branch at %x: %w", key, err) } if len(data) == 0 { - if !pph.rootChecked && pph.currentKey.bitLen == 0 { - pph.rootChecked = true - return nil - } return fmt.Errorf("%w at %x (%d bits)", errPBinMissingBranch, key, pph.currentKey.bitLen) } diff --git a/execution/commitment/pbin_process_test.go b/execution/commitment/pbin_process_test.go index 0d55e1ca379..b49a259bcbb 100644 --- a/execution/commitment/pbin_process_test.go +++ b/execution/commitment/pbin_process_test.go @@ -19,6 +19,7 @@ package commitment import ( "bytes" "context" + "errors" "testing" "github.com/stretchr/testify/require" @@ -26,6 +27,7 @@ import ( "github.com/erigontech/erigon/common" "github.com/erigontech/erigon/common/empty" "github.com/erigontech/erigon/common/length" + "github.com/erigontech/erigon/db/kv" ) // pbinTestCorpus collects plain-key updates in the two shapes the engine @@ -308,6 +310,31 @@ func TestPBinProcessMissingStateIsAbsent(t *testing.T) { require.Equal(t, present.oracleRoot(t), root, "keys with no state contribute no leaf") } +// TestPBinProcessRejectsDeletedLeaf is the case an absent state read must not be +// confused with: the key already holds a leaf, so "no state" means the leaf has +// to go — which EIP-8297 does not define. Skipping it would leave the stale leaf +// in the tree and return a root with no error. +func TestPBinProcessRejectsDeletedLeaf(t *testing.T) { + t.Parallel() + + addr := pbinOracleAddr(23) + corpus := new(pbinTestCorpus). + storage(addr, pbinOracleSlot(256), 0x01). + storage(addr, pbinOracleSlot(257), 0x02) + + pph, ms := pbinTestEngine(t) + require.NoError(t, ms.applyPlainUpdates(corpus.plainKeys, corpus.updates)) + pbinTestProcess(t, pph, corpus.plainKeys, corpus.updates) + + gone := new(pbinTestCorpus).storage(addr, pbinOracleSlot(257), 0x02) + require.NoError(t, ms.applyPlainUpdates(gone.plainKeys, []Update{{Flags: DeleteUpdate}})) + + pph.Reset() + upd := WrapKeyUpdates(t, ModeDirect, pbinKeyHasher(), gone.plainKeys, gone.updates) + _, err := pph.Process(context.Background(), upd, "", nil, WarmupConfig{}) + require.ErrorIs(t, err, errPBinDeleteUnsupported) +} + // TestPBinProcessRepeatedKeyKeepsOneLeaf checks a stem touched twice in one run // still holds a single leaf, so the second visit updates rather than splits. func TestPBinProcessRepeatedKeyKeepsOneLeaf(t *testing.T) { @@ -337,3 +364,98 @@ func TestPBinProcessEmptyUpdatesKeepsEmptyRoot(t *testing.T) { root := pbinTestProcess(t, pph, nil, nil) require.Equal(t, make([]byte, length.Hash), root) } + +var errPBinTestContext = errors.New("pbin test: context failure") + +// pbinFailingContext fails one context call, letting a chosen number through +// first, so each read and write the engine makes can be checked to reach the +// caller instead of being swallowed. +type pbinFailingContext struct { + PatriciaContext + method string + skip int + seen int +} + +func (c *pbinFailingContext) trip(method string) error { + if c.method != method { + return nil + } + c.seen++ + if c.seen <= c.skip { + return nil + } + return errPBinTestContext +} + +func (c *pbinFailingContext) Branch(prefix []byte) ([]byte, kv.Step, error) { + if err := c.trip("Branch"); err != nil { + return nil, 0, err + } + return c.PatriciaContext.Branch(prefix) +} + +func (c *pbinFailingContext) PutBranch(prefix, data, prevData []byte) error { + if err := c.trip("PutBranch"); err != nil { + return err + } + return c.PatriciaContext.PutBranch(prefix, data, prevData) +} + +func (c *pbinFailingContext) Account(plainKey []byte) (*Update, error) { + if err := c.trip("Account"); err != nil { + return nil, err + } + return c.PatriciaContext.Account(plainKey) +} + +func (c *pbinFailingContext) Storage(plainKey []byte) (*Update, error) { + if err := c.trip("Storage"); err != nil { + return nil, err + } + return c.PatriciaContext.Storage(plainKey) +} + +// TestPBinProcessSurfacesContextErrors runs a second batch over a stored tree — +// the path that reads the root cell, a node record, a leaf's state and a branch +// it has to rebuild — and fails one call at a time. +func TestPBinProcessSurfacesContextErrors(t *testing.T) { + t.Parallel() + + addr := pbinOracleAddr(81) + stored := new(pbinTestCorpus). + storage(addr, pbinOracleSlot(256), 0x01). + storage(addr, pbinOracleSlot(257), 0x02). + account(pbinOracleAddr(82), 3, 4, common.Hash{0x82}) + touch := new(pbinTestCorpus). + storage(addr, pbinOracleSlot(258), 0x03). + account(pbinOracleAddr(82), 5, 6, common.Hash{0x82}) + + for _, tc := range []struct { + name string + method string + skip int + }{ + {"root cell read", "Branch", 0}, + {"node record read", "Branch", 1}, + {"storage state read", "Storage", 0}, + {"account state read", "Account", 0}, + {"node record write", "PutBranch", 0}, + {"root cell write", "PutBranch", 1}, + } { + t.Run(tc.name, func(t *testing.T) { + t.Parallel() + + pph, ms := pbinTestEngine(t) + require.NoError(t, ms.applyPlainUpdates(stored.plainKeys, stored.updates)) + pbinTestProcess(t, pph, stored.plainKeys, stored.updates) + require.NoError(t, ms.applyPlainUpdates(touch.plainKeys, touch.updates)) + + pph.Reset() + pph.ResetContext(&pbinFailingContext{PatriciaContext: ms, method: tc.method, skip: tc.skip}) + upd := WrapKeyUpdates(t, ModeDirect, pbinKeyHasher(), touch.plainKeys, touch.updates) + _, err := pph.Process(context.Background(), upd, "", nil, WarmupConfig{}) + require.ErrorIs(t, err, errPBinTestContext) + }) + } +} diff --git a/execution/commitment/pbin_unfold_test.go b/execution/commitment/pbin_unfold_test.go index 44c7b8a6e91..2a83b7decd6 100644 --- a/execution/commitment/pbin_unfold_test.go +++ b/execution/commitment/pbin_unfold_test.go @@ -40,8 +40,15 @@ func pbinTestSpecCell(t *testing.T, kind pbinNodeKind, spec string) pbinCell { c := pbinTestEmptyCell() c.kind = kind c.prefix = pbinTestPathFromBits(t, pbinTestBitSpec(t, spec)) - c.hash = common.Hash{0xB1, byte(len(spec))} - c.hashLen = length.Hash + switch kind { + case pbinNodeLeaf: + // A stored leaf always names a plain key; a record without one is rejected. + c.storageAddrLen = length.Addr + length.Hash + c.storageAddr[0], c.storageAddr[1] = 0xB1, byte(len(spec)) + case pbinNodeBranch: + c.hash = common.Hash{0xB1, byte(len(spec))} + c.hashLen = length.Hash + } return c } @@ -53,6 +60,33 @@ func pbinTestPutRecord(t *testing.T, ms *MockState, path pbinBitpath, cells [2]p require.NoError(t, ms.PutBranch(pbinEncodeBitPath(&path), bytes.Clone(rec), nil)) } +func pbinTestPutRootCell(t *testing.T, ms *MockState, c pbinCell) { + t.Helper() + rec, err := pbinAppendCell(nil, &c) + require.NoError(t, err) + require.NoError(t, ms.PutBranch(pbinRootKey, rec, nil)) +} + +// pbinTestPutTopRecord seeds a node record at the empty path together with the +// root cell that names it — the pair a stored tree always writes. +func pbinTestPutTopRecord(t *testing.T, ms *MockState, cells [2]pbinCell) { + t.Helper() + pbinTestPutRecord(t, ms, pbinBitpath{}, cells) + pbinTestPutRootCell(t, ms, pbinTestSpecCell(t, pbinNodeBranch, "")) +} + +// pbinTestUnfoldStep opens one more row, loading the root cell first when the +// grid is still empty. +func pbinTestUnfoldStep(t *testing.T, pph *PBinPatriciaHashed, probe *pbinBitpath) { + t.Helper() + u := pph.needUnfolding(probe) + if u.action == pbinUnfoldRoot { + require.NoError(t, pph.unfold(probe, u)) + u = pph.needUnfolding(probe) + } + require.NoError(t, pph.unfold(probe, u)) +} + // TestPBinNeedUnfolding guards H9: the hex engine's cpl+1 hides a terminator // nibble, so the binary engine states each outcome instead. What matters is that // "the probe agrees with the whole prefix" and "the probe leaves the prefix @@ -69,10 +103,10 @@ func TestPBinNeedUnfolding(t *testing.T) { want pbinUnfolding }{ { - name: "an unchecked empty root reads the root record", + name: "an unchecked empty root reads the root cell record", root: pbinTestEmptyCell(), probe: "1010", - want: pbinUnfolding{action: pbinUnfoldRecord}, + want: pbinUnfolding{action: pbinUnfoldRoot}, }, { name: "a checked empty root needs nothing", @@ -138,13 +172,13 @@ func TestPBinNeedUnfoldingSelectsCellByBranchBit(t *testing.T) { t.Parallel() pph, ms := pbinTestEngine(t) - pbinTestPutRecord(t, ms, pbinBitpath{}, [2]pbinCell{ + pbinTestPutTopRecord(t, ms, [2]pbinCell{ pbinTestSpecCell(t, pbinNodeLeaf, "000"), pbinTestSpecCell(t, pbinNodeBranch, "111"), }) probe := pbinTestPathFromBits(t, pbinTestBitSpec(t, "0000")) - require.NoError(t, pph.unfold(&probe, pph.needUnfolding(&probe))) + pbinTestUnfoldStep(t, pph, &probe) require.Equal(t, 1, pph.grid.activeRows) require.Equal(t, int16(1), pph.grid.depths[0]) @@ -182,11 +216,11 @@ func TestPBinUnfoldEmptyPrefixBranchRecord(t *testing.T) { childPath := pbinTestPathFromBits(t, pbinTestBitSpec(t, "1")) pph, ms := pbinTestEngine(t) - pbinTestPutRecord(t, ms, pbinBitpath{}, rootCells) + pbinTestPutTopRecord(t, ms, rootCells) pbinTestPutRecord(t, ms, childPath, childCells) probe := pbinTestPathFromBits(t, pbinTestBitSpec(t, "1101")) - require.NoError(t, pph.unfold(&probe, pph.needUnfolding(&probe))) + pbinTestUnfoldStep(t, pph, &probe) require.Equal(t, 1, pph.grid.activeRows) u := pph.needUnfolding(&probe) @@ -209,13 +243,13 @@ func TestPBinUnfoldEmptyPrefixBranchRecordMissing(t *testing.T) { t.Parallel() pph, ms := pbinTestEngine(t) - pbinTestPutRecord(t, ms, pbinBitpath{}, [2]pbinCell{ + pbinTestPutTopRecord(t, ms, [2]pbinCell{ pbinTestSpecCell(t, pbinNodeLeaf, "010"), pbinTestSpecCell(t, pbinNodeBranch, ""), }) probe := pbinTestPathFromBits(t, pbinTestBitSpec(t, "1101")) - require.NoError(t, pph.unfold(&probe, pph.needUnfolding(&probe))) + pbinTestUnfoldStep(t, pph, &probe) require.ErrorIs(t, pph.unfold(&probe, pph.needUnfolding(&probe)), errPBinMissingBranch) } @@ -226,7 +260,7 @@ func TestPBinUnfoldEmptyRoot(t *testing.T) { probe := pbinPathFromBytes(pbinTreeKeyAccount(pbinOracleAddr(1), pbinBasicDataLeafKey)) u := pph.needUnfolding(&probe) - require.Equal(t, pbinUnfolding{action: pbinUnfoldRecord}, u) + require.Equal(t, pbinUnfolding{action: pbinUnfoldRoot}, u) require.NoError(t, pph.unfold(&probe, u)) require.Equal(t, 0, pph.grid.activeRows) @@ -345,14 +379,14 @@ func TestPBinUnfoldDeletedSubtree(t *testing.T) { childPath := pbinTestPathFromBits(t, pbinTestBitSpec(t, "1")) pph, ms := pbinTestEngine(t) - pbinTestPutRecord(t, ms, pbinBitpath{}, [2]pbinCell{ + pbinTestPutTopRecord(t, ms, [2]pbinCell{ pbinTestSpecCell(t, pbinNodeLeaf, "010"), pbinTestSpecCell(t, pbinNodeBranch, ""), }) pbinTestPutRecord(t, ms, childPath, childCells) probe := pbinTestPathFromBits(t, pbinTestBitSpec(t, "1101")) - require.NoError(t, pph.unfold(&probe, pph.needUnfolding(&probe))) + pbinTestUnfoldStep(t, pph, &probe) pph.grid.touchMap[0], pph.grid.afterMap[0] = 0b10, 0 require.NoError(t, pph.unfold(&probe, pph.needUnfolding(&probe))) diff --git a/execution/commitment/pbin_variant_test.go b/execution/commitment/pbin_variant_test.go index 6038ff48b67..9f583fb80c9 100644 --- a/execution/commitment/pbin_variant_test.go +++ b/execution/commitment/pbin_variant_test.go @@ -72,6 +72,56 @@ func TestPBinResetReuse(t *testing.T) { require.Equal(t, want, pbinTestProcess(t, fresh, corpus.plainKeys, corpus.updates), "fresh engine over the same state agrees") } +// TestPBinResetReuseTouchingOneKey is the reuse case a re-run of the whole +// corpus hides: after Reset the engine must find the leaves it is not told about +// again. A tree confined to one zone has a non-empty root prefix, so its top +// record is not at the zero-bit key and only the root cell record names it. +func TestPBinResetReuseTouchingOneKey(t *testing.T) { + t.Parallel() + + addr := pbinOracleAddr(71) + corpus := new(pbinTestCorpus). + storage(addr, pbinOracleSlot(256), 0x01). + storage(addr, pbinOracleSlot(257), 0x02) + + pph, ms := pbinTestEngine(t) + require.NoError(t, ms.applyPlainUpdates(corpus.plainKeys, corpus.updates)) + want := corpus.oracleRoot(t) + require.Equal(t, want, pbinTestProcess(t, pph, corpus.plainKeys, corpus.updates)) + + touchOne := new(pbinTestCorpus).storage(addr, pbinOracleSlot(257), 0x02) + + pph.Reset() + require.Equal(t, want, pbinTestProcess(t, pph, touchOne.plainKeys, touchOne.updates), + "the untouched sibling must survive a reset") + + fresh := NewPBinPatriciaHashed(ms) + require.Equal(t, want, pbinTestProcess(t, fresh, touchOne.plainKeys, touchOne.updates)) +} + +// TestPBinResetReuseSingleLeaf covers the shape that writes no node record at +// all: a one-leaf tree lives entirely in the root cell record. +func TestPBinResetReuseSingleLeaf(t *testing.T) { + t.Parallel() + + addr := pbinOracleAddr(72) + first := new(pbinTestCorpus).storage(addr, pbinOracleSlot(1000), 0x01) + second := new(pbinTestCorpus).storage(pbinOracleAddr(73), pbinOracleSlot(2000), 0x02) + + pph, ms := pbinTestEngine(t) + require.NoError(t, ms.applyPlainUpdates(first.plainKeys, first.updates)) + require.Equal(t, first.oracleRoot(t), pbinTestProcess(t, pph, first.plainKeys, first.updates)) + + require.NoError(t, ms.applyPlainUpdates(second.plainKeys, second.updates)) + both := new(pbinTestCorpus). + storage(addr, pbinOracleSlot(1000), 0x01). + storage(pbinOracleAddr(73), pbinOracleSlot(2000), 0x02) + + pph.Reset() + require.Equal(t, both.oracleRoot(t), pbinTestProcess(t, pph, second.plainKeys, second.updates), + "the leaf that was the whole tree must survive a reset") +} + // TestPBinResetClearsTrieState is the state-level half of the reuse contract: // Reset leaves the engine indistinguishable from a new one but keeps the // context, which the Trie interface hands over separately. diff --git a/execution/commitment/pbin_verify_test.go b/execution/commitment/pbin_verify_test.go index 4276054a239..c57cf7b1d74 100644 --- a/execution/commitment/pbin_verify_test.go +++ b/execution/commitment/pbin_verify_test.go @@ -46,12 +46,13 @@ var ( errPBinVerifyPosition = errors.New("pbin verify: leaf sits where its key does not") ) -// recordPaths decodes the key of every live record. A record put with no data is -// a deletion and names no node. +// recordPaths decodes the key of every live node record. A record put with no +// data is a deletion and names no node; the root cell record is keyed outside the +// bit-path space and is read through rootCell. func (v *pbinVerifier) recordPaths() ([]pbinBitpath, error) { paths := make([]pbinBitpath, 0, len(v.ms.cm)) for key, data := range v.ms.cm { - if len(data) == 0 { + if len(data) == 0 || key == string(pbinRootKey) { continue } p, err := pbinDecodeBitPath([]byte(key)) @@ -93,14 +94,36 @@ func (v *pbinVerifier) rootPath() (pbinBitpath, error) { return roots[0], nil } -// recomputeRoot hashes the whole record set bottom up. The root node starts at -// depth 0, so its prefix is its entire path. +// rootCell decodes the record holding the root cell, the entry point the rest of +// the record set hangs off. +func (v *pbinVerifier) rootCell() (pbinCell, error) { + var c pbinCell + data, _, err := v.ms.Branch(pbinRootKey) + if err != nil { + return c, err + } + if len(data) == 0 { + return c, errPBinVerifyNoRecords + } + pos, err := pbinDecodeCell(data, 0, &c) + if err != nil { + return c, fmt.Errorf("pbin verify: root cell: %w", err) + } + if pos != len(data) { + return c, fmt.Errorf("pbin verify: %d trailing bytes after the root cell", len(data)-pos) + } + return c, nil +} + +// recomputeRoot hashes the whole record set bottom up, entering at the stored +// root cell rather than guessing which record has no ancestor. func (v *pbinVerifier) recomputeRoot() ([]byte, error) { - root, err := v.rootPath() + c, err := v.rootCell() if err != nil { return nil, err } - return v.nodeHash(&root, &root) + var start pbinBitpath + return v.cellHash(&start, &c) } func (v *pbinVerifier) nodeHash(nodePath, prefix *pbinBitpath) ([]byte, error) { @@ -196,11 +219,22 @@ func (v *pbinVerifier) recordAt(nodePath *pbinBitpath) ([2]pbinCell, error) { // treeKey(plainKey). A slot routed into the wrong zone still builds a tree that // hashes consistently, so position against derivation is what catches it (H8). func (v *pbinVerifier) checkPlainKeys() (int, error) { - paths, err := v.recordPaths() + root, err := v.rootCell() if err != nil { return 0, err } leaves := 0 + if root.kind == pbinNodeLeaf { + var start pbinBitpath + if err = v.checkLeafPosition(&start, &root); err != nil { + return 0, err + } + leaves++ + } + paths, err := v.recordPaths() + if err != nil { + return 0, err + } for _, path := range paths { cells, err := v.recordAt(&path) if err != nil { @@ -213,23 +247,30 @@ func (v *pbinVerifier) checkPlainKeys() (int, error) { } start := path start.appendBit(uint64(bit)) - key, _, err := v.leaf(&start, c) - if err != nil { + if err = v.checkLeafPosition(&start, c); err != nil { return 0, err } - want, err := pbinVerifyDerivedKey(c, key) - if err != nil { - return 0, err - } - if !bytes.Equal(want, key) { - return 0, fmt.Errorf("%w: stored at %x, derives %x", errPBinVerifyPosition, key, want) - } leaves++ } } return leaves, nil } +func (v *pbinVerifier) checkLeafPosition(start *pbinBitpath, c *pbinCell) error { + key, _, err := v.leaf(start, c) + if err != nil { + return err + } + want, err := pbinVerifyDerivedKey(c, key) + if err != nil { + return err + } + if !bytes.Equal(want, key) { + return fmt.Errorf("%w: stored at %x, derives %x", errPBinVerifyPosition, key, want) + } + return nil +} + // pbinVerifyDerivedKey re-derives a leaf's tree key from its plain key. The // sub-index comes from the stored key because the two account-header leaves share // one address; which of the two it is, the record does not say. @@ -352,17 +393,31 @@ func TestPBinVerifyRootRecordIsUnique(t *testing.T) { require.Equal(t, pph.grid.root.prefix, root, "the root record's path is the root node's prefix") } -// TestPBinVerifyEmptyStateHasNoRecords checks the recompute refuses to invent a -// tree: a root that is a bare leaf writes no record at all. -func TestPBinVerifyEmptyStateHasNoRecords(t *testing.T) { +// TestPBinVerifySingleLeafRoot checks the one shape that writes no node record: +// a root that is a bare leaf is still recoverable, because the root cell record +// carries it. +func TestPBinVerifySingleLeafRoot(t *testing.T) { t.Parallel() corpus := new(pbinTestCorpus).storage(pbinOracleAddr(55), pbinOracleSlot(1000), 0x01) pph, ms := pbinTestEngine(t) require.NoError(t, ms.applyPlainUpdates(corpus.plainKeys, corpus.updates)) - pbinTestProcess(t, pph, corpus.plainKeys, corpus.updates) + root := pbinTestProcess(t, pph, corpus.plainKeys, corpus.updates) v := &pbinVerifier{t: t, ms: ms} + paths, err := v.recordPaths() + require.NoError(t, err) + require.Empty(t, paths, "a bare-leaf root has no node record") + + pbinTestVerifyRecords(t, ms, root, 1) +} + +// TestPBinVerifyEmptyStateHasNoRecords checks the recompute refuses to invent a +// tree out of a state nothing was ever written to. +func TestPBinVerifyEmptyStateHasNoRecords(t *testing.T) { + t.Parallel() + + v := &pbinVerifier{t: t, ms: NewMockState(t)} _, err := v.recomputeRoot() require.ErrorIs(t, err, errPBinVerifyNoRecords) } From e8e19e758a9c873fb577a71fa867ade0f9eb34e9 Mon Sep 17 00:00:00 2001 From: awskii Date: Wed, 29 Jul 2026 20:29:12 +0700 Subject: [PATCH 16/56] fix: address code review findings --- execution/commitment/pbin_patricia_hashed.go | 26 ++++++++++++++++--- execution/commitment/pbin_process_test.go | 27 ++++++++++++++++++++ execution/commitment/pbin_variant_test.go | 25 +++++++++++++++++- 3 files changed, 73 insertions(+), 5 deletions(-) diff --git a/execution/commitment/pbin_patricia_hashed.go b/execution/commitment/pbin_patricia_hashed.go index 76b3c654b8c..bd6de284289 100644 --- a/execution/commitment/pbin_patricia_hashed.go +++ b/execution/commitment/pbin_patricia_hashed.go @@ -302,6 +302,13 @@ func (pph *PBinPatriciaHashed) RootHash() ([]byte, error) { if pph.grid.activeRows != 0 { return nil, fmt.Errorf("pbin: root hash requested with %d rows still open", pph.grid.activeRows) } + // A run that touches no key never descends, so nothing has pulled the stored + // root in yet and an untouched grid would report the empty tree. + if !pph.rootChecked { + if err := pph.loadRoot(); err != nil { + return nil, err + } + } var path pbinBitpath hash, err := pph.cellHash(&pph.grid.root, &path) if err != nil { @@ -769,19 +776,30 @@ func (pph *PBinPatriciaHashed) cellHash(c *pbinCell, path *pbinBitpath) (common. // loadCellState fills a leaf cell whose plain key arrived from a record and // whose value therefore did not. +// The leaf is already in the tree, so an absent read means it has to go, which +// EIP-8297 does not define. Applying it would hash a zero-valued leaf and return +// a root with no error. func (pph *PBinPatriciaHashed) loadCellState(c *pbinCell) error { if c.accountAddrLen > 0 && !c.loaded.account() { - update, err := pph.ctx.Account(c.accountAddr[:c.accountAddrLen]) + plainKey := c.accountAddr[:c.accountAddrLen] + update, err := pph.ctx.Account(plainKey) if err != nil { - return fmt.Errorf("pbin: read account %x: %w", c.accountAddr[:c.accountAddrLen], err) + return fmt.Errorf("pbin: read account %x: %w", plainKey, err) + } + if update.Deleted() { + return fmt.Errorf("%w: %x", errPBinDeleteUnsupported, plainKey) } c.setFromUpdate(update) c.loaded = c.loaded.addFlag(cellLoadAccount) } if c.storageAddrLen > 0 && !c.loaded.storage() { - update, err := pph.ctx.Storage(c.storageAddr[:c.storageAddrLen]) + plainKey := c.storageAddr[:c.storageAddrLen] + update, err := pph.ctx.Storage(plainKey) if err != nil { - return fmt.Errorf("pbin: read storage %x: %w", c.storageAddr[:c.storageAddrLen], err) + return fmt.Errorf("pbin: read storage %x: %w", plainKey, err) + } + if update.Deleted() { + return fmt.Errorf("%w: %x", errPBinDeleteUnsupported, plainKey) } c.setFromUpdate(update) c.loaded = c.loaded.addFlag(cellLoadStorage) diff --git a/execution/commitment/pbin_process_test.go b/execution/commitment/pbin_process_test.go index b49a259bcbb..e3f0cdf8656 100644 --- a/execution/commitment/pbin_process_test.go +++ b/execution/commitment/pbin_process_test.go @@ -335,6 +335,33 @@ func TestPBinProcessRejectsDeletedLeaf(t *testing.T) { require.ErrorIs(t, err, errPBinDeleteUnsupported) } +// TestPBinProcessRejectsDeletedSibling is the same hazard reached through the +// fold rather than the update stream: the vanished leaf is never touched, so it +// is rehydrated from its branch record and hashed with whatever the state read +// returns. Applying an absent read there would hash a zero-valued leaf and +// return a root with no error. +func TestPBinProcessRejectsDeletedSibling(t *testing.T) { + t.Parallel() + + addr := pbinOracleAddr(24) + corpus := new(pbinTestCorpus). + storage(addr, pbinOracleSlot(256), 0x01). + storage(addr, pbinOracleSlot(257), 0x02) + + pph, ms := pbinTestEngine(t) + require.NoError(t, ms.applyPlainUpdates(corpus.plainKeys, corpus.updates)) + pbinTestProcess(t, pph, corpus.plainKeys, corpus.updates) + + gone := new(pbinTestCorpus).storage(addr, pbinOracleSlot(256), 0x01) + require.NoError(t, ms.applyPlainUpdates(gone.plainKeys, []Update{{Flags: DeleteUpdate}})) + + touched := new(pbinTestCorpus).storage(addr, pbinOracleSlot(257), 0x02) + pph.Reset() + upd := WrapKeyUpdates(t, ModeDirect, pbinKeyHasher(), touched.plainKeys, touched.updates) + _, err := pph.Process(context.Background(), upd, "", nil, WarmupConfig{}) + require.ErrorIs(t, err, errPBinDeleteUnsupported) +} + // TestPBinProcessRepeatedKeyKeepsOneLeaf checks a stem touched twice in one run // still holds a single leaf, so the second visit updates rather than splits. func TestPBinProcessRepeatedKeyKeepsOneLeaf(t *testing.T) { diff --git a/execution/commitment/pbin_variant_test.go b/execution/commitment/pbin_variant_test.go index 9f583fb80c9..5186cc27910 100644 --- a/execution/commitment/pbin_variant_test.go +++ b/execution/commitment/pbin_variant_test.go @@ -18,6 +18,7 @@ package commitment import ( "bytes" + "context" "testing" "github.com/stretchr/testify/require" @@ -145,10 +146,32 @@ func TestPBinResetClearsTrieState(t *testing.T) { require.False(t, pph.rootTouched) require.False(t, pph.rootPresent) require.Same(t, ms, pph.ctx) +} + +// TestPBinRootHashAfterResetLoadsStoredRoot pins the zero-update path the domain +// layer takes: it asks for the root without processing anything, so RootHash has +// to reach the stored tree rather than report the empty-tree hash. +func TestPBinRootHashAfterResetLoadsStoredRoot(t *testing.T) { + t.Parallel() + + corpus := new(pbinTestCorpus). + storage(pbinOracleAddr(81), pbinOracleSlot(256), 0x01). + storage(pbinOracleAddr(81), pbinOracleSlot(257), 0x02) + + pph, ms := pbinTestEngine(t) + require.NoError(t, ms.applyPlainUpdates(corpus.plainKeys, corpus.updates)) + want := corpus.oracleRoot(t) + require.Equal(t, want, pbinTestProcess(t, pph, corpus.plainKeys, corpus.updates)) + pph.Reset() root, err := pph.RootHash() require.NoError(t, err) - require.Equal(t, make([]byte, 32), root) + require.Equal(t, want, root) + + fresh := NewPBinPatriciaHashed(ms) + empty, err := fresh.Process(context.Background(), WrapKeyUpdates(t, ModeDirect, pbinKeyHasher(), nil, nil), "", nil, WarmupConfig{}) + require.NoError(t, err) + require.Equal(t, want, empty, "a run with no updates must not shrink the tree to empty") } // TestPBinResetContext swaps the state under a released-and-reused engine. From 60b621b69b5a9d863080d88f4e59317ea5f21f99 Mon Sep 17 00:00:00 2001 From: awskii Date: Wed, 29 Jul 2026 21:10:11 +0700 Subject: [PATCH 17/56] fix: address codex review findings --- .../commitmentdb/commitment_context.go | 5 +++ .../commitmentdb/commitment_context_test.go | 14 ++++++++ execution/commitment/pbin_keys.go | 18 ++++++++-- execution/commitment/pbin_keys_test.go | 35 +++++++++++++++++++ 4 files changed, 70 insertions(+), 2 deletions(-) diff --git a/execution/commitment/commitmentdb/commitment_context.go b/execution/commitment/commitmentdb/commitment_context.go index ace94f09d78..fb23ead2d4a 100644 --- a/execution/commitment/commitmentdb/commitment_context.go +++ b/execution/commitment/commitmentdb/commitment_context.go @@ -222,6 +222,11 @@ func NewSharedDomainsCommitmentContext(sd sd, mode commitment.Mode, tmpDir strin if variant == "" { variant = commitment.VariantHexPatriciaTrie } + if variant == commitment.VariantBinPatriciaTrie { + // encodeCommitmentState/restorePatriciaState are hex-only, so this variant + // would fail after a full Process instead of at configuration time. + panic("commitment variant " + string(variant) + " has no state save/restore and cannot back a domain commitment context") + } ctx := &SharedDomainsCommitmentContext{ sharedDomains: sd, tmpDir: tmpDir, diff --git a/execution/commitment/commitmentdb/commitment_context_test.go b/execution/commitment/commitmentdb/commitment_context_test.go index 11d04181f11..be8216501cf 100644 --- a/execution/commitment/commitmentdb/commitment_context_test.go +++ b/execution/commitment/commitmentdb/commitment_context_test.go @@ -6,6 +6,7 @@ import ( "testing" "github.com/erigontech/erigon/db/kv" + "github.com/erigontech/erigon/execution/commitment" "github.com/stretchr/testify/require" ) @@ -83,3 +84,16 @@ func Test_TrieContext_BranchCopiesData(t *testing.T) { branch[1] = 8 require.Equal(t, []byte{9, 2, 3}, reader.branchData) } + +// Test_NewSharedDomainsCommitmentContext_RejectsBinVariant pins that a variant +// without commitment state save/restore is refused at construction rather than +// mid-block, where encodeCommitmentState would fail after a full Process. +func Test_NewSharedDomainsCommitmentContext_RejectsBinVariant(t *testing.T) { + t.Parallel() + + cfg := commitment.DefaultTrieConfig() + cfg.Variant = commitment.VariantBinPatriciaTrie + require.Panics(t, func() { + NewSharedDomainsCommitmentContext(nil, commitment.ModeDirect, t.TempDir(), cfg) + }) +} diff --git a/execution/commitment/pbin_keys.go b/execution/commitment/pbin_keys.go index 3e0bfffdb66..1e2978e825f 100644 --- a/execution/commitment/pbin_keys.go +++ b/execution/commitment/pbin_keys.go @@ -18,6 +18,7 @@ package commitment import ( "fmt" + "sync" keccak "github.com/erigontech/fastkeccak" @@ -86,9 +87,22 @@ func pbinTreeKeyStorage(addr, slot []byte) []byte { // BASIC_DATA for an account, the slot's own leaf for storage. The CODE_HASH // sibling shares the stem and is written by the engine during the same visit, // so it needs no key of its own here. +// +// The digest cache is borrowed per call rather than captured: Updates.NewEmpty +// copies the hasher value, so a captured cache would be written by two buffers +// hashing concurrently. Every hit is validated against the address it was built +// from, so borrowing another goroutine's cache stays correct. func pbinKeyHasher() keyHasher { - var c pbinDigestCache - return c.treeKey + var pool sync.Pool + return func(plainKey []byte) []byte { + c, _ := pool.Get().(*pbinDigestCache) + if c == nil { + c = new(pbinDigestCache) + } + key := c.treeKey(plainKey) + pool.Put(c) + return key + } } // pbinDigestCache memoizes the two hash-derived key components across a run of diff --git a/execution/commitment/pbin_keys_test.go b/execution/commitment/pbin_keys_test.go index 5f7b809ae73..e8fe2168f29 100644 --- a/execution/commitment/pbin_keys_test.go +++ b/execution/commitment/pbin_keys_test.go @@ -19,8 +19,10 @@ package commitment import ( "encoding/binary" "encoding/hex" + "sync" "testing" + "github.com/stretchr/testify/assert" "github.com/stretchr/testify/require" "golang.org/x/crypto/sha3" ) @@ -219,6 +221,39 @@ func TestPBinKeyHasherRejectsMalformedPlainKey(t *testing.T) { require.Panics(t, func() { hasher(nil) }) } +// TestPBinKeyHasherSharedAcrossBuffers hashes through two Updates buffers that +// share one hasher value (Updates.NewEmpty copies it) from two goroutines. Run +// under -race this fails if the hasher keeps a cache the copies can both write. +func TestPBinKeyHasherSharedAcrossBuffers(t *testing.T) { + t.Parallel() + + addrs := [][]byte{ + pbinTestAddr(t, "0102030405060708090a0b0c0d0e0f1011121314"), + pbinTestAddr(t, "cafebabe000000000000000000000000deadbeef"), + } + slots := []uint64{0, 64, 256, 1000} + + base := NewUpdates(ModeDirect, t.TempDir(), pbinKeyHasher()) + clone := base.NewEmpty() + + var wg sync.WaitGroup + for _, buf := range []*Updates{base, clone} { + wg.Go(func() { + for range 50 { + for _, addr := range addrs { + assert.Equal(t, pbinTreeKeyAccount(addr, pbinBasicDataLeafKey), buf.hashKey(addr)) + for _, slot := range slots { + plainKey := pbinTestConcat(addr, pbinTestSlot(slot)) + assert.Equal(t, pbinTreeKeyStorage(addr, pbinTestSlot(slot)), buf.hashKey(plainKey), + "addr %x slot %d", addr, slot) + } + } + } + }) + } + wg.Wait() +} + // TestPBinDigestCacheMatchesFreshDerivation drives one hasher across interleaved // addresses and slot groups: a cache entry kept past its address or tree index // would silently place a leaf under the wrong stem. From 619065cadd4cd7ddf5b7b59c1e2a79e497796782 Mon Sep 17 00:00:00 2001 From: awskii Date: Wed, 29 Jul 2026 21:35:05 +0700 Subject: [PATCH 18/56] execution/commitment: fold the storage tree-index shift into groupDigest pbinSplitSlot took a pointer and returned a 32-byte array by value, so the copy it avoided on the way in was paid on the way out. The array it built existed only to be copied again into the digest buffer. The tree index is slot>>8, i.e. a zero byte followed by the slot's top 31 bytes, so groupDigest writes it straight into the buffer it hashes. Drops one stack array and two copies per storage-key derivation, and narrows the cache key to the 31 bytes that actually discriminate. --- execution/commitment/pbin_keys.go | 28 +++++++++++----------------- 1 file changed, 11 insertions(+), 17 deletions(-) diff --git a/execution/commitment/pbin_keys.go b/execution/commitment/pbin_keys.go index 1e2978e825f..3d55b6cc7cf 100644 --- a/execution/commitment/pbin_keys.go +++ b/execution/commitment/pbin_keys.go @@ -115,7 +115,7 @@ type pbinDigestCache struct { stem [32]byte valid bool - groupIndex [32]byte + groupIndex [31]byte groupHash [32]byte groupValid bool @@ -133,14 +133,18 @@ func (c *pbinDigestCache) stemDigest(addr32 *[32]byte) *[32]byte { return &c.stem } -func (c *pbinDigestCache) groupDigest(addr32, treeIndex *[32]byte) *[32]byte { - if c.groupValid && c.addr32 == *addr32 && c.groupIndex == *treeIndex { +// groupDigest hashes addr32 || tree_index, where tree_index is slot>>8 as a +// 32-byte big-endian value: a zero byte followed by the slot's top 31 bytes. +func (c *pbinDigestCache) groupDigest(addr32, slot32 *[32]byte) *[32]byte { + idx := (*[31]byte)(slot32[:31]) + if c.groupValid && c.addr32 == *addr32 && c.groupIndex == *idx { return &c.groupHash } copy(c.buf[:32], addr32[:]) - copy(c.buf[32:], treeIndex[:]) + c.buf[32] = 0 + copy(c.buf[33:], idx[:]) c.groupHash = keccak.Sum256(c.buf[:]) - c.groupIndex = *treeIndex + c.groupIndex = *idx c.groupValid = true return &c.groupHash } @@ -156,12 +160,10 @@ func (c *pbinDigestCache) storageKey(addr, slot []byte) []byte { if pbinSlotInHeader(&slot32) { return pbinTreeKey(pbinAccountZone, c.stemDigest(&addr32)[:], pbinHeaderStorageOffset+slot32[31]) } - treeIndex, subIndex := pbinSplitSlot(&slot32) - var position [64]byte copy(position[:32], c.stemDigest(&addr32)[:]) - copy(position[32:], c.groupDigest(&addr32, &treeIndex)[:]) - return pbinTreeKey(pbinStorageZone, position[:], subIndex) + copy(position[32:], c.groupDigest(&addr32, &slot32)[:]) + return pbinTreeKey(pbinStorageZone, position[:], slot32[31]) } func (c *pbinDigestCache) treeKey(plainKey []byte) []byte { @@ -192,11 +194,3 @@ func pbinSlotInHeader(slot *[32]byte) bool { } return slot[31] < pbinCodeOffset-pbinHeaderStorageOffset } - -// pbinSplitSlot divides a slot into its storage group and position within it. -// STEM_SUBTREE_WIDTH is 256, so the division is a one-byte shift and the -// sub-index is the raw low byte — which is what co-locates adjacent slots. -func pbinSplitSlot(slot *[32]byte) (treeIndex [32]byte, subIndex byte) { - copy(treeIndex[1:], slot[:31]) - return treeIndex, slot[31] -} From e9f93baec520f05fae2f9e9eb9d968c0ab496a0d Mon Sep 17 00:00:00 2001 From: awskii Date: Wed, 29 Jul 2026 22:05:53 +0700 Subject: [PATCH 19/56] execution/commitment: compare binary and hex engine shape over one corpus MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Drives both engines over the same updates and reports branch-record count, record bytes and path depth in key bits, plus the binary engine's split counters. Roots differ by construction, so the test asserts they disagree. Adds a stem co-location test: slots sharing a tree_index differ only in the sub-index byte, a path random 32-byte slots never reach. The hex engine needs accountKeyLen=20 here — the existing corpora use one-byte stand-in addresses, which the binary engine cannot consume at all since it derives its zone from the plain-key length. --- .../commitment/pbin_vs_hex_compare_test.go | 216 ++++++++++++++++++ 1 file changed, 216 insertions(+) create mode 100644 execution/commitment/pbin_vs_hex_compare_test.go diff --git a/execution/commitment/pbin_vs_hex_compare_test.go b/execution/commitment/pbin_vs_hex_compare_test.go new file mode 100644 index 00000000000..43a068403c8 --- /dev/null +++ b/execution/commitment/pbin_vs_hex_compare_test.go @@ -0,0 +1,216 @@ +package commitment + +import ( + "context" + "fmt" + "math/bits" + "sort" + "testing" + + "github.com/stretchr/testify/require" + + "github.com/erigontech/erigon/common/length" +) + +// Structural comparison of the hex and binary commitment engines over one +// corpus. Roots differ by construction — the trees, keys and node preimages all +// differ — so this measures shape and footprint, not equality. + +type engineShape struct { + name string + root []byte + records int + recordByte int + depthBits []int // path length to each stored branch, in key bits +} + +func (s engineShape) depthStats() (maxD, p50, mean int) { + if len(s.depthBits) == 0 { + return 0, 0, 0 + } + d := append([]int(nil), s.depthBits...) + sort.Ints(d) + sum := 0 + for _, v := range d { + sum += v + } + return d[len(d)-1], d[len(d)/2], sum / len(d) +} + +// hexPathBits converts a HexToCompact-encoded branch key to a path length in +// key bits so the two radices are comparable: one nibble is four bits. +func hexPathBits(compact string) int { + if len(compact) == 0 { + return 0 + } + nibbles := (len(compact)-1)*2 + 1 + if compact[0]&0x10 == 0 { + nibbles-- + } + return nibbles * 4 +} + +func pbinPathBits(key string) int { + p, err := pbinDecodeBitPath([]byte(key)) + if err != nil { + return -1 + } + return int(p.bitLen) +} + +func runHex(t *testing.T, plainKeys [][]byte, updates []Update) engineShape { + t.Helper() + ms := NewMockState(t) + // PBin derives its zone from the plain-key length, so a comparison corpus + // must use real 20-byte addresses; the hex engine has to be told the same. + hph := NewHexPatriciaHashed(length.Addr, ms, DefaultTrieConfig()) + upds := WrapKeyUpdates(t, ModeDirect, KeyToHexNibbleHash, plainKeys, updates) + defer upds.Close() + require.NoError(t, ms.applyPlainUpdates(plainKeys, updates)) + + root, err := hph.Process(context.Background(), upds, "", nil, WarmupConfig{}) + require.NoError(t, err) + + s := engineShape{name: "hex", root: root} + for k, v := range ms.cm { + s.records++ + s.recordByte += len(v) + s.depthBits = append(s.depthBits, hexPathBits(k)) + } + return s +} + +func runPBin(t *testing.T, plainKeys [][]byte, updates []Update) (engineShape, pbinCounters) { + t.Helper() + ms := NewMockState(t) + pph := NewPBinPatriciaHashed(ms) + upds := WrapKeyUpdates(t, ModeDirect, pbinKeyHasher(), plainKeys, updates) + defer upds.Close() + require.NoError(t, ms.applyPlainUpdates(plainKeys, updates)) + + root, err := pph.Process(context.Background(), upds, "", nil, WarmupConfig{}) + require.NoError(t, err) + + s := engineShape{name: "bin", root: root} + for k, v := range ms.cm { + s.records++ + s.recordByte += len(v) + s.depthBits = append(s.depthBits, pbinPathBits(k)) + } + return s, pph.counters +} + +// clusteredCorpus gives every contract slots that share a storage group, which +// is what EIP-8297's raw sub-index co-locates. scatteredCorpus spreads slots so +// no two share a group — the mapping-style access random corpora produce. +func clusteredCorpus(contracts, slotsPer int) ([][]byte, []Update) { + ub := NewUpdateBuilder() + for c := range contracts { + addr := fmt.Sprintf("%040x", c+1) + ub.Balance(addr, uint64(c+1)) + for s := range slotsPer { + ub.Storage(addr, fmt.Sprintf("%064x", 0x100+s), fmt.Sprintf("%064x", s+1)) + } + } + return ub.Build() +} + +func scatteredCorpus(contracts, slotsPer int) ([][]byte, []Update) { + ub := NewUpdateBuilder() + for c := range contracts { + addr := fmt.Sprintf("%040x", c+1) + ub.Balance(addr, uint64(c+1)) + for s := range slotsPer { + // one slot per group: step by STEM_SUBTREE_WIDTH + ub.Storage(addr, fmt.Sprintf("%064x", (s+1)*256), fmt.Sprintf("%064x", s+1)) + } + } + return ub.Build() +} + +func TestPBinVsHexStructure(t *testing.T) { + t.Parallel() + + for _, tc := range []struct { + name string + build func(int, int) ([][]byte, []Update) + }{ + {"clustered", clusteredCorpus}, + {"scattered", scatteredCorpus}, + } { + t.Run(tc.name, func(t *testing.T) { + plainKeys, updates := tc.build(16, 16) + + hex := runHex(t, plainKeys, updates) + bin, counters := runPBin(t, plainKeys, updates) + + require.NotEqual(t, hex.root, bin.root, + "hex and binary trees must not agree on a root; equality means one of them is not the tree it claims to be") + + hMax, hP50, hMean := hex.depthStats() + bMax, bP50, bMean := bin.depthStats() + + t.Logf("corpus=%s accounts=%d storage=%d", tc.name, 16, 16*16) + t.Logf(" %-4s records=%4d bytes=%7d depthBits max=%3d p50=%3d mean=%3d", + hex.name, hex.records, hex.recordByte, hMax, hP50, hMean) + t.Logf(" %-4s records=%4d bytes=%7d depthBits max=%3d p50=%3d mean=%3d", + bin.name, bin.records, bin.recordByte, bMax, bP50, bMean) + t.Logf(" bin/hex records=%.2fx bytes=%.2fx", + float64(bin.records)/float64(hex.records), + float64(bin.recordByte)/float64(hex.recordByte)) + t.Logf(" bin splitsInsidePrefix=%d materializeReads=%d", + counters.splitsInsidePrefix, counters.materializeReads) + }) + } +} + +// TestPBinStemCoLocation pins the storage behaviour that distinguishes +// EIP-8297: slots sharing a tree_index differ only in the last key byte, so +// they hang off one stem. Random 32-byte slots never collide in a group, so +// without a deliberate corpus this path goes untested. +func TestPBinStemCoLocation(t *testing.T) { + t.Parallel() + + addr := make([]byte, 20) + addr[19] = 0xAB + + slotOf := func(n uint64) []byte { + s := make([]byte, 32) + s[31] = byte(n) + s[30] = byte(n >> 8) + return s + } + + var c pbinDigestCache + // slots 256..511 share tree_index 1 and differ only in sub_index. + base := c.storageKey(addr, slotOf(256)) + require.Len(t, base, pbinStorageKeyLength) + + for _, n := range []uint64{257, 300, 511} { + k := c.storageKey(addr, slotOf(n)) + require.Equal(t, base[:pbinStorageKeyLength-1], k[:pbinStorageKeyLength-1], + "slots in one group must share every byte but the sub-index") + require.Equal(t, byte(n%256), k[pbinStorageKeyLength-1], "sub-index is the raw low byte") + } + + // crossing into the next group must change the second digest + next := c.storageKey(addr, slotOf(512)) + require.NotEqual(t, base[33:65], next[33:65], "a new tree_index must move the group digest") + + // a co-located pair shares a long prefix; a cross-group pair does not + sharedBits := commonPrefixBitsOfKeys(base, c.storageKey(addr, slotOf(257))) + crossBits := commonPrefixBitsOfKeys(base, next) + require.Greater(t, sharedBits, crossBits, + "co-located slots must share a longer key prefix than cross-group slots") + t.Logf("co-located slots share %d bits; cross-group share %d bits", sharedBits, crossBits) +} + +func commonPrefixBitsOfKeys(a, b []byte) int { + n := min(len(a), len(b)) + for i := range n { + if a[i] != b[i] { + return i*8 + bits.LeadingZeros8(a[i]^b[i]) + } + } + return n * 8 +} From bb03aab6068b8b246af96a98894765c2293b920e Mon Sep 17 00:00:00 2001 From: awskii Date: Wed, 29 Jul 2026 22:57:06 +0700 Subject: [PATCH 20/56] execution/commitment: check PBin against EIP-8297 reference vectors Vectors exported from the reference implementation in ethereum/execution-specs branch projects/binary-trie. That reference hashes with BLAKE3 and this engine with Keccak-256, so roots and tree-key bodies cannot be compared yet. Two things survive the hash difference and are checked: BASIC_DATA packing, which involves no hash, and the zone/length/sub-index routing, which is positional. The routing vectors cover the header/storage-zone boundary at slots 63 and 64 and a slot of 2**255. --- execution/commitment/pbin_specvectors_test.go | 106 + .../commitment/testdata/eip8297_vectors.json | 4729 +++++++++++++++++ 2 files changed, 4835 insertions(+) create mode 100644 execution/commitment/pbin_specvectors_test.go create mode 100644 execution/commitment/testdata/eip8297_vectors.json diff --git a/execution/commitment/pbin_specvectors_test.go b/execution/commitment/pbin_specvectors_test.go new file mode 100644 index 00000000000..1706b123e2b --- /dev/null +++ b/execution/commitment/pbin_specvectors_test.go @@ -0,0 +1,106 @@ +package commitment + +import ( + "encoding/hex" + "encoding/json" + "os" + "testing" + + "github.com/holiman/uint256" + "github.com/stretchr/testify/require" +) + +// Vectors exported from the EIP-8297 reference implementation in +// ethereum/execution-specs (branch projects/binary-trie). The reference hashes +// with BLAKE3 and this engine with Keccak-256, so every digest-bearing vector — +// the trie roots and the tree-key bodies — cannot be compared directly. What +// survives the hash difference is checked here: the BASIC_DATA packing, which +// involves no hash at all, and the zone/length/sub-index routing, which is +// positional. +type pbinSpecVectors struct { + Meta map[string]string `json:"meta"` + BasicData []struct { + CodeSize uint64 `json:"code_size"` + Nonce uint64 `json:"nonce"` + Balance string `json:"balance"` + Value string `json:"value"` + } `json:"basic_data_vectors"` + Embedding struct { + Address string `json:"address"` + BasicDataKey string `json:"basic_data_key"` + CodeHashKey string `json:"code_hash_key"` + // slot reaches 2**255, so it must not go through float64 + Slots []struct { + Slot json.Number `json:"slot"` + Key string `json:"key"` + } `json:"slots"` + } `json:"embedding_vectors"` +} + +func loadPBinSpecVectors(t *testing.T) pbinSpecVectors { + t.Helper() + raw, err := os.ReadFile("testdata/eip8297_vectors.json") + require.NoError(t, err) + var v pbinSpecVectors + require.NoError(t, json.Unmarshal(raw, &v)) + return v +} + +func mustHex(t *testing.T, s string) []byte { + t.Helper() + b, err := hex.DecodeString(s[2:]) + require.NoError(t, err) + return b +} + +// TestPBinSpecBasicDataVectors is the one fully external check available under a +// different hash: BASIC_DATA packing is pure byte layout. +func TestPBinSpecBasicDataVectors(t *testing.T) { + t.Parallel() + v := loadPBinSpecVectors(t) + require.NotEmpty(t, v.BasicData) + + for _, tc := range v.BasicData { + bal, err := uint256.FromDecimal(tc.Balance) + require.NoError(t, err) + + got, err := pbinEncodeBasicData(tc.Nonce, bal, tc.CodeSize) + require.NoError(t, err) + require.Equal(t, mustHex(t, tc.Value), got[:], + "BASIC_DATA mismatch for code_size=%d nonce=%d balance=%s", tc.CodeSize, tc.Nonce, tc.Balance) + } +} + +// TestPBinSpecKeyRouting checks the positional half of key derivation against +// the reference: which zone a key lands in, how long it is, and which sub-index +// it carries. The 32-byte digest bodies differ by hash and are not compared. +func TestPBinSpecKeyRouting(t *testing.T) { + t.Parallel() + v := loadPBinSpecVectors(t) + addr := mustHex(t, v.Embedding.Address) + require.Len(t, addr, 20) + + var c pbinDigestCache + + header := mustHex(t, v.Embedding.BasicDataKey) + got := c.accountKey(addr, pbinBasicDataLeafKey) + require.Len(t, got, len(header)) + require.Equal(t, header[0], got[0], "account zone byte") + require.Equal(t, header[len(header)-1], got[len(got)-1], "BASIC_DATA sub-index") + + codeHash := mustHex(t, v.Embedding.CodeHashKey) + got = c.accountKey(addr, pbinCodeHashLeafKey) + require.Equal(t, codeHash[len(codeHash)-1], got[len(got)-1], "CODE_HASH sub-index") + + for _, s := range v.Embedding.Slots { + slot, err := uint256.FromDecimal(s.Slot.String()) + require.NoError(t, err, "slot %s", s.Slot) + want := mustHex(t, s.Key) + slotBytes := slot.Bytes32() + + got := c.storageKey(addr, slotBytes[:]) + require.Len(t, got, len(want), "slot %s key length", s.Slot) + require.Equal(t, want[0], got[0], "slot %s zone byte", s.Slot) + require.Equal(t, want[len(want)-1], got[len(got)-1], "slot %s sub-index", s.Slot) + } +} diff --git a/execution/commitment/testdata/eip8297_vectors.json b/execution/commitment/testdata/eip8297_vectors.json new file mode 100644 index 00000000000..93f68d1c7db --- /dev/null +++ b/execution/commitment/testdata/eip8297_vectors.json @@ -0,0 +1,4729 @@ +{ + "meta": { + "source": "execution-specs@ec412acfd (branch eip-8297-tests)", + "hasher": "blake3", + "generator": "export_vectors.py" + }, + "empty_root": "0x0000000000000000000000000000000000000000000000000000000000000000", + "trie_vectors": [ + { + "name": "empty", + "entries": [], + "root": "0x0000000000000000000000000000000000000000000000000000000000000000" + }, + { + "name": "single_account_leaf", + "entries": [ + { + "key": "0x0055bbfb5b05f80543bd662a7b7a74980a2992612798087fa24e1fc86b8e8508e400", + "value": "0x0000000000000000000000000000000000000000000000000000000000000007" + } + ], + "root": "0x22da077ee89a0e6e6259303169fb6c7a3133fafa3033515a355af9c4f9ed5ea0" + }, + { + "name": "one_header_stem_two_leaves", + "entries": [ + { + "key": "0x0055bbfb5b05f80543bd662a7b7a74980a2992612798087fa24e1fc86b8e8508e400", + "value": "0x0000000000000000000000000000000000000000000000000000000000000007" + }, + { + "key": "0x0055bbfb5b05f80543bd662a7b7a74980a2992612798087fa24e1fc86b8e8508e401", + "value": "0x0000000000000000000000000000000000000000000000000000000000000009" + } + ], + "root": "0x3599d1dd23fef634f3845fd935375be8a42169ecc90906632aa8afa2fa380812" + }, + { + "name": "two_accounts", + "entries": [ + { + "key": "0x0055bbfb5b05f80543bd662a7b7a74980a2992612798087fa24e1fc86b8e8508e400", + "value": "0x0000000000000000000000000000000000000000000000000000000000000007" + }, + { + "key": "0x0034bb4f3468340d5f72275da92420aed94db48bbc61823e92f29f14f51590373a00", + "value": "0x0000000000000000000000000000000000000000000000000000000000000008" + } + ], + "root": "0xbcca03773c89779e136f983eba516339660c732d5b0a1ccfc2c725dca29eefbc" + }, + { + "name": "cross_zone_small", + "entries": [ + { + "key": "0x0055bbfb5b05f80543bd662a7b7a74980a2992612798087fa24e1fc86b8e8508e400", + "value": "0x0000000000000000000000000000000000000000000000000000000000000001" + }, + { + "key": "0x0055bbfb5b05f80543bd662a7b7a74980a2992612798087fa24e1fc86b8e8508e401", + "value": "0x0000000000000000000000000000000000000000000000000000000000000002" + }, + { + "key": "0xff55bbfb5b05f80543bd662a7b7a74980a2992612798087fa24e1fc86b8e8508e406f5e5117ba26652e3cbef5ea24cc46f42709eb47be3c46f3a923b68a67f44c264", + "value": "0x0000000000000000000000000000000000000000000000000000000000000003" + }, + { + "key": "0xff55bbfb5b05f80543bd662a7b7a74980a2992612798087fa24e1fc86b8e8508e4f1ce240e3efd0b0855a335ac6b58ea33be1b2afb193608d6d21fd91308dd74bc64", + "value": "0x0000000000000000000000000000000000000000000000000000000000000004" + }, + { + "key": "0x0034bb4f3468340d5f72275da92420aed94db48bbc61823e92f29f14f51590373a00", + "value": "0x0000000000000000000000000000000000000000000000000000000000000005" + } + ], + "root": "0x3fc0f35560e81b2b1bc349decef42eb48a614d8497646e401e7b85abc0b16a30" + }, + { + "name": "zero_value_present", + "entries": [ + { + "key": "0x00a43fb2d7beb12b34786a837467f62b7d6c5dab4ab7eecc89c0b67db76a68292700", + "value": "0x0000000000000000000000000000000000000000000000000000000000000000" + } + ], + "root": "0x343a84978f71225f27f6dbdd2e0dd603a2ae3b83028a907ae0f8f4db262c9d13" + }, + { + "name": "full_header_stem", + "entries": [ + { + "key": "0x004c3a8541d9ec369a9fd4bcdad885cc3b166da70ed6a83066924727d9ff147fce00", + "value": "0x0000000000000000000000000000000000000000000000000000000000000001" + }, + { + "key": "0x004c3a8541d9ec369a9fd4bcdad885cc3b166da70ed6a83066924727d9ff147fce01", + "value": "0x0000000000000000000000000000000000000000000000000000000000000002" + }, + { + "key": "0x004c3a8541d9ec369a9fd4bcdad885cc3b166da70ed6a83066924727d9ff147fce02", + "value": "0x0000000000000000000000000000000000000000000000000000000000000003" + }, + { + "key": "0x004c3a8541d9ec369a9fd4bcdad885cc3b166da70ed6a83066924727d9ff147fce03", + "value": "0x0000000000000000000000000000000000000000000000000000000000000004" + }, + { + "key": "0x004c3a8541d9ec369a9fd4bcdad885cc3b166da70ed6a83066924727d9ff147fce04", + "value": "0x0000000000000000000000000000000000000000000000000000000000000005" + }, + { + "key": "0x004c3a8541d9ec369a9fd4bcdad885cc3b166da70ed6a83066924727d9ff147fce05", + "value": "0x0000000000000000000000000000000000000000000000000000000000000006" + }, + { + "key": "0x004c3a8541d9ec369a9fd4bcdad885cc3b166da70ed6a83066924727d9ff147fce06", + "value": "0x0000000000000000000000000000000000000000000000000000000000000007" + }, + { + "key": "0x004c3a8541d9ec369a9fd4bcdad885cc3b166da70ed6a83066924727d9ff147fce07", + "value": "0x0000000000000000000000000000000000000000000000000000000000000008" + }, + { + "key": "0x004c3a8541d9ec369a9fd4bcdad885cc3b166da70ed6a83066924727d9ff147fce08", + "value": "0x0000000000000000000000000000000000000000000000000000000000000009" + }, + { + "key": "0x004c3a8541d9ec369a9fd4bcdad885cc3b166da70ed6a83066924727d9ff147fce09", + "value": "0x000000000000000000000000000000000000000000000000000000000000000a" + }, + { + "key": "0x004c3a8541d9ec369a9fd4bcdad885cc3b166da70ed6a83066924727d9ff147fce0a", + "value": "0x000000000000000000000000000000000000000000000000000000000000000b" + }, + { + "key": "0x004c3a8541d9ec369a9fd4bcdad885cc3b166da70ed6a83066924727d9ff147fce0b", + "value": "0x000000000000000000000000000000000000000000000000000000000000000c" + }, + { + "key": "0x004c3a8541d9ec369a9fd4bcdad885cc3b166da70ed6a83066924727d9ff147fce0c", + "value": "0x000000000000000000000000000000000000000000000000000000000000000d" + }, + { + "key": "0x004c3a8541d9ec369a9fd4bcdad885cc3b166da70ed6a83066924727d9ff147fce0d", + "value": "0x000000000000000000000000000000000000000000000000000000000000000e" + }, + { + "key": "0x004c3a8541d9ec369a9fd4bcdad885cc3b166da70ed6a83066924727d9ff147fce0e", + "value": "0x000000000000000000000000000000000000000000000000000000000000000f" + }, + { + "key": "0x004c3a8541d9ec369a9fd4bcdad885cc3b166da70ed6a83066924727d9ff147fce0f", + "value": "0x0000000000000000000000000000000000000000000000000000000000000010" + }, + { + "key": "0x004c3a8541d9ec369a9fd4bcdad885cc3b166da70ed6a83066924727d9ff147fce10", + "value": "0x0000000000000000000000000000000000000000000000000000000000000011" + }, + { + "key": "0x004c3a8541d9ec369a9fd4bcdad885cc3b166da70ed6a83066924727d9ff147fce11", + "value": "0x0000000000000000000000000000000000000000000000000000000000000012" + }, + { + "key": "0x004c3a8541d9ec369a9fd4bcdad885cc3b166da70ed6a83066924727d9ff147fce12", + "value": "0x0000000000000000000000000000000000000000000000000000000000000013" + }, + { + "key": "0x004c3a8541d9ec369a9fd4bcdad885cc3b166da70ed6a83066924727d9ff147fce13", + "value": "0x0000000000000000000000000000000000000000000000000000000000000014" + }, + { + "key": "0x004c3a8541d9ec369a9fd4bcdad885cc3b166da70ed6a83066924727d9ff147fce14", + "value": "0x0000000000000000000000000000000000000000000000000000000000000015" + }, + { + "key": "0x004c3a8541d9ec369a9fd4bcdad885cc3b166da70ed6a83066924727d9ff147fce15", + "value": "0x0000000000000000000000000000000000000000000000000000000000000016" + }, + { + "key": "0x004c3a8541d9ec369a9fd4bcdad885cc3b166da70ed6a83066924727d9ff147fce16", + "value": "0x0000000000000000000000000000000000000000000000000000000000000017" + }, + { + "key": "0x004c3a8541d9ec369a9fd4bcdad885cc3b166da70ed6a83066924727d9ff147fce17", + "value": "0x0000000000000000000000000000000000000000000000000000000000000018" + }, + { + "key": "0x004c3a8541d9ec369a9fd4bcdad885cc3b166da70ed6a83066924727d9ff147fce18", + "value": "0x0000000000000000000000000000000000000000000000000000000000000019" + }, + { + "key": "0x004c3a8541d9ec369a9fd4bcdad885cc3b166da70ed6a83066924727d9ff147fce19", + "value": "0x000000000000000000000000000000000000000000000000000000000000001a" + }, + { + "key": "0x004c3a8541d9ec369a9fd4bcdad885cc3b166da70ed6a83066924727d9ff147fce1a", + "value": "0x000000000000000000000000000000000000000000000000000000000000001b" + }, + { + "key": "0x004c3a8541d9ec369a9fd4bcdad885cc3b166da70ed6a83066924727d9ff147fce1b", + "value": "0x000000000000000000000000000000000000000000000000000000000000001c" + }, + { + "key": "0x004c3a8541d9ec369a9fd4bcdad885cc3b166da70ed6a83066924727d9ff147fce1c", + "value": "0x000000000000000000000000000000000000000000000000000000000000001d" + }, + { + "key": "0x004c3a8541d9ec369a9fd4bcdad885cc3b166da70ed6a83066924727d9ff147fce1d", + "value": "0x000000000000000000000000000000000000000000000000000000000000001e" + }, + { + "key": "0x004c3a8541d9ec369a9fd4bcdad885cc3b166da70ed6a83066924727d9ff147fce1e", + "value": "0x000000000000000000000000000000000000000000000000000000000000001f" + }, + { + "key": "0x004c3a8541d9ec369a9fd4bcdad885cc3b166da70ed6a83066924727d9ff147fce1f", + "value": "0x0000000000000000000000000000000000000000000000000000000000000020" + }, + { + "key": "0x004c3a8541d9ec369a9fd4bcdad885cc3b166da70ed6a83066924727d9ff147fce20", + "value": "0x0000000000000000000000000000000000000000000000000000000000000021" + }, + { + "key": "0x004c3a8541d9ec369a9fd4bcdad885cc3b166da70ed6a83066924727d9ff147fce21", + "value": "0x0000000000000000000000000000000000000000000000000000000000000022" + }, + { + "key": "0x004c3a8541d9ec369a9fd4bcdad885cc3b166da70ed6a83066924727d9ff147fce22", + "value": "0x0000000000000000000000000000000000000000000000000000000000000023" + }, + { + "key": "0x004c3a8541d9ec369a9fd4bcdad885cc3b166da70ed6a83066924727d9ff147fce23", + "value": "0x0000000000000000000000000000000000000000000000000000000000000024" + }, + { + "key": "0x004c3a8541d9ec369a9fd4bcdad885cc3b166da70ed6a83066924727d9ff147fce24", + "value": "0x0000000000000000000000000000000000000000000000000000000000000025" + }, + { + "key": "0x004c3a8541d9ec369a9fd4bcdad885cc3b166da70ed6a83066924727d9ff147fce25", + "value": "0x0000000000000000000000000000000000000000000000000000000000000026" + }, + { + "key": "0x004c3a8541d9ec369a9fd4bcdad885cc3b166da70ed6a83066924727d9ff147fce26", + "value": "0x0000000000000000000000000000000000000000000000000000000000000027" + }, + { + "key": "0x004c3a8541d9ec369a9fd4bcdad885cc3b166da70ed6a83066924727d9ff147fce27", + "value": "0x0000000000000000000000000000000000000000000000000000000000000028" + }, + { + "key": "0x004c3a8541d9ec369a9fd4bcdad885cc3b166da70ed6a83066924727d9ff147fce28", + "value": "0x0000000000000000000000000000000000000000000000000000000000000029" + }, + { + "key": "0x004c3a8541d9ec369a9fd4bcdad885cc3b166da70ed6a83066924727d9ff147fce29", + "value": "0x000000000000000000000000000000000000000000000000000000000000002a" + }, + { + "key": "0x004c3a8541d9ec369a9fd4bcdad885cc3b166da70ed6a83066924727d9ff147fce2a", + "value": "0x000000000000000000000000000000000000000000000000000000000000002b" + }, + { + "key": "0x004c3a8541d9ec369a9fd4bcdad885cc3b166da70ed6a83066924727d9ff147fce2b", + "value": "0x000000000000000000000000000000000000000000000000000000000000002c" + }, + { + "key": "0x004c3a8541d9ec369a9fd4bcdad885cc3b166da70ed6a83066924727d9ff147fce2c", + "value": "0x000000000000000000000000000000000000000000000000000000000000002d" + }, + { + "key": "0x004c3a8541d9ec369a9fd4bcdad885cc3b166da70ed6a83066924727d9ff147fce2d", + "value": "0x000000000000000000000000000000000000000000000000000000000000002e" + }, + { + "key": "0x004c3a8541d9ec369a9fd4bcdad885cc3b166da70ed6a83066924727d9ff147fce2e", + "value": "0x000000000000000000000000000000000000000000000000000000000000002f" + }, + { + "key": "0x004c3a8541d9ec369a9fd4bcdad885cc3b166da70ed6a83066924727d9ff147fce2f", + "value": "0x0000000000000000000000000000000000000000000000000000000000000030" + }, + { + "key": "0x004c3a8541d9ec369a9fd4bcdad885cc3b166da70ed6a83066924727d9ff147fce30", + "value": "0x0000000000000000000000000000000000000000000000000000000000000031" + }, + { + "key": "0x004c3a8541d9ec369a9fd4bcdad885cc3b166da70ed6a83066924727d9ff147fce31", + "value": "0x0000000000000000000000000000000000000000000000000000000000000032" + }, + { + "key": "0x004c3a8541d9ec369a9fd4bcdad885cc3b166da70ed6a83066924727d9ff147fce32", + "value": "0x0000000000000000000000000000000000000000000000000000000000000033" + }, + { + "key": "0x004c3a8541d9ec369a9fd4bcdad885cc3b166da70ed6a83066924727d9ff147fce33", + "value": "0x0000000000000000000000000000000000000000000000000000000000000034" + }, + { + "key": "0x004c3a8541d9ec369a9fd4bcdad885cc3b166da70ed6a83066924727d9ff147fce34", + "value": "0x0000000000000000000000000000000000000000000000000000000000000035" + }, + { + "key": "0x004c3a8541d9ec369a9fd4bcdad885cc3b166da70ed6a83066924727d9ff147fce35", + "value": "0x0000000000000000000000000000000000000000000000000000000000000036" + }, + { + "key": "0x004c3a8541d9ec369a9fd4bcdad885cc3b166da70ed6a83066924727d9ff147fce36", + "value": "0x0000000000000000000000000000000000000000000000000000000000000037" + }, + { + "key": "0x004c3a8541d9ec369a9fd4bcdad885cc3b166da70ed6a83066924727d9ff147fce37", + "value": "0x0000000000000000000000000000000000000000000000000000000000000038" + }, + { + "key": "0x004c3a8541d9ec369a9fd4bcdad885cc3b166da70ed6a83066924727d9ff147fce38", + "value": "0x0000000000000000000000000000000000000000000000000000000000000039" + }, + { + "key": "0x004c3a8541d9ec369a9fd4bcdad885cc3b166da70ed6a83066924727d9ff147fce39", + "value": "0x000000000000000000000000000000000000000000000000000000000000003a" + }, + { + "key": "0x004c3a8541d9ec369a9fd4bcdad885cc3b166da70ed6a83066924727d9ff147fce3a", + "value": "0x000000000000000000000000000000000000000000000000000000000000003b" + }, + { + "key": "0x004c3a8541d9ec369a9fd4bcdad885cc3b166da70ed6a83066924727d9ff147fce3b", + "value": "0x000000000000000000000000000000000000000000000000000000000000003c" + }, + { + "key": "0x004c3a8541d9ec369a9fd4bcdad885cc3b166da70ed6a83066924727d9ff147fce3c", + "value": "0x000000000000000000000000000000000000000000000000000000000000003d" + }, + { + "key": "0x004c3a8541d9ec369a9fd4bcdad885cc3b166da70ed6a83066924727d9ff147fce3d", + "value": "0x000000000000000000000000000000000000000000000000000000000000003e" + }, + { + "key": "0x004c3a8541d9ec369a9fd4bcdad885cc3b166da70ed6a83066924727d9ff147fce3e", + "value": "0x000000000000000000000000000000000000000000000000000000000000003f" + }, + { + "key": "0x004c3a8541d9ec369a9fd4bcdad885cc3b166da70ed6a83066924727d9ff147fce3f", + "value": "0x0000000000000000000000000000000000000000000000000000000000000040" + }, + { + "key": "0x004c3a8541d9ec369a9fd4bcdad885cc3b166da70ed6a83066924727d9ff147fce40", + "value": "0x0000000000000000000000000000000000000000000000000000000000000041" + }, + { + "key": "0x004c3a8541d9ec369a9fd4bcdad885cc3b166da70ed6a83066924727d9ff147fce41", + "value": "0x0000000000000000000000000000000000000000000000000000000000000042" + }, + { + "key": "0x004c3a8541d9ec369a9fd4bcdad885cc3b166da70ed6a83066924727d9ff147fce42", + "value": "0x0000000000000000000000000000000000000000000000000000000000000043" + }, + { + "key": "0x004c3a8541d9ec369a9fd4bcdad885cc3b166da70ed6a83066924727d9ff147fce43", + "value": "0x0000000000000000000000000000000000000000000000000000000000000044" + }, + { + "key": "0x004c3a8541d9ec369a9fd4bcdad885cc3b166da70ed6a83066924727d9ff147fce44", + "value": "0x0000000000000000000000000000000000000000000000000000000000000045" + }, + { + "key": "0x004c3a8541d9ec369a9fd4bcdad885cc3b166da70ed6a83066924727d9ff147fce45", + "value": "0x0000000000000000000000000000000000000000000000000000000000000046" + }, + { + "key": "0x004c3a8541d9ec369a9fd4bcdad885cc3b166da70ed6a83066924727d9ff147fce46", + "value": "0x0000000000000000000000000000000000000000000000000000000000000047" + }, + { + "key": "0x004c3a8541d9ec369a9fd4bcdad885cc3b166da70ed6a83066924727d9ff147fce47", + "value": "0x0000000000000000000000000000000000000000000000000000000000000048" + }, + { + "key": "0x004c3a8541d9ec369a9fd4bcdad885cc3b166da70ed6a83066924727d9ff147fce48", + "value": "0x0000000000000000000000000000000000000000000000000000000000000049" + }, + { + "key": "0x004c3a8541d9ec369a9fd4bcdad885cc3b166da70ed6a83066924727d9ff147fce49", + "value": "0x000000000000000000000000000000000000000000000000000000000000004a" + }, + { + "key": "0x004c3a8541d9ec369a9fd4bcdad885cc3b166da70ed6a83066924727d9ff147fce4a", + "value": "0x000000000000000000000000000000000000000000000000000000000000004b" + }, + { + "key": "0x004c3a8541d9ec369a9fd4bcdad885cc3b166da70ed6a83066924727d9ff147fce4b", + "value": "0x000000000000000000000000000000000000000000000000000000000000004c" + }, + { + "key": "0x004c3a8541d9ec369a9fd4bcdad885cc3b166da70ed6a83066924727d9ff147fce4c", + "value": "0x000000000000000000000000000000000000000000000000000000000000004d" + }, + { + "key": "0x004c3a8541d9ec369a9fd4bcdad885cc3b166da70ed6a83066924727d9ff147fce4d", + "value": "0x000000000000000000000000000000000000000000000000000000000000004e" + }, + { + "key": "0x004c3a8541d9ec369a9fd4bcdad885cc3b166da70ed6a83066924727d9ff147fce4e", + "value": "0x000000000000000000000000000000000000000000000000000000000000004f" + }, + { + "key": "0x004c3a8541d9ec369a9fd4bcdad885cc3b166da70ed6a83066924727d9ff147fce4f", + "value": "0x0000000000000000000000000000000000000000000000000000000000000050" + }, + { + "key": "0x004c3a8541d9ec369a9fd4bcdad885cc3b166da70ed6a83066924727d9ff147fce50", + "value": "0x0000000000000000000000000000000000000000000000000000000000000051" + }, + { + "key": "0x004c3a8541d9ec369a9fd4bcdad885cc3b166da70ed6a83066924727d9ff147fce51", + "value": "0x0000000000000000000000000000000000000000000000000000000000000052" + }, + { + "key": "0x004c3a8541d9ec369a9fd4bcdad885cc3b166da70ed6a83066924727d9ff147fce52", + "value": "0x0000000000000000000000000000000000000000000000000000000000000053" + }, + { + "key": "0x004c3a8541d9ec369a9fd4bcdad885cc3b166da70ed6a83066924727d9ff147fce53", + "value": "0x0000000000000000000000000000000000000000000000000000000000000054" + }, + { + "key": "0x004c3a8541d9ec369a9fd4bcdad885cc3b166da70ed6a83066924727d9ff147fce54", + "value": "0x0000000000000000000000000000000000000000000000000000000000000055" + }, + { + "key": "0x004c3a8541d9ec369a9fd4bcdad885cc3b166da70ed6a83066924727d9ff147fce55", + "value": "0x0000000000000000000000000000000000000000000000000000000000000056" + }, + { + "key": "0x004c3a8541d9ec369a9fd4bcdad885cc3b166da70ed6a83066924727d9ff147fce56", + "value": "0x0000000000000000000000000000000000000000000000000000000000000057" + }, + { + "key": "0x004c3a8541d9ec369a9fd4bcdad885cc3b166da70ed6a83066924727d9ff147fce57", + "value": "0x0000000000000000000000000000000000000000000000000000000000000058" + }, + { + "key": "0x004c3a8541d9ec369a9fd4bcdad885cc3b166da70ed6a83066924727d9ff147fce58", + "value": "0x0000000000000000000000000000000000000000000000000000000000000059" + }, + { + "key": "0x004c3a8541d9ec369a9fd4bcdad885cc3b166da70ed6a83066924727d9ff147fce59", + "value": "0x000000000000000000000000000000000000000000000000000000000000005a" + }, + { + "key": "0x004c3a8541d9ec369a9fd4bcdad885cc3b166da70ed6a83066924727d9ff147fce5a", + "value": "0x000000000000000000000000000000000000000000000000000000000000005b" + }, + { + "key": "0x004c3a8541d9ec369a9fd4bcdad885cc3b166da70ed6a83066924727d9ff147fce5b", + "value": "0x000000000000000000000000000000000000000000000000000000000000005c" + }, + { + "key": "0x004c3a8541d9ec369a9fd4bcdad885cc3b166da70ed6a83066924727d9ff147fce5c", + "value": "0x000000000000000000000000000000000000000000000000000000000000005d" + }, + { + "key": "0x004c3a8541d9ec369a9fd4bcdad885cc3b166da70ed6a83066924727d9ff147fce5d", + "value": "0x000000000000000000000000000000000000000000000000000000000000005e" + }, + { + "key": "0x004c3a8541d9ec369a9fd4bcdad885cc3b166da70ed6a83066924727d9ff147fce5e", + "value": "0x000000000000000000000000000000000000000000000000000000000000005f" + }, + { + "key": "0x004c3a8541d9ec369a9fd4bcdad885cc3b166da70ed6a83066924727d9ff147fce5f", + "value": "0x0000000000000000000000000000000000000000000000000000000000000060" + }, + { + "key": "0x004c3a8541d9ec369a9fd4bcdad885cc3b166da70ed6a83066924727d9ff147fce60", + "value": "0x0000000000000000000000000000000000000000000000000000000000000061" + }, + { + "key": "0x004c3a8541d9ec369a9fd4bcdad885cc3b166da70ed6a83066924727d9ff147fce61", + "value": "0x0000000000000000000000000000000000000000000000000000000000000062" + }, + { + "key": "0x004c3a8541d9ec369a9fd4bcdad885cc3b166da70ed6a83066924727d9ff147fce62", + "value": "0x0000000000000000000000000000000000000000000000000000000000000063" + }, + { + "key": "0x004c3a8541d9ec369a9fd4bcdad885cc3b166da70ed6a83066924727d9ff147fce63", + "value": "0x0000000000000000000000000000000000000000000000000000000000000064" + }, + { + "key": "0x004c3a8541d9ec369a9fd4bcdad885cc3b166da70ed6a83066924727d9ff147fce64", + "value": "0x0000000000000000000000000000000000000000000000000000000000000065" + }, + { + "key": "0x004c3a8541d9ec369a9fd4bcdad885cc3b166da70ed6a83066924727d9ff147fce65", + "value": "0x0000000000000000000000000000000000000000000000000000000000000066" + }, + { + "key": "0x004c3a8541d9ec369a9fd4bcdad885cc3b166da70ed6a83066924727d9ff147fce66", + "value": "0x0000000000000000000000000000000000000000000000000000000000000067" + }, + { + "key": "0x004c3a8541d9ec369a9fd4bcdad885cc3b166da70ed6a83066924727d9ff147fce67", + "value": "0x0000000000000000000000000000000000000000000000000000000000000068" + }, + { + "key": "0x004c3a8541d9ec369a9fd4bcdad885cc3b166da70ed6a83066924727d9ff147fce68", + "value": "0x0000000000000000000000000000000000000000000000000000000000000069" + }, + { + "key": "0x004c3a8541d9ec369a9fd4bcdad885cc3b166da70ed6a83066924727d9ff147fce69", + "value": "0x000000000000000000000000000000000000000000000000000000000000006a" + }, + { + "key": "0x004c3a8541d9ec369a9fd4bcdad885cc3b166da70ed6a83066924727d9ff147fce6a", + "value": "0x000000000000000000000000000000000000000000000000000000000000006b" + }, + { + "key": "0x004c3a8541d9ec369a9fd4bcdad885cc3b166da70ed6a83066924727d9ff147fce6b", + "value": "0x000000000000000000000000000000000000000000000000000000000000006c" + }, + { + "key": "0x004c3a8541d9ec369a9fd4bcdad885cc3b166da70ed6a83066924727d9ff147fce6c", + "value": "0x000000000000000000000000000000000000000000000000000000000000006d" + }, + { + "key": "0x004c3a8541d9ec369a9fd4bcdad885cc3b166da70ed6a83066924727d9ff147fce6d", + "value": "0x000000000000000000000000000000000000000000000000000000000000006e" + }, + { + "key": "0x004c3a8541d9ec369a9fd4bcdad885cc3b166da70ed6a83066924727d9ff147fce6e", + "value": "0x000000000000000000000000000000000000000000000000000000000000006f" + }, + { + "key": "0x004c3a8541d9ec369a9fd4bcdad885cc3b166da70ed6a83066924727d9ff147fce6f", + "value": "0x0000000000000000000000000000000000000000000000000000000000000070" + }, + { + "key": "0x004c3a8541d9ec369a9fd4bcdad885cc3b166da70ed6a83066924727d9ff147fce70", + "value": "0x0000000000000000000000000000000000000000000000000000000000000071" + }, + { + "key": "0x004c3a8541d9ec369a9fd4bcdad885cc3b166da70ed6a83066924727d9ff147fce71", + "value": "0x0000000000000000000000000000000000000000000000000000000000000072" + }, + { + "key": "0x004c3a8541d9ec369a9fd4bcdad885cc3b166da70ed6a83066924727d9ff147fce72", + "value": "0x0000000000000000000000000000000000000000000000000000000000000073" + }, + { + "key": "0x004c3a8541d9ec369a9fd4bcdad885cc3b166da70ed6a83066924727d9ff147fce73", + "value": "0x0000000000000000000000000000000000000000000000000000000000000074" + }, + { + "key": "0x004c3a8541d9ec369a9fd4bcdad885cc3b166da70ed6a83066924727d9ff147fce74", + "value": "0x0000000000000000000000000000000000000000000000000000000000000075" + }, + { + "key": "0x004c3a8541d9ec369a9fd4bcdad885cc3b166da70ed6a83066924727d9ff147fce75", + "value": "0x0000000000000000000000000000000000000000000000000000000000000076" + }, + { + "key": "0x004c3a8541d9ec369a9fd4bcdad885cc3b166da70ed6a83066924727d9ff147fce76", + "value": "0x0000000000000000000000000000000000000000000000000000000000000077" + }, + { + "key": "0x004c3a8541d9ec369a9fd4bcdad885cc3b166da70ed6a83066924727d9ff147fce77", + "value": "0x0000000000000000000000000000000000000000000000000000000000000078" + }, + { + "key": "0x004c3a8541d9ec369a9fd4bcdad885cc3b166da70ed6a83066924727d9ff147fce78", + "value": "0x0000000000000000000000000000000000000000000000000000000000000079" + }, + { + "key": "0x004c3a8541d9ec369a9fd4bcdad885cc3b166da70ed6a83066924727d9ff147fce79", + "value": "0x000000000000000000000000000000000000000000000000000000000000007a" + }, + { + "key": "0x004c3a8541d9ec369a9fd4bcdad885cc3b166da70ed6a83066924727d9ff147fce7a", + "value": "0x000000000000000000000000000000000000000000000000000000000000007b" + }, + { + "key": "0x004c3a8541d9ec369a9fd4bcdad885cc3b166da70ed6a83066924727d9ff147fce7b", + "value": "0x000000000000000000000000000000000000000000000000000000000000007c" + }, + { + "key": "0x004c3a8541d9ec369a9fd4bcdad885cc3b166da70ed6a83066924727d9ff147fce7c", + "value": "0x000000000000000000000000000000000000000000000000000000000000007d" + }, + { + "key": "0x004c3a8541d9ec369a9fd4bcdad885cc3b166da70ed6a83066924727d9ff147fce7d", + "value": "0x000000000000000000000000000000000000000000000000000000000000007e" + }, + { + "key": "0x004c3a8541d9ec369a9fd4bcdad885cc3b166da70ed6a83066924727d9ff147fce7e", + "value": "0x000000000000000000000000000000000000000000000000000000000000007f" + }, + { + "key": "0x004c3a8541d9ec369a9fd4bcdad885cc3b166da70ed6a83066924727d9ff147fce7f", + "value": "0x0000000000000000000000000000000000000000000000000000000000000080" + }, + { + "key": "0x004c3a8541d9ec369a9fd4bcdad885cc3b166da70ed6a83066924727d9ff147fce80", + "value": "0x0000000000000000000000000000000000000000000000000000000000000081" + }, + { + "key": "0x004c3a8541d9ec369a9fd4bcdad885cc3b166da70ed6a83066924727d9ff147fce81", + "value": "0x0000000000000000000000000000000000000000000000000000000000000082" + }, + { + "key": "0x004c3a8541d9ec369a9fd4bcdad885cc3b166da70ed6a83066924727d9ff147fce82", + "value": "0x0000000000000000000000000000000000000000000000000000000000000083" + }, + { + "key": "0x004c3a8541d9ec369a9fd4bcdad885cc3b166da70ed6a83066924727d9ff147fce83", + "value": "0x0000000000000000000000000000000000000000000000000000000000000084" + }, + { + "key": "0x004c3a8541d9ec369a9fd4bcdad885cc3b166da70ed6a83066924727d9ff147fce84", + "value": "0x0000000000000000000000000000000000000000000000000000000000000085" + }, + { + "key": "0x004c3a8541d9ec369a9fd4bcdad885cc3b166da70ed6a83066924727d9ff147fce85", + "value": "0x0000000000000000000000000000000000000000000000000000000000000086" + }, + { + "key": "0x004c3a8541d9ec369a9fd4bcdad885cc3b166da70ed6a83066924727d9ff147fce86", + "value": "0x0000000000000000000000000000000000000000000000000000000000000087" + }, + { + "key": "0x004c3a8541d9ec369a9fd4bcdad885cc3b166da70ed6a83066924727d9ff147fce87", + "value": "0x0000000000000000000000000000000000000000000000000000000000000088" + }, + { + "key": "0x004c3a8541d9ec369a9fd4bcdad885cc3b166da70ed6a83066924727d9ff147fce88", + "value": "0x0000000000000000000000000000000000000000000000000000000000000089" + }, + { + "key": "0x004c3a8541d9ec369a9fd4bcdad885cc3b166da70ed6a83066924727d9ff147fce89", + "value": "0x000000000000000000000000000000000000000000000000000000000000008a" + }, + { + "key": "0x004c3a8541d9ec369a9fd4bcdad885cc3b166da70ed6a83066924727d9ff147fce8a", + "value": "0x000000000000000000000000000000000000000000000000000000000000008b" + }, + { + "key": "0x004c3a8541d9ec369a9fd4bcdad885cc3b166da70ed6a83066924727d9ff147fce8b", + "value": "0x000000000000000000000000000000000000000000000000000000000000008c" + }, + { + "key": "0x004c3a8541d9ec369a9fd4bcdad885cc3b166da70ed6a83066924727d9ff147fce8c", + "value": "0x000000000000000000000000000000000000000000000000000000000000008d" + }, + { + "key": "0x004c3a8541d9ec369a9fd4bcdad885cc3b166da70ed6a83066924727d9ff147fce8d", + "value": "0x000000000000000000000000000000000000000000000000000000000000008e" + }, + { + "key": "0x004c3a8541d9ec369a9fd4bcdad885cc3b166da70ed6a83066924727d9ff147fce8e", + "value": "0x000000000000000000000000000000000000000000000000000000000000008f" + }, + { + "key": "0x004c3a8541d9ec369a9fd4bcdad885cc3b166da70ed6a83066924727d9ff147fce8f", + "value": "0x0000000000000000000000000000000000000000000000000000000000000090" + }, + { + "key": "0x004c3a8541d9ec369a9fd4bcdad885cc3b166da70ed6a83066924727d9ff147fce90", + "value": "0x0000000000000000000000000000000000000000000000000000000000000091" + }, + { + "key": "0x004c3a8541d9ec369a9fd4bcdad885cc3b166da70ed6a83066924727d9ff147fce91", + "value": "0x0000000000000000000000000000000000000000000000000000000000000092" + }, + { + "key": "0x004c3a8541d9ec369a9fd4bcdad885cc3b166da70ed6a83066924727d9ff147fce92", + "value": "0x0000000000000000000000000000000000000000000000000000000000000093" + }, + { + "key": "0x004c3a8541d9ec369a9fd4bcdad885cc3b166da70ed6a83066924727d9ff147fce93", + "value": "0x0000000000000000000000000000000000000000000000000000000000000094" + }, + { + "key": "0x004c3a8541d9ec369a9fd4bcdad885cc3b166da70ed6a83066924727d9ff147fce94", + "value": "0x0000000000000000000000000000000000000000000000000000000000000095" + }, + { + "key": "0x004c3a8541d9ec369a9fd4bcdad885cc3b166da70ed6a83066924727d9ff147fce95", + "value": "0x0000000000000000000000000000000000000000000000000000000000000096" + }, + { + "key": "0x004c3a8541d9ec369a9fd4bcdad885cc3b166da70ed6a83066924727d9ff147fce96", + "value": "0x0000000000000000000000000000000000000000000000000000000000000097" + }, + { + "key": "0x004c3a8541d9ec369a9fd4bcdad885cc3b166da70ed6a83066924727d9ff147fce97", + "value": "0x0000000000000000000000000000000000000000000000000000000000000098" + }, + { + "key": "0x004c3a8541d9ec369a9fd4bcdad885cc3b166da70ed6a83066924727d9ff147fce98", + "value": "0x0000000000000000000000000000000000000000000000000000000000000099" + }, + { + "key": "0x004c3a8541d9ec369a9fd4bcdad885cc3b166da70ed6a83066924727d9ff147fce99", + "value": "0x000000000000000000000000000000000000000000000000000000000000009a" + }, + { + "key": "0x004c3a8541d9ec369a9fd4bcdad885cc3b166da70ed6a83066924727d9ff147fce9a", + "value": "0x000000000000000000000000000000000000000000000000000000000000009b" + }, + { + "key": "0x004c3a8541d9ec369a9fd4bcdad885cc3b166da70ed6a83066924727d9ff147fce9b", + "value": "0x000000000000000000000000000000000000000000000000000000000000009c" + }, + { + "key": "0x004c3a8541d9ec369a9fd4bcdad885cc3b166da70ed6a83066924727d9ff147fce9c", + "value": "0x000000000000000000000000000000000000000000000000000000000000009d" + }, + { + "key": "0x004c3a8541d9ec369a9fd4bcdad885cc3b166da70ed6a83066924727d9ff147fce9d", + "value": "0x000000000000000000000000000000000000000000000000000000000000009e" + }, + { + "key": "0x004c3a8541d9ec369a9fd4bcdad885cc3b166da70ed6a83066924727d9ff147fce9e", + "value": "0x000000000000000000000000000000000000000000000000000000000000009f" + }, + { + "key": "0x004c3a8541d9ec369a9fd4bcdad885cc3b166da70ed6a83066924727d9ff147fce9f", + "value": "0x00000000000000000000000000000000000000000000000000000000000000a0" + }, + { + "key": "0x004c3a8541d9ec369a9fd4bcdad885cc3b166da70ed6a83066924727d9ff147fcea0", + "value": "0x00000000000000000000000000000000000000000000000000000000000000a1" + }, + { + "key": "0x004c3a8541d9ec369a9fd4bcdad885cc3b166da70ed6a83066924727d9ff147fcea1", + "value": "0x00000000000000000000000000000000000000000000000000000000000000a2" + }, + { + "key": "0x004c3a8541d9ec369a9fd4bcdad885cc3b166da70ed6a83066924727d9ff147fcea2", + "value": "0x00000000000000000000000000000000000000000000000000000000000000a3" + }, + { + "key": "0x004c3a8541d9ec369a9fd4bcdad885cc3b166da70ed6a83066924727d9ff147fcea3", + "value": "0x00000000000000000000000000000000000000000000000000000000000000a4" + }, + { + "key": "0x004c3a8541d9ec369a9fd4bcdad885cc3b166da70ed6a83066924727d9ff147fcea4", + "value": "0x00000000000000000000000000000000000000000000000000000000000000a5" + }, + { + "key": "0x004c3a8541d9ec369a9fd4bcdad885cc3b166da70ed6a83066924727d9ff147fcea5", + "value": "0x00000000000000000000000000000000000000000000000000000000000000a6" + }, + { + "key": "0x004c3a8541d9ec369a9fd4bcdad885cc3b166da70ed6a83066924727d9ff147fcea6", + "value": "0x00000000000000000000000000000000000000000000000000000000000000a7" + }, + { + "key": "0x004c3a8541d9ec369a9fd4bcdad885cc3b166da70ed6a83066924727d9ff147fcea7", + "value": "0x00000000000000000000000000000000000000000000000000000000000000a8" + }, + { + "key": "0x004c3a8541d9ec369a9fd4bcdad885cc3b166da70ed6a83066924727d9ff147fcea8", + "value": "0x00000000000000000000000000000000000000000000000000000000000000a9" + }, + { + "key": "0x004c3a8541d9ec369a9fd4bcdad885cc3b166da70ed6a83066924727d9ff147fcea9", + "value": "0x00000000000000000000000000000000000000000000000000000000000000aa" + }, + { + "key": "0x004c3a8541d9ec369a9fd4bcdad885cc3b166da70ed6a83066924727d9ff147fceaa", + "value": "0x00000000000000000000000000000000000000000000000000000000000000ab" + }, + { + "key": "0x004c3a8541d9ec369a9fd4bcdad885cc3b166da70ed6a83066924727d9ff147fceab", + "value": "0x00000000000000000000000000000000000000000000000000000000000000ac" + }, + { + "key": "0x004c3a8541d9ec369a9fd4bcdad885cc3b166da70ed6a83066924727d9ff147fceac", + "value": "0x00000000000000000000000000000000000000000000000000000000000000ad" + }, + { + "key": "0x004c3a8541d9ec369a9fd4bcdad885cc3b166da70ed6a83066924727d9ff147fcead", + "value": "0x00000000000000000000000000000000000000000000000000000000000000ae" + }, + { + "key": "0x004c3a8541d9ec369a9fd4bcdad885cc3b166da70ed6a83066924727d9ff147fceae", + "value": "0x00000000000000000000000000000000000000000000000000000000000000af" + }, + { + "key": "0x004c3a8541d9ec369a9fd4bcdad885cc3b166da70ed6a83066924727d9ff147fceaf", + "value": "0x00000000000000000000000000000000000000000000000000000000000000b0" + }, + { + "key": "0x004c3a8541d9ec369a9fd4bcdad885cc3b166da70ed6a83066924727d9ff147fceb0", + "value": "0x00000000000000000000000000000000000000000000000000000000000000b1" + }, + { + "key": "0x004c3a8541d9ec369a9fd4bcdad885cc3b166da70ed6a83066924727d9ff147fceb1", + "value": "0x00000000000000000000000000000000000000000000000000000000000000b2" + }, + { + "key": "0x004c3a8541d9ec369a9fd4bcdad885cc3b166da70ed6a83066924727d9ff147fceb2", + "value": "0x00000000000000000000000000000000000000000000000000000000000000b3" + }, + { + "key": "0x004c3a8541d9ec369a9fd4bcdad885cc3b166da70ed6a83066924727d9ff147fceb3", + "value": "0x00000000000000000000000000000000000000000000000000000000000000b4" + }, + { + "key": "0x004c3a8541d9ec369a9fd4bcdad885cc3b166da70ed6a83066924727d9ff147fceb4", + "value": "0x00000000000000000000000000000000000000000000000000000000000000b5" + }, + { + "key": "0x004c3a8541d9ec369a9fd4bcdad885cc3b166da70ed6a83066924727d9ff147fceb5", + "value": "0x00000000000000000000000000000000000000000000000000000000000000b6" + }, + { + "key": "0x004c3a8541d9ec369a9fd4bcdad885cc3b166da70ed6a83066924727d9ff147fceb6", + "value": "0x00000000000000000000000000000000000000000000000000000000000000b7" + }, + { + "key": "0x004c3a8541d9ec369a9fd4bcdad885cc3b166da70ed6a83066924727d9ff147fceb7", + "value": "0x00000000000000000000000000000000000000000000000000000000000000b8" + }, + { + "key": "0x004c3a8541d9ec369a9fd4bcdad885cc3b166da70ed6a83066924727d9ff147fceb8", + "value": "0x00000000000000000000000000000000000000000000000000000000000000b9" + }, + { + "key": "0x004c3a8541d9ec369a9fd4bcdad885cc3b166da70ed6a83066924727d9ff147fceb9", + "value": "0x00000000000000000000000000000000000000000000000000000000000000ba" + }, + { + "key": "0x004c3a8541d9ec369a9fd4bcdad885cc3b166da70ed6a83066924727d9ff147fceba", + "value": "0x00000000000000000000000000000000000000000000000000000000000000bb" + }, + { + "key": "0x004c3a8541d9ec369a9fd4bcdad885cc3b166da70ed6a83066924727d9ff147fcebb", + "value": "0x00000000000000000000000000000000000000000000000000000000000000bc" + }, + { + "key": "0x004c3a8541d9ec369a9fd4bcdad885cc3b166da70ed6a83066924727d9ff147fcebc", + "value": "0x00000000000000000000000000000000000000000000000000000000000000bd" + }, + { + "key": "0x004c3a8541d9ec369a9fd4bcdad885cc3b166da70ed6a83066924727d9ff147fcebd", + "value": "0x00000000000000000000000000000000000000000000000000000000000000be" + }, + { + "key": "0x004c3a8541d9ec369a9fd4bcdad885cc3b166da70ed6a83066924727d9ff147fcebe", + "value": "0x00000000000000000000000000000000000000000000000000000000000000bf" + }, + { + "key": "0x004c3a8541d9ec369a9fd4bcdad885cc3b166da70ed6a83066924727d9ff147fcebf", + "value": "0x00000000000000000000000000000000000000000000000000000000000000c0" + }, + { + "key": "0x004c3a8541d9ec369a9fd4bcdad885cc3b166da70ed6a83066924727d9ff147fcec0", + "value": "0x00000000000000000000000000000000000000000000000000000000000000c1" + }, + { + "key": "0x004c3a8541d9ec369a9fd4bcdad885cc3b166da70ed6a83066924727d9ff147fcec1", + "value": "0x00000000000000000000000000000000000000000000000000000000000000c2" + }, + { + "key": "0x004c3a8541d9ec369a9fd4bcdad885cc3b166da70ed6a83066924727d9ff147fcec2", + "value": "0x00000000000000000000000000000000000000000000000000000000000000c3" + }, + { + "key": "0x004c3a8541d9ec369a9fd4bcdad885cc3b166da70ed6a83066924727d9ff147fcec3", + "value": "0x00000000000000000000000000000000000000000000000000000000000000c4" + }, + { + "key": "0x004c3a8541d9ec369a9fd4bcdad885cc3b166da70ed6a83066924727d9ff147fcec4", + "value": "0x00000000000000000000000000000000000000000000000000000000000000c5" + }, + { + "key": "0x004c3a8541d9ec369a9fd4bcdad885cc3b166da70ed6a83066924727d9ff147fcec5", + "value": "0x00000000000000000000000000000000000000000000000000000000000000c6" + }, + { + "key": "0x004c3a8541d9ec369a9fd4bcdad885cc3b166da70ed6a83066924727d9ff147fcec6", + "value": "0x00000000000000000000000000000000000000000000000000000000000000c7" + }, + { + "key": "0x004c3a8541d9ec369a9fd4bcdad885cc3b166da70ed6a83066924727d9ff147fcec7", + "value": "0x00000000000000000000000000000000000000000000000000000000000000c8" + }, + { + "key": "0x004c3a8541d9ec369a9fd4bcdad885cc3b166da70ed6a83066924727d9ff147fcec8", + "value": "0x00000000000000000000000000000000000000000000000000000000000000c9" + }, + { + "key": "0x004c3a8541d9ec369a9fd4bcdad885cc3b166da70ed6a83066924727d9ff147fcec9", + "value": "0x00000000000000000000000000000000000000000000000000000000000000ca" + }, + { + "key": "0x004c3a8541d9ec369a9fd4bcdad885cc3b166da70ed6a83066924727d9ff147fceca", + "value": "0x00000000000000000000000000000000000000000000000000000000000000cb" + }, + { + "key": "0x004c3a8541d9ec369a9fd4bcdad885cc3b166da70ed6a83066924727d9ff147fcecb", + "value": "0x00000000000000000000000000000000000000000000000000000000000000cc" + }, + { + "key": "0x004c3a8541d9ec369a9fd4bcdad885cc3b166da70ed6a83066924727d9ff147fcecc", + "value": "0x00000000000000000000000000000000000000000000000000000000000000cd" + }, + { + "key": "0x004c3a8541d9ec369a9fd4bcdad885cc3b166da70ed6a83066924727d9ff147fcecd", + "value": "0x00000000000000000000000000000000000000000000000000000000000000ce" + }, + { + "key": "0x004c3a8541d9ec369a9fd4bcdad885cc3b166da70ed6a83066924727d9ff147fcece", + "value": "0x00000000000000000000000000000000000000000000000000000000000000cf" + }, + { + "key": "0x004c3a8541d9ec369a9fd4bcdad885cc3b166da70ed6a83066924727d9ff147fcecf", + "value": "0x00000000000000000000000000000000000000000000000000000000000000d0" + }, + { + "key": "0x004c3a8541d9ec369a9fd4bcdad885cc3b166da70ed6a83066924727d9ff147fced0", + "value": "0x00000000000000000000000000000000000000000000000000000000000000d1" + }, + { + "key": "0x004c3a8541d9ec369a9fd4bcdad885cc3b166da70ed6a83066924727d9ff147fced1", + "value": "0x00000000000000000000000000000000000000000000000000000000000000d2" + }, + { + "key": "0x004c3a8541d9ec369a9fd4bcdad885cc3b166da70ed6a83066924727d9ff147fced2", + "value": "0x00000000000000000000000000000000000000000000000000000000000000d3" + }, + { + "key": "0x004c3a8541d9ec369a9fd4bcdad885cc3b166da70ed6a83066924727d9ff147fced3", + "value": "0x00000000000000000000000000000000000000000000000000000000000000d4" + }, + { + "key": "0x004c3a8541d9ec369a9fd4bcdad885cc3b166da70ed6a83066924727d9ff147fced4", + "value": "0x00000000000000000000000000000000000000000000000000000000000000d5" + }, + { + "key": "0x004c3a8541d9ec369a9fd4bcdad885cc3b166da70ed6a83066924727d9ff147fced5", + "value": "0x00000000000000000000000000000000000000000000000000000000000000d6" + }, + { + "key": "0x004c3a8541d9ec369a9fd4bcdad885cc3b166da70ed6a83066924727d9ff147fced6", + "value": "0x00000000000000000000000000000000000000000000000000000000000000d7" + }, + { + "key": "0x004c3a8541d9ec369a9fd4bcdad885cc3b166da70ed6a83066924727d9ff147fced7", + "value": "0x00000000000000000000000000000000000000000000000000000000000000d8" + }, + { + "key": "0x004c3a8541d9ec369a9fd4bcdad885cc3b166da70ed6a83066924727d9ff147fced8", + "value": "0x00000000000000000000000000000000000000000000000000000000000000d9" + }, + { + "key": "0x004c3a8541d9ec369a9fd4bcdad885cc3b166da70ed6a83066924727d9ff147fced9", + "value": "0x00000000000000000000000000000000000000000000000000000000000000da" + }, + { + "key": "0x004c3a8541d9ec369a9fd4bcdad885cc3b166da70ed6a83066924727d9ff147fceda", + "value": "0x00000000000000000000000000000000000000000000000000000000000000db" + }, + { + "key": "0x004c3a8541d9ec369a9fd4bcdad885cc3b166da70ed6a83066924727d9ff147fcedb", + "value": "0x00000000000000000000000000000000000000000000000000000000000000dc" + }, + { + "key": "0x004c3a8541d9ec369a9fd4bcdad885cc3b166da70ed6a83066924727d9ff147fcedc", + "value": "0x00000000000000000000000000000000000000000000000000000000000000dd" + }, + { + "key": "0x004c3a8541d9ec369a9fd4bcdad885cc3b166da70ed6a83066924727d9ff147fcedd", + "value": "0x00000000000000000000000000000000000000000000000000000000000000de" + }, + { + "key": "0x004c3a8541d9ec369a9fd4bcdad885cc3b166da70ed6a83066924727d9ff147fcede", + "value": "0x00000000000000000000000000000000000000000000000000000000000000df" + }, + { + "key": "0x004c3a8541d9ec369a9fd4bcdad885cc3b166da70ed6a83066924727d9ff147fcedf", + "value": "0x00000000000000000000000000000000000000000000000000000000000000e0" + }, + { + "key": "0x004c3a8541d9ec369a9fd4bcdad885cc3b166da70ed6a83066924727d9ff147fcee0", + "value": "0x00000000000000000000000000000000000000000000000000000000000000e1" + }, + { + "key": "0x004c3a8541d9ec369a9fd4bcdad885cc3b166da70ed6a83066924727d9ff147fcee1", + "value": "0x00000000000000000000000000000000000000000000000000000000000000e2" + }, + { + "key": "0x004c3a8541d9ec369a9fd4bcdad885cc3b166da70ed6a83066924727d9ff147fcee2", + "value": "0x00000000000000000000000000000000000000000000000000000000000000e3" + }, + { + "key": "0x004c3a8541d9ec369a9fd4bcdad885cc3b166da70ed6a83066924727d9ff147fcee3", + "value": "0x00000000000000000000000000000000000000000000000000000000000000e4" + }, + { + "key": "0x004c3a8541d9ec369a9fd4bcdad885cc3b166da70ed6a83066924727d9ff147fcee4", + "value": "0x00000000000000000000000000000000000000000000000000000000000000e5" + }, + { + "key": "0x004c3a8541d9ec369a9fd4bcdad885cc3b166da70ed6a83066924727d9ff147fcee5", + "value": "0x00000000000000000000000000000000000000000000000000000000000000e6" + }, + { + "key": "0x004c3a8541d9ec369a9fd4bcdad885cc3b166da70ed6a83066924727d9ff147fcee6", + "value": "0x00000000000000000000000000000000000000000000000000000000000000e7" + }, + { + "key": "0x004c3a8541d9ec369a9fd4bcdad885cc3b166da70ed6a83066924727d9ff147fcee7", + "value": "0x00000000000000000000000000000000000000000000000000000000000000e8" + }, + { + "key": "0x004c3a8541d9ec369a9fd4bcdad885cc3b166da70ed6a83066924727d9ff147fcee8", + "value": "0x00000000000000000000000000000000000000000000000000000000000000e9" + }, + { + "key": "0x004c3a8541d9ec369a9fd4bcdad885cc3b166da70ed6a83066924727d9ff147fcee9", + "value": "0x00000000000000000000000000000000000000000000000000000000000000ea" + }, + { + "key": "0x004c3a8541d9ec369a9fd4bcdad885cc3b166da70ed6a83066924727d9ff147fceea", + "value": "0x00000000000000000000000000000000000000000000000000000000000000eb" + }, + { + "key": "0x004c3a8541d9ec369a9fd4bcdad885cc3b166da70ed6a83066924727d9ff147fceeb", + "value": "0x00000000000000000000000000000000000000000000000000000000000000ec" + }, + { + "key": "0x004c3a8541d9ec369a9fd4bcdad885cc3b166da70ed6a83066924727d9ff147fceec", + "value": "0x00000000000000000000000000000000000000000000000000000000000000ed" + }, + { + "key": "0x004c3a8541d9ec369a9fd4bcdad885cc3b166da70ed6a83066924727d9ff147fceed", + "value": "0x00000000000000000000000000000000000000000000000000000000000000ee" + }, + { + "key": "0x004c3a8541d9ec369a9fd4bcdad885cc3b166da70ed6a83066924727d9ff147fceee", + "value": "0x00000000000000000000000000000000000000000000000000000000000000ef" + }, + { + "key": "0x004c3a8541d9ec369a9fd4bcdad885cc3b166da70ed6a83066924727d9ff147fceef", + "value": "0x00000000000000000000000000000000000000000000000000000000000000f0" + }, + { + "key": "0x004c3a8541d9ec369a9fd4bcdad885cc3b166da70ed6a83066924727d9ff147fcef0", + "value": "0x00000000000000000000000000000000000000000000000000000000000000f1" + }, + { + "key": "0x004c3a8541d9ec369a9fd4bcdad885cc3b166da70ed6a83066924727d9ff147fcef1", + "value": "0x00000000000000000000000000000000000000000000000000000000000000f2" + }, + { + "key": "0x004c3a8541d9ec369a9fd4bcdad885cc3b166da70ed6a83066924727d9ff147fcef2", + "value": "0x00000000000000000000000000000000000000000000000000000000000000f3" + }, + { + "key": "0x004c3a8541d9ec369a9fd4bcdad885cc3b166da70ed6a83066924727d9ff147fcef3", + "value": "0x00000000000000000000000000000000000000000000000000000000000000f4" + }, + { + "key": "0x004c3a8541d9ec369a9fd4bcdad885cc3b166da70ed6a83066924727d9ff147fcef4", + "value": "0x00000000000000000000000000000000000000000000000000000000000000f5" + }, + { + "key": "0x004c3a8541d9ec369a9fd4bcdad885cc3b166da70ed6a83066924727d9ff147fcef5", + "value": "0x00000000000000000000000000000000000000000000000000000000000000f6" + }, + { + "key": "0x004c3a8541d9ec369a9fd4bcdad885cc3b166da70ed6a83066924727d9ff147fcef6", + "value": "0x00000000000000000000000000000000000000000000000000000000000000f7" + }, + { + "key": "0x004c3a8541d9ec369a9fd4bcdad885cc3b166da70ed6a83066924727d9ff147fcef7", + "value": "0x00000000000000000000000000000000000000000000000000000000000000f8" + }, + { + "key": "0x004c3a8541d9ec369a9fd4bcdad885cc3b166da70ed6a83066924727d9ff147fcef8", + "value": "0x00000000000000000000000000000000000000000000000000000000000000f9" + }, + { + "key": "0x004c3a8541d9ec369a9fd4bcdad885cc3b166da70ed6a83066924727d9ff147fcef9", + "value": "0x00000000000000000000000000000000000000000000000000000000000000fa" + }, + { + "key": "0x004c3a8541d9ec369a9fd4bcdad885cc3b166da70ed6a83066924727d9ff147fcefa", + "value": "0x00000000000000000000000000000000000000000000000000000000000000fb" + }, + { + "key": "0x004c3a8541d9ec369a9fd4bcdad885cc3b166da70ed6a83066924727d9ff147fcefb", + "value": "0x00000000000000000000000000000000000000000000000000000000000000fc" + }, + { + "key": "0x004c3a8541d9ec369a9fd4bcdad885cc3b166da70ed6a83066924727d9ff147fcefc", + "value": "0x00000000000000000000000000000000000000000000000000000000000000fd" + }, + { + "key": "0x004c3a8541d9ec369a9fd4bcdad885cc3b166da70ed6a83066924727d9ff147fcefd", + "value": "0x00000000000000000000000000000000000000000000000000000000000000fe" + }, + { + "key": "0x004c3a8541d9ec369a9fd4bcdad885cc3b166da70ed6a83066924727d9ff147fcefe", + "value": "0x00000000000000000000000000000000000000000000000000000000000000ff" + }, + { + "key": "0x004c3a8541d9ec369a9fd4bcdad885cc3b166da70ed6a83066924727d9ff147fceff", + "value": "0x0000000000000000000000000000000000000000000000000000000000000100" + } + ], + "root": "0x090ef773023b99997f3c8c72e3fa1fbd9b0b62833ffb662e457b60530b358721" + } + ], + "sequence_vectors": [ + { + "seed": 8297, + "ops": [ + { + "op": "set", + "key": "0x0015c4813bb5b1499e0a7e0d46daab8e10c70940c008c566d5c68557b1523d1f2999", + "value": "0x00000000000000000000000000000000000000000000000000000000362952bd" + }, + { + "op": "set", + "key": "0xff2eb90959edcd0b6a8a7922b8b8d37cd9664001c217b4c8e6d2df4b8ede329a7191c71427661efcc7f991940e68aae22bede52be877c5e2b9b4e0c22051e2c61706", + "value": "0x000000000000000000000000000000000000000000000000000000005912e971" + }, + { + "op": "delete", + "key": "0xff2eb90959edcd0b6a8a7922b8b8d37cd9664001c217b4c8e6d2df4b8ede329a7191c71427661efcc7f991940e68aae22bede52be877c5e2b9b4e0c22051e2c61706" + }, + { + "op": "set", + "key": "0xff9c847c439d6431d36ddeb326cb553da2e967778a24afbd1d23a62412186faf016d21858cec8b9c323663e513206217295d0693bb4eb042fd7e5e863612ec45fe5c", + "value": "0x000000000000000000000000000000000000000000000000000000009e92aea6" + }, + { + "op": "set", + "key": "0x00998f08d7054351e5fa3ccadda32ab8d6c0c86a825dd360d57c0fe6b46612297c21", + "value": "0x0000000000000000000000000000000000000000000000000000000037f3974d" + }, + { + "op": "set", + "key": "0x00e61994d31ceae39cae5feb02303eb90e0d4ae90f5b0755c3c26adc81f360ec8516", + "value": "0x0000000000000000000000000000000000000000000000000000000091546180" + }, + { + "op": "set", + "key": "0xffd1afd6427b13f2ce7beb82a2cd6bbfc58eac8b2d46bc87bfbc78fbf440e18a635bd91a1bb3fea09cd07c1f6f0b5b0928ec465c3d5409c3dc4d9f3b384fd276b0df", + "value": "0x00000000000000000000000000000000000000000000000000000000d560d2d0" + }, + { + "op": "set", + "key": "0x002eb90959edcd0b6a8a7922b8b8d37cd9664001c217b4c8e6d2df4b8ede329a717c", + "value": "0x00000000000000000000000000000000000000000000000000000000b7e649ff" + }, + { + "op": "delete", + "key": "0xff9c847c439d6431d36ddeb326cb553da2e967778a24afbd1d23a62412186faf016d21858cec8b9c323663e513206217295d0693bb4eb042fd7e5e863612ec45fe5c" + }, + { + "op": "set", + "key": "0xffe60d91d1d11837912311124089c6b73c9d726c99f4df797eb48c2b7fec6e5e2cedb6b011186812600d2c38950eb3aa1e9e39d11456e96ac38d3ec9cb37e4816783", + "value": "0x0000000000000000000000000000000000000000000000000000000015716296" + }, + { + "op": "set", + "key": "0x0078a2812688114caf4452a5611322977e4b1000e59ef408d4c8e2c90028370bcbf3", + "value": "0x00000000000000000000000000000000000000000000000000000000d566656c" + }, + { + "op": "set", + "key": "0x000861030791246737d706c0d0017de01a26278bbe4ce8a134f5e0cd6f7eb69bcf6b", + "value": "0x00000000000000000000000000000000000000000000000000000000c6f30fd3" + }, + { + "op": "set", + "key": "0x0068e867f5f899fc01d641a00cea2c42d58f3cc061797135198a85301d1e2903062a", + "value": "0x00000000000000000000000000000000000000000000000000000000308a8072" + }, + { + "op": "set", + "key": "0xff46aeb3243f905d4e0eac7f60ee8962848c87e79dfbd019ece14fe45bd720ea6bc7d37b238b059b3f39fb34f03f189fc8c596ad3bc45f7231d1a8e723510074014d", + "value": "0x00000000000000000000000000000000000000000000000000000000fd6c27f9" + }, + { + "op": "set", + "key": "0x00a43fb2d7beb12b34786a837467f62b7d6c5dab4ab7eecc89c0b67db76a682927c1", + "value": "0x000000000000000000000000000000000000000000000000000000000b5daa14" + }, + { + "op": "set", + "key": "0x00543907253acb230f5dc96bbafd8ea0f37dd63b30e8a4a3675bb007673a31fbc5d7", + "value": "0x00000000000000000000000000000000000000000000000000000000ef86c437" + }, + { + "op": "set", + "key": "0xff0fb23a930b05e3bdfbdd0e6694fa9df9c5059ceabc60618bc40931ddb379f1f64c1111b166c73d061477381d77db7818b678c5a5595df51af307fccfda674238c3", + "value": "0x000000000000000000000000000000000000000000000000000000008687ece2" + }, + { + "op": "set", + "key": "0x0046aeb3243f905d4e0eac7f60ee8962848c87e79dfbd019ece14fe45bd720ea6b69", + "value": "0x000000000000000000000000000000000000000000000000000000008d81d15d" + }, + { + "op": "set", + "key": "0xff998f08d7054351e5fa3ccadda32ab8d6c0c86a825dd360d57c0fe6b46612297c216fd3a73b07a45409a19b5e6d545779e55b6900fddc4443dcb06e3d97e25e1908", + "value": "0x000000000000000000000000000000000000000000000000000000008f91e546" + }, + { + "op": "set", + "key": "0x002cebacb183e2f51a3b6cb9f7fe2bd11c399bf1bdb8ce95ed457d3e46190197a0fd", + "value": "0x00000000000000000000000000000000000000000000000000000000b638fa76" + }, + { + "op": "set", + "key": "0xffe60d91d1d11837912311124089c6b73c9d726c99f4df797eb48c2b7fec6e5e2c3b9034b692f2597d8c9c9ad6921ecd112d2f69cc50a6e065034e2ace540e89288e", + "value": "0x0000000000000000000000000000000000000000000000000000000091e7fc09" + }, + { + "op": "set", + "key": "0x002eb90959edcd0b6a8a7922b8b8d37cd9664001c217b4c8e6d2df4b8ede329a71bf", + "value": "0x00000000000000000000000000000000000000000000000000000000cea86aa1" + }, + { + "op": "delete", + "key": "0xffd1afd6427b13f2ce7beb82a2cd6bbfc58eac8b2d46bc87bfbc78fbf440e18a635bd91a1bb3fea09cd07c1f6f0b5b0928ec465c3d5409c3dc4d9f3b384fd276b0df" + }, + { + "op": "set", + "key": "0x00a43fb2d7beb12b34786a837467f62b7d6c5dab4ab7eecc89c0b67db76a68292735", + "value": "0x0000000000000000000000000000000000000000000000000000000049742ebd" + }, + { + "op": "delete", + "key": "0x0078a2812688114caf4452a5611322977e4b1000e59ef408d4c8e2c90028370bcbf3" + }, + { + "op": "set", + "key": "0x007293ce391f5cd652216ca8ea31069a06a82f393028dfc8786aa5efd7be734b2c59", + "value": "0x0000000000000000000000000000000000000000000000000000000006c1a51d" + }, + { + "op": "delete", + "key": "0xff46aeb3243f905d4e0eac7f60ee8962848c87e79dfbd019ece14fe45bd720ea6bc7d37b238b059b3f39fb34f03f189fc8c596ad3bc45f7231d1a8e723510074014d" + }, + { + "op": "set", + "key": "0xff4b83f061efb2f4708114e46b5ef50a255528c28c80bbe5fc6ad88e892bd4c4ea37491f84eb77d352d7d8fca0484b856ceab7236e2f773d157b3fbd6d0586de7fb5", + "value": "0x0000000000000000000000000000000000000000000000000000000097b8536d" + }, + { + "op": "set", + "key": "0x000ed33feae99b046218c56bec56750a98beb5fd6a51ec13218024cb2455586b4323", + "value": "0x0000000000000000000000000000000000000000000000000000000038e606b4" + }, + { + "op": "delete", + "key": "0x0046aeb3243f905d4e0eac7f60ee8962848c87e79dfbd019ece14fe45bd720ea6b69" + }, + { + "op": "delete", + "key": "0xffe60d91d1d11837912311124089c6b73c9d726c99f4df797eb48c2b7fec6e5e2cedb6b011186812600d2c38950eb3aa1e9e39d11456e96ac38d3ec9cb37e4816783" + }, + { + "op": "set", + "key": "0xff34bb4f3468340d5f72275da92420aed94db48bbc61823e92f29f14f51590373aef2032e9c5c80ba9048f874aaea79ab7ce9e0f910b0e98955e60542e3a7f4464d1", + "value": "0x00000000000000000000000000000000000000000000000000000000417abfdc" + }, + { + "op": "set", + "key": "0x00e6b6d8d98be54d4c00cee378bedf964f755b5257bf1eabf52cf39aa987686fa7b0", + "value": "0x0000000000000000000000000000000000000000000000000000000082455405" + }, + { + "op": "set", + "key": "0xffc28c21982a56a6cecf595bfeb78297c7432abfb487cc75c09db2856a118f441d4d68c9b03da1dec117fa853416c0249fb7b863e57d51f6ac8e86e023c336f4686c", + "value": "0x0000000000000000000000000000000000000000000000000000000058e72fcf" + }, + { + "op": "set", + "key": "0xff4b83f061efb2f4708114e46b5ef50a255528c28c80bbe5fc6ad88e892bd4c4ea37491f84eb77d352d7d8fca0484b856ceab7236e2f773d157b3fbd6d0586de7f9d", + "value": "0x00000000000000000000000000000000000000000000000000000000713293e5" + }, + { + "op": "delete", + "key": "0x002eb90959edcd0b6a8a7922b8b8d37cd9664001c217b4c8e6d2df4b8ede329a71bf" + }, + { + "op": "delete", + "key": "0xff998f08d7054351e5fa3ccadda32ab8d6c0c86a825dd360d57c0fe6b46612297c216fd3a73b07a45409a19b5e6d545779e55b6900fddc4443dcb06e3d97e25e1908" + }, + { + "op": "delete", + "key": "0x00543907253acb230f5dc96bbafd8ea0f37dd63b30e8a4a3675bb007673a31fbc5d7" + }, + { + "op": "delete", + "key": "0x00e61994d31ceae39cae5feb02303eb90e0d4ae90f5b0755c3c26adc81f360ec8516" + }, + { + "op": "set", + "key": "0xff7293ce391f5cd652216ca8ea31069a06a82f393028dfc8786aa5efd7be734b2ca38663c8c6b9a008190c48cd4ee4fd87a0d65a077938f6b18ea2ebb292ca4b0a03", + "value": "0x00000000000000000000000000000000000000000000000000000000b4088d5c" + }, + { + "op": "set", + "key": "0x00c901cfbb39e551189323bea0af6cf9e81008c90dce65cbc2ff4672e263d7bb87fd", + "value": "0x0000000000000000000000000000000000000000000000000000000024c1f69c" + }, + { + "op": "set", + "key": "0xffe57860ba0adcdd1294caf74da8b990a80a8a6861bc01b30dfe07450d25bab38f63b4527cc539651483c1b056383959f91cb98fdbd3c1799a854a2781ddb32290fb", + "value": "0x00000000000000000000000000000000000000000000000000000000b1043313" + }, + { + "op": "set", + "key": "0x0068e867f5f899fc01d641a00cea2c42d58f3cc061797135198a85301d1e29030648", + "value": "0x00000000000000000000000000000000000000000000000000000000fb5f463b" + }, + { + "op": "set", + "key": "0xff1df77729bb76b139342cfc86aff011590bf6bb810f47e69a6babac30af96f9f5451673405763851d69ce90fb56d8dd98d3ef3ad6e6deccec4a6cc4303c04606320", + "value": "0x00000000000000000000000000000000000000000000000000000000694aeca8" + }, + { + "op": "set", + "key": "0xff34bb4f3468340d5f72275da92420aed94db48bbc61823e92f29f14f51590373aeaa08ae14db4be9f284878fd074dce4d3ac11a41a69fde2feb4b3aaaa37da25bc3", + "value": "0x000000000000000000000000000000000000000000000000000000009beca248" + }, + { + "op": "set", + "key": "0x001df77729bb76b139342cfc86aff011590bf6bb810f47e69a6babac30af96f9f5cd", + "value": "0x00000000000000000000000000000000000000000000000000000000fd378d1e" + }, + { + "op": "set", + "key": "0xff15c4813bb5b1499e0a7e0d46daab8e10c70940c008c566d5c68557b1523d1f29d090338251613ee5dbd2ea5b763cb40885f3f40530835887585c70141b84e95741", + "value": "0x000000000000000000000000000000000000000000000000000000008c0aed86" + }, + { + "op": "set", + "key": "0x0029e4e6bbd168fe5b46b7d34c34b628f95d54c4e75f9335a7e96282d1173b07df55", + "value": "0x00000000000000000000000000000000000000000000000000000000b0e9c48a" + }, + { + "op": "set", + "key": "0x00e2f41518226671332da4adddab1fdcb9903c7b895c7943b669067fe96b0baacf0c", + "value": "0x000000000000000000000000000000000000000000000000000000003841a708" + }, + { + "op": "set", + "key": "0x002eb90959edcd0b6a8a7922b8b8d37cd9664001c217b4c8e6d2df4b8ede329a7196", + "value": "0x00000000000000000000000000000000000000000000000000000000f7e377a8" + }, + { + "op": "set", + "key": "0xff9c847c439d6431d36ddeb326cb553da2e967778a24afbd1d23a62412186faf01fe7c1926f4dca1966bcd404358f4eb1d2d8e2104e04d995e7e23fbfd410d75cd9c", + "value": "0x0000000000000000000000000000000000000000000000000000000006937fd1" + }, + { + "op": "set", + "key": "0x00e61994d31ceae39cae5feb02303eb90e0d4ae90f5b0755c3c26adc81f360ec857c", + "value": "0x000000000000000000000000000000000000000000000000000000003f08c9e1" + }, + { + "op": "delete", + "key": "0xff4b83f061efb2f4708114e46b5ef50a255528c28c80bbe5fc6ad88e892bd4c4ea37491f84eb77d352d7d8fca0484b856ceab7236e2f773d157b3fbd6d0586de7fb5" + }, + { + "op": "set", + "key": "0xff18e25b9b05a714bd17ede1445ee7c785983a0194a3ed103783662bdc401dd7ffd358410b57fde52d1120a72e241687991ced2ec523b82168713b13b557349ab826", + "value": "0x00000000000000000000000000000000000000000000000000000000c425ef1b" + }, + { + "op": "delete", + "key": "0x00e61994d31ceae39cae5feb02303eb90e0d4ae90f5b0755c3c26adc81f360ec857c" + }, + { + "op": "set", + "key": "0x0015c4813bb5b1499e0a7e0d46daab8e10c70940c008c566d5c68557b1523d1f293c", + "value": "0x00000000000000000000000000000000000000000000000000000000ea48f5fd" + }, + { + "op": "set", + "key": "0x0055a6923f7414ba482ae8a64b407f386a0bb0491b742bd4b22b5b4b44fca84e8471", + "value": "0x000000000000000000000000000000000000000000000000000000006319b687" + }, + { + "op": "delete", + "key": "0x00a43fb2d7beb12b34786a837467f62b7d6c5dab4ab7eecc89c0b67db76a68292735" + }, + { + "op": "set", + "key": "0xff4c3a8541d9ec369a9fd4bcdad885cc3b166da70ed6a83066924727d9ff147fce963472d01342d91a0a268bdfee2ecf01a6d30f9021151faf2e2e6719120bf40ffc", + "value": "0x00000000000000000000000000000000000000000000000000000000e6005531" + }, + { + "op": "delete", + "key": "0xff0fb23a930b05e3bdfbdd0e6694fa9df9c5059ceabc60618bc40931ddb379f1f64c1111b166c73d061477381d77db7818b678c5a5595df51af307fccfda674238c3" + }, + { + "op": "set", + "key": "0x00e6b6d8d98be54d4c00cee378bedf964f755b5257bf1eabf52cf39aa987686fa7f8", + "value": "0x000000000000000000000000000000000000000000000000000000002665b225" + }, + { + "op": "set", + "key": "0x0046aeb3243f905d4e0eac7f60ee8962848c87e79dfbd019ece14fe45bd720ea6ba4", + "value": "0x00000000000000000000000000000000000000000000000000000000c0e4632f" + }, + { + "op": "set", + "key": "0xffbded9122e6171a5dad9ce2aaebd933abf4718e9bf12448cf113064c9fd8e6ad0551bc91eb18a241f3262e8f9c56018ae23b3e1e9d55d93bf499ad6f830b8447f65", + "value": "0x000000000000000000000000000000000000000000000000000000009740d889" + }, + { + "op": "set", + "key": "0xff18e25b9b05a714bd17ede1445ee7c785983a0194a3ed103783662bdc401dd7ff361b271b7deb81ee7027749296cc9369ae699ca1ef815575a5c0bd3b5b0b0cf8d2", + "value": "0x00000000000000000000000000000000000000000000000000000000ba230288" + }, + { + "op": "delete", + "key": "0x002cebacb183e2f51a3b6cb9f7fe2bd11c399bf1bdb8ce95ed457d3e46190197a0fd" + }, + { + "op": "set", + "key": "0x00ec64b2de1fc1d53b795f49a1c84ca4172bc3292472c3e244e4f8c225fa786f564f", + "value": "0x00000000000000000000000000000000000000000000000000000000798636a1" + }, + { + "op": "set", + "key": "0xff5963897639cd11d686b199366a1249d76bc89e0f7161c9eca48d9a199435952d8457e83ae86e00b414b40db6057395b86b6ef8d238f669d109a1044de6ee415ead", + "value": "0x00000000000000000000000000000000000000000000000000000000b84e8c3d" + }, + { + "op": "set", + "key": "0x007293ce391f5cd652216ca8ea31069a06a82f393028dfc8786aa5efd7be734b2c2c", + "value": "0x00000000000000000000000000000000000000000000000000000000e75bf304" + }, + { + "op": "set", + "key": "0xffc28c21982a56a6cecf595bfeb78297c7432abfb487cc75c09db2856a118f441df2a84c02b0cbaafcabc4cbb5f988d916f3287032f07e3f8bf51588b4311ed04c53", + "value": "0x00000000000000000000000000000000000000000000000000000000d43345f9" + }, + { + "op": "delete", + "key": "0x0055a6923f7414ba482ae8a64b407f386a0bb0491b742bd4b22b5b4b44fca84e8471" + }, + { + "op": "set", + "key": "0xff412c4d14e5bde6ea4c898a1d0f911f12a2cf775a62e3cc4c0f28331d432e8f1064ac84ee2be5e94ce2135d3a58e8221431668d3da7a5b6f8cf31778f0cd7acad7e", + "value": "0x00000000000000000000000000000000000000000000000000000000918cbe16" + }, + { + "op": "set", + "key": "0xff7b1eefd69ea014244937aaeaea4e8d4d296c04edd75ae47897e5612ef5a1a252477cbbc3c4e091807fc19bbcc03de36f41608c44f5a2f4a8cd97490324eb42b585", + "value": "0x00000000000000000000000000000000000000000000000000000000093912d6" + }, + { + "op": "delete", + "key": "0x00e2f41518226671332da4adddab1fdcb9903c7b895c7943b669067fe96b0baacf0c" + }, + { + "op": "set", + "key": "0x00e61994d31ceae39cae5feb02303eb90e0d4ae90f5b0755c3c26adc81f360ec85cf", + "value": "0x00000000000000000000000000000000000000000000000000000000c357d7e2" + }, + { + "op": "set", + "key": "0xff412c4d14e5bde6ea4c898a1d0f911f12a2cf775a62e3cc4c0f28331d432e8f10c065da61a7bd36e4bd11a16fd628e8bd5c6f79eb75cbc8ea1aa2ac6d870713c249", + "value": "0x00000000000000000000000000000000000000000000000000000000391eba89" + }, + { + "op": "delete", + "key": "0xff18e25b9b05a714bd17ede1445ee7c785983a0194a3ed103783662bdc401dd7ff361b271b7deb81ee7027749296cc9369ae699ca1ef815575a5c0bd3b5b0b0cf8d2" + }, + { + "op": "set", + "key": "0x00e6b6d8d98be54d4c00cee378bedf964f755b5257bf1eabf52cf39aa987686fa75b", + "value": "0x0000000000000000000000000000000000000000000000000000000069a38a4b" + }, + { + "op": "delete", + "key": "0xff4b83f061efb2f4708114e46b5ef50a255528c28c80bbe5fc6ad88e892bd4c4ea37491f84eb77d352d7d8fca0484b856ceab7236e2f773d157b3fbd6d0586de7f9d" + }, + { + "op": "delete", + "key": "0x000ed33feae99b046218c56bec56750a98beb5fd6a51ec13218024cb2455586b4323" + }, + { + "op": "set", + "key": "0x007293ce391f5cd652216ca8ea31069a06a82f393028dfc8786aa5efd7be734b2c4c", + "value": "0x0000000000000000000000000000000000000000000000000000000084ac6f36" + }, + { + "op": "set", + "key": "0xff1df77729bb76b139342cfc86aff011590bf6bb810f47e69a6babac30af96f9f5ffa1e869c4566231d09357daec90e71aa93821a7347b29175ef4a1bc02a0b38158", + "value": "0x000000000000000000000000000000000000000000000000000000005902afd7" + }, + { + "op": "set", + "key": "0xffe6b6d8d98be54d4c00cee378bedf964f755b5257bf1eabf52cf39aa987686fa7631039493afa587d6693f06562405982839acd12cb82734e1f59fb5256053d6062", + "value": "0x000000000000000000000000000000000000000000000000000000005d43735b" + }, + { + "op": "set", + "key": "0x0078a2812688114caf4452a5611322977e4b1000e59ef408d4c8e2c90028370bcb77", + "value": "0x00000000000000000000000000000000000000000000000000000000bb55d92e" + }, + { + "op": "set", + "key": "0xff15c4813bb5b1499e0a7e0d46daab8e10c70940c008c566d5c68557b1523d1f29d090338251613ee5dbd2ea5b763cb40885f3f40530835887585c70141b84e957b2", + "value": "0x000000000000000000000000000000000000000000000000000000004668e5c4" + }, + { + "op": "set", + "key": "0x002cebacb183e2f51a3b6cb9f7fe2bd11c399bf1bdb8ce95ed457d3e46190197a0ff", + "value": "0x000000000000000000000000000000000000000000000000000000005ba8964d" + }, + { + "op": "set", + "key": "0x00e61994d31ceae39cae5feb02303eb90e0d4ae90f5b0755c3c26adc81f360ec8582", + "value": "0x00000000000000000000000000000000000000000000000000000000a7b0735b" + }, + { + "op": "delete", + "key": "0x002eb90959edcd0b6a8a7922b8b8d37cd9664001c217b4c8e6d2df4b8ede329a717c" + }, + { + "op": "set", + "key": "0x000861030791246737d706c0d0017de01a26278bbe4ce8a134f5e0cd6f7eb69bcf43", + "value": "0x0000000000000000000000000000000000000000000000000000000052635493" + }, + { + "op": "set", + "key": "0x000ed33feae99b046218c56bec56750a98beb5fd6a51ec13218024cb2455586b43fb", + "value": "0x00000000000000000000000000000000000000000000000000000000d55f3939" + }, + { + "op": "set", + "key": "0x007b1eefd69ea014244937aaeaea4e8d4d296c04edd75ae47897e5612ef5a1a2521e", + "value": "0x000000000000000000000000000000000000000000000000000000006aa15dff" + }, + { + "op": "set", + "key": "0x00ec64b2de1fc1d53b795f49a1c84ca4172bc3292472c3e244e4f8c225fa786f56db", + "value": "0x00000000000000000000000000000000000000000000000000000000a79bbea0" + }, + { + "op": "set", + "key": "0xff0fb23a930b05e3bdfbdd0e6694fa9df9c5059ceabc60618bc40931ddb379f1f6f8ec4b65102d57431a895525e044c325b5cfaf3bec31d977b0a27ca73735d9da14", + "value": "0x00000000000000000000000000000000000000000000000000000000a3db056e" + }, + { + "op": "delete", + "key": "0x0068e867f5f899fc01d641a00cea2c42d58f3cc061797135198a85301d1e29030648" + }, + { + "op": "set", + "key": "0x0078a2812688114caf4452a5611322977e4b1000e59ef408d4c8e2c90028370bcb62", + "value": "0x00000000000000000000000000000000000000000000000000000000b82dbff4" + }, + { + "op": "set", + "key": "0x00e60d91d1d11837912311124089c6b73c9d726c99f4df797eb48c2b7fec6e5e2c22", + "value": "0x00000000000000000000000000000000000000000000000000000000576e2f9d" + }, + { + "op": "set", + "key": "0x0078a2812688114caf4452a5611322977e4b1000e59ef408d4c8e2c90028370bcbb2", + "value": "0x000000000000000000000000000000000000000000000000000000003afc2613" + }, + { + "op": "set", + "key": "0xff0fb23a930b05e3bdfbdd0e6694fa9df9c5059ceabc60618bc40931ddb379f1f6065b90a89bb317ff71b1f933493a08b55f152abe9f5ae5b58400664d141c5121a8", + "value": "0x0000000000000000000000000000000000000000000000000000000032db368d" + }, + { + "op": "set", + "key": "0x0055a6923f7414ba482ae8a64b407f386a0bb0491b742bd4b22b5b4b44fca84e84ee", + "value": "0x00000000000000000000000000000000000000000000000000000000790b45dd" + }, + { + "op": "set", + "key": "0x0078a2812688114caf4452a5611322977e4b1000e59ef408d4c8e2c90028370bcb76", + "value": "0x000000000000000000000000000000000000000000000000000000004ff61c82" + }, + { + "op": "set", + "key": "0x00412c4d14e5bde6ea4c898a1d0f911f12a2cf775a62e3cc4c0f28331d432e8f1060", + "value": "0x0000000000000000000000000000000000000000000000000000000098ae0f91" + }, + { + "op": "set", + "key": "0x00998f08d7054351e5fa3ccadda32ab8d6c0c86a825dd360d57c0fe6b46612297c6d", + "value": "0x00000000000000000000000000000000000000000000000000000000e7bf52c8" + }, + { + "op": "set", + "key": "0x00d1d141c1bed8bb4f809292e9340a0209cd167484a24a5ca993fcac4b97086d89fe", + "value": "0x000000000000000000000000000000000000000000000000000000000c86ac1f" + }, + { + "op": "set", + "key": "0xff543907253acb230f5dc96bbafd8ea0f37dd63b30e8a4a3675bb007673a31fbc54e3628f3b56f95f4e49d0141e2d17a7a533a1aee0d22c6cc49df3fb8b348ad60ff", + "value": "0x00000000000000000000000000000000000000000000000000000000c60fe37a" + }, + { + "op": "set", + "key": "0xff7293ce391f5cd652216ca8ea31069a06a82f393028dfc8786aa5efd7be734b2ca38663c8c6b9a008190c48cd4ee4fd87a0d65a077938f6b18ea2ebb292ca4b0a7e", + "value": "0x00000000000000000000000000000000000000000000000000000000cf47b878" + }, + { + "op": "delete", + "key": "0x007b1eefd69ea014244937aaeaea4e8d4d296c04edd75ae47897e5612ef5a1a2521e" + }, + { + "op": "set", + "key": "0x005963897639cd11d686b199366a1249d76bc89e0f7161c9eca48d9a199435952da0", + "value": "0x00000000000000000000000000000000000000000000000000000000b50709cf" + }, + { + "op": "set", + "key": "0x004c1eb4996f3098c89ee461d61bd5f177587b0f4363ea73f4387e6296c66a260f25", + "value": "0x00000000000000000000000000000000000000000000000000000000b0026e41" + }, + { + "op": "set", + "key": "0x00d1afd6427b13f2ce7beb82a2cd6bbfc58eac8b2d46bc87bfbc78fbf440e18a632c", + "value": "0x000000000000000000000000000000000000000000000000000000003ecb28b7" + }, + { + "op": "set", + "key": "0x0046aeb3243f905d4e0eac7f60ee8962848c87e79dfbd019ece14fe45bd720ea6b0e", + "value": "0x000000000000000000000000000000000000000000000000000000008b1fc034" + }, + { + "op": "set", + "key": "0x00d1afd6427b13f2ce7beb82a2cd6bbfc58eac8b2d46bc87bfbc78fbf440e18a63a2", + "value": "0x00000000000000000000000000000000000000000000000000000000089224ae" + }, + { + "op": "delete", + "key": "0x0015c4813bb5b1499e0a7e0d46daab8e10c70940c008c566d5c68557b1523d1f2999" + }, + { + "op": "set", + "key": "0x004c3a8541d9ec369a9fd4bcdad885cc3b166da70ed6a83066924727d9ff147fce85", + "value": "0x00000000000000000000000000000000000000000000000000000000a940c1b7" + }, + { + "op": "delete", + "key": "0x004c3a8541d9ec369a9fd4bcdad885cc3b166da70ed6a83066924727d9ff147fce85" + }, + { + "op": "delete", + "key": "0xffe57860ba0adcdd1294caf74da8b990a80a8a6861bc01b30dfe07450d25bab38f63b4527cc539651483c1b056383959f91cb98fdbd3c1799a854a2781ddb32290fb" + }, + { + "op": "delete", + "key": "0x00e6b6d8d98be54d4c00cee378bedf964f755b5257bf1eabf52cf39aa987686fa75b" + }, + { + "op": "set", + "key": "0x002cebacb183e2f51a3b6cb9f7fe2bd11c399bf1bdb8ce95ed457d3e46190197a053", + "value": "0x000000000000000000000000000000000000000000000000000000004aff7879" + }, + { + "op": "set", + "key": "0xff0ed33feae99b046218c56bec56750a98beb5fd6a51ec13218024cb2455586b43ce51c5b488a38762ddd335b3b0f645fefed9e6d3f01457ac62638b1dcc5e4e760c", + "value": "0x0000000000000000000000000000000000000000000000000000000029ca0908" + }, + { + "op": "set", + "key": "0x0068e867f5f899fc01d641a00cea2c42d58f3cc061797135198a85301d1e29030605", + "value": "0x00000000000000000000000000000000000000000000000000000000923415f8" + }, + { + "op": "set", + "key": "0x0018e25b9b05a714bd17ede1445ee7c785983a0194a3ed103783662bdc401dd7ffa8", + "value": "0x00000000000000000000000000000000000000000000000000000000bf8ad82f" + }, + { + "op": "delete", + "key": "0x00e6b6d8d98be54d4c00cee378bedf964f755b5257bf1eabf52cf39aa987686fa7f8" + } + ], + "roots_after": [ + "0xcf6ca0dad8d69c45120da214b9052e614649a640e52560d0f47487073325e93d", + "0xa7247b2d9c8c6e49a18add897d235a5e72c292a5144008676972983ebd5e624e", + "0xcf6ca0dad8d69c45120da214b9052e614649a640e52560d0f47487073325e93d", + "0x9b659884c441cf90291fe8e2336c65d34cb0da60709da8a6165486c1a00829e4", + "0x1b5adc55d232b1c9b93c8bae8c1e736a2e596efabd3cc466de195d38ecbe9746", + "0xa604a44382e79cbca1ae6140d150d84980510447609e2a2356dce852861b3217", + "0x67994f2a47248a4677f1691706ec0ad76d479894fb7015f06bda6d4af4d46a55", + "0x99b2ce17b9b779b3f63f68d218182a59b928c80c1ac8d8bcb716cd3c202a20b3", + "0xda86ba5028d29db7d6006f7cf340f6af5203506cbcc76e72ba27302933a4943f", + "0xaa845405281cd62dc569bf03f8a6c1bd35609cd2c1860db56a9478254f8cb25e", + "0x1708bbd96a838a1f0816667f74a2d2a1a01b8c5499bcb533be4a45cf684b8bef", + "0xf8850e79c7adbebb051eb0ea11e3ca9866c11f660cb2b7d2c3892beb20246f26", + "0xc6c221bffc3947d15ad0e994e150fa0e654897bde5039e88bcda287cf24dc827", + "0xf1036ba9ebb2780554ab2596581122a999fe301b33446a3bf13c623dd8796f96", + "0x61cd8daf48607b812e063019741d2ae0d77bf9193b2b5f5996b756a22a488b1f", + "0x73aaf9a0a5b4f179bacf1f1c2fbcb630163797d02cdc5115977d26e8ea9f48ff", + "0xff83d59bd08841abbfc5cf10620dd334ec2fbb9bd0e579aec30905b4e40ccab9", + "0x44b55ce7592e1ba9aa3ea9c465cdadee3f7e09abccbec49aec6290eab112d0a4", + "0xe1d9ef033ebdf35275483848038f858fa43063805c1cc1b5ea96654745cc3b94", + "0xcff0d627850d4d4ea368e66ff2349c0caf454129e387d23df3c8dcf71534768f", + "0xb360073c9f6fea4b2b8613ce10a4dd82166b7b0cf4a58cf03d05b6e1bf0a603a", + "0x208c7e70c17d7209c3694fbf9bcf7e9e7874c87ce2c44b6be466a2356747c308", + "0x9526700579706ca7fdbf2f9559d2e9fa2cb2687e811098ea6b19d7b5e4580723", + "0x2ba99e97e461801969c394fa8826fa3e9d0d7d2fc4c530687a04911e41c276b6", + "0x29b448496222b5901eb9317dcece4d5df49ceda5956390b19ca5d4e72a7aca4b", + "0xda1d4cf6de46c0cdff6d48b8a0c116df3b6b0844799c3c83e8bb694f22415c7e", + "0xb431f7d742f75fbc4b008e8576ce79c98d5e62fee5da59aea65ea1824481dc58", + "0x497441710aa139263ef9fbb9550e582d3f68ab98187bc980fd5f1e1cc77b1915", + "0xc4b25bd3a6559c7a169bdb71e97383ec9a44bda2324704a29f0d3e3c05e109af", + "0xb1ac7843c5e776c94da54b0471ec3f7ec058155491be05b8a4a6a914f532fece", + "0xf04c71372a36681f215c7626a234518f4a397e9ce7ea69b1969da0dbec5c6aba", + "0x963bf8843b41679c0c805fe2e078c88afc89e800ae68c7bac8654d2b9fae8275", + "0xc937494bae732ef43bda9f36f41d7811b724a0000c7f12b18d68fa244f85f83a", + "0xa1b52f51aff2ec9148e27600812a6f7f250b193d547541cb7291ac5b27531377", + "0x49e7d2ed49f0a16a80f9ee32cff2b92b1c5563ca2976dedf4377faa32934b587", + "0x142c035ccdb7dbb9e7c964dd932f0cacd28a730334a714894d87c1d473694115", + "0x257026d0fdee39ac10d16f45fc41bdc67cb7c65f5462b162e97123335a721087", + "0x44210262b17ef2ce604b8f760e40ba4f6810d096a9c6badf610919a4c240e106", + "0x34b2eccd7b5f3767b7b379e23a79b51cb5aa96bc00d14860b85ff4484ad28616", + "0x06d575937706dcab6c13c7c5c50f2dc50347560e8cfcf7d41558b3ea3143a051", + "0xbf5e02b80d4a44b750e14e5005c55693c9c04c17da5af780b76297c1ab54e5a6", + "0x2556391529759b8428f4fe99357b3137b3448f8d7a6ea6935c4f93aa38dc8373", + "0x0798de4a687305f7f63fdc90196e8ae7c40b1abf8b864d169bd5e1e27afa451f", + "0x6a278acb5e817c9bda137843e72bf5472cc85dea63b0a83b8f026d757bfe0e9d", + "0x861f2f4c797ddb8653b22fc5cfea3c015c9c4e9cc361b16f0aad3a0f3a007e23", + "0xc2c1120e643b57b9fdc8951f313a49e5cbd36b6597a1dae37d5966202550ccc1", + "0x8b9d66932ed20ec20776164266f4aafcfe0dfd9e6abda346378b4a128e2a8663", + "0xc1ecbbfc8782deb03b2449a28213cb163a83d00b84a9ea8b1587850c6339a75d", + "0xed23f054d3766b19c862cf082afae679e31dbd3608bd16c2de0a1cf10f2e57ef", + "0x8ef00a29a96eddcae4bc1747194add0f6b633046912a15e065422bb15a02a821", + "0x0b1166c05d3c49875381e7238db4e9a2bd0f31172ffd4640333a94787f78c6a2", + "0x7c9f26d17ce7379676d56b608379b4af2d5f5c8a66422eebd0f2c2ad1d47cc7b", + "0x065ba2ffdead957828b37f2315ac3d4a278a916df97c15e871b311e6975c6a62", + "0x3b65db9ab1623eafbb0be692575c1a0fe8c89e3c44c31c0ae4b8f7e49b143d6b", + "0x248a7ddabf2e6682ad71ec54ce32f23c2b34cc8ecb009b4ea05674e593b15c6e", + "0xe5e21bec2c3bab941951849a4d26d929f0e2ba3cd1fc0a036c8eb7d89a9875ec", + "0x1ac665122b19a6dc65665cae30c062c630502c6383d8a9789a5707a046886936", + "0xf7090e29c8d58586496cefd6be820c5ed07df662e8bf7bb20417990caa798083", + "0xc7c92a89957d8bcde3d89a06af03ba1ffed14a91f322aa9e5c0e8cb4add1b54e", + "0xc7c8c808f74ee9820231ed33be6f19507361432d9db3754eca149722276e3d7b", + "0xfd83b3ef076ffc238e781a0d0e5581667ec298df0392fd1995d4e85a07733ec3", + "0x9f6e3c675e6849dc67b4e8bf74c7ab92625029aea49a457c5fa5a40ccfad8bfb", + "0xec7f5ff4c9a4223a9d5136dfee1781491caf7e2eb6f1bc7b13d63e6339dbb0be", + "0xeab647ff5c2d31ee101e1b14d01c1b19fa9278fc0d6ad87c1ad34d8cb139c69f", + "0x9f59d79991a7dacd90df4ebff5fad773e8cfa2a7aad270e53f95ff5d51025d14", + "0x40c37dfc185c9ad30b36a6c3d70c261d6929dc95dba1d21bb8f21d36d70ac7ae", + "0xcd9f2abf02b4c8dce567e841164ef0aa9878975c17e7d0cd6ec2a283f8bacc0b", + "0x237cd11f13b7f4def4bffba3ca20cfba81257eba3371da7b9b959ab91b92eae9", + "0xb40d35cdf4fec5f5712567d8ffe3755cf06525413569717a6b396c0628298a2a", + "0xcf4a9616f8c8b981cf9e907e11e2065d4ae55936d73838a0b80928c228989a79", + "0xf9c724516024072146d39cba478679a2b72bfecf65c5e4bf6630adb8a5001f69", + "0xb19da381011d4e18e47f011a3649ef120bc64b7e1155f58ee57a23a7c3fdfb0f", + "0xc5cfb10924de3fa07e5915085a6b9bfc16921211247b530bdd6072b61342e58f", + "0x2bc4d35f7b4ad0058ad3cc3643c8e30273c83177e2cad26d3aee861e39a849a4", + "0x0406ed0eb0da6947478234d14425dba144f51b93fe2bcc0ee7665410780f8c09", + "0x08273095920f4b0b4d8ec6f39d4b14852d329ca089c87c79e086488368245dca", + "0xe3b6c6491a6028f66dff35de1f6d1bab2f197fecf10f986880b5a5cf340a4835", + "0xd35bf68cd5bae8ba07b9e2866704ee68284690646176e0fe2f2b41c0ade6ee80", + "0x8fab00b973dfcb6003c0f8d9e91a48c90ee220413c4ab4f92261275d7586e9a1", + "0xfe4795724e5b9f8d80b75caf4dd1b5dd3b3f12fe338537ee36663e2f3e63a5ba", + "0x132d929abced32b1e26c9311a9e6abaf9e562beddac5ccfef40f154a1ba4396f", + "0x2520dd342924ff668896b4a6edcdcc770694c5419f363a45cec4474e48033553", + "0xa48fc239a6d81a479e40eb89781d3c2e7143ab32bf70e6a6bf4bb0c6e940c1b8", + "0x670a87d76a7eb5b1bad2ad6a7242e8fe3620207d34e12ee1ca912c9711d926cd", + "0x31517aec30a94303afef5d99b31932f01af948a02413c3c780f4e0abcf76ffce", + "0x06f21ad8ddeb29f151d30788940737d13659a69e10a8341931e5110de0b78de1", + "0xc508e6241e421a9699d4a9a066c2a12e43ffe9d95d716bb0b40454676869f35d", + "0x7c0b579b0e3567f75fe3e8ee2c5892c66b438ef5b31bced869c84891358d5d5b", + "0xb2aaeec514853a4dbfb295b700c7cf634bbc544e129524bf15c2fbc544177666", + "0x9824d449ce4cfd0b389a62c6c2b63651ff11ec5f9671437bea5f59f9635ebbd7", + "0x0f368a50ab7d5eefa7dd840a5e5b10e36b9b6e4342d851ddde4c65ec51e014fd", + "0x148f52411c2264f84d5fa98f63eb355575874db367701f6d4d44162a0e688450", + "0x1bf78390c33f7f04f515f0c806dcb716eaf40048b0a89cd0f2aecc7c04c45586", + "0x3b3a18162aaa32dc6344b386d64cac002639d6a47d13eb60b94b1890e5deecde", + "0xcb6fc59c42d9c64244d7110dd91f8af058d25859c4c48194f77b4648751d1f73", + "0x62f208cc34424fbc0c40ed208049c677655e9a443afa82f931575897df26bca7", + "0xc7a8ecbccd8bf1975a3d820a43fa7f490777c827703fa340b5acd7801a63a37f", + "0x2686b1ead22026fdd6fbf3efd5d72d58987db12a75ea4c7c6484a8bc7b693145", + "0xfeff75d397216247329810da9c4e1585d875a27c9ba0aee51f4dfd679f02a17c", + "0xc8c528b117b2e87fb327884281b6afa72edde356d92f443803b0c60e1d45fb8f", + "0x08c7cfe28bf531575668da302173840473511a38e10857f0eb61a09be7959559", + "0x4a2520c9a2e39870e8be946efd1dec6221fb323eec658ad7961a728fce936fb7", + "0x77b6bba491fbc13dd07b8679026034494fbf53cffef0992e1e2005c3dfcfed45", + "0xe3c3ebd215b446c5aa9d2923e80b1bef94a3b424a789455fed664556de879e05", + "0x4edb1a5ff8f4bfcd0230433b7c210a55a4f2f78fa5c2bf406d6cbe9df3c86475", + "0xa635fa92e36e6939836f612e3a39ac974a6a38c32e61ef4f26225509e6f04492", + "0x7632441b27590957b807b49ab1a6ebd2c481a8fa2b32c9f052a7315740ae8c69", + "0xb9e84c69e52ffaf839e02addbe4a2ad0d1d2d2803e203e63a0035012435ac5d7", + "0xecb9401c0a9b1408d3e201fac4aa2d5a7d53a0858788ae2cff8df6634ce84dbf", + "0x8972767bae825a2e3335c8067c2d6442f4f98b23cd402638b4a2b9f54811a8eb", + "0x39ad2334dd4be73bd16b9f82c28d1351bcd1e786c9af8fa7613746f983bfabf6", + "0xab65d7a1eabebf9a1cf96fe7ff3c34a4dfd2d192991fafeaf30397c96079c7c5", + "0x39ad2334dd4be73bd16b9f82c28d1351bcd1e786c9af8fa7613746f983bfabf6", + "0xafd2dd6b97b8c543460cdd780aff2d87ceb9db784bfe7a686af6e858c3a22555", + "0x41b885d3566da95eba3e8036b50242145b3d8eabdf0db050a4d8f40e406087b9", + "0x3500c426109dd2f374df8ae87fc054bff2e222831ef21dfd6a77146e8f8d7e91", + "0x853cc2b1cea36805654ad6d2137f65bc443edb7829ea6cda66df575d9a098404", + "0x5acd94e7c18c7b26b7dee3716e72e8fc096dc2e53ca281034aa34143b2cadab2", + "0x64f69ee1df07d749cae00990210420c8ed362fa2e0d63391267d3e34323da6bc", + "0xaa7c0921513588b382bb634d1cf114bbfd3c72b0033b50503101e9c2b3025bd4" + ] + }, + { + "seed": 11832, + "ops": [ + { + "op": "set", + "key": "0xff1df77729bb76b139342cfc86aff011590bf6bb810f47e69a6babac30af96f9f5ffa1e869c4566231d09357daec90e71aa93821a7347b29175ef4a1bc02a0b38120", + "value": "0x0000000000000000000000000000000000000000000000000000000079b57838" + }, + { + "op": "set", + "key": "0xff3cbfd03e3f083b0987f841c4372031faf655a63472518a3613e2e30ecb542b51ef2c19e3ed459de7a67968b7004609b54e8e34fef08d7f9ed3c6eedcc980488255", + "value": "0x00000000000000000000000000000000000000000000000000000000449c8b5d" + }, + { + "op": "set", + "key": "0x007b1eefd69ea014244937aaeaea4e8d4d296c04edd75ae47897e5612ef5a1a25289", + "value": "0x00000000000000000000000000000000000000000000000000000000b5b13d29" + }, + { + "op": "set", + "key": "0xffa43fb2d7beb12b34786a837467f62b7d6c5dab4ab7eecc89c0b67db76a6829272094bf023796a3d4bdc16643cddf5395ab048ba346a4f50f1cc64da78ad7dd5266", + "value": "0x000000000000000000000000000000000000000000000000000000008cc69019" + }, + { + "op": "set", + "key": "0xff5963897639cd11d686b199366a1249d76bc89e0f7161c9eca48d9a199435952d4a28b184a4e022d38844c7967da6fbdf879ffafc6cfabf88c929ec9c9ff90040d1", + "value": "0x00000000000000000000000000000000000000000000000000000000af9bbd7d" + }, + { + "op": "set", + "key": "0x00e61994d31ceae39cae5feb02303eb90e0d4ae90f5b0755c3c26adc81f360ec85bf", + "value": "0x000000000000000000000000000000000000000000000000000000005dde837c" + }, + { + "op": "delete", + "key": "0xffa43fb2d7beb12b34786a837467f62b7d6c5dab4ab7eecc89c0b67db76a6829272094bf023796a3d4bdc16643cddf5395ab048ba346a4f50f1cc64da78ad7dd5266" + }, + { + "op": "delete", + "key": "0xff1df77729bb76b139342cfc86aff011590bf6bb810f47e69a6babac30af96f9f5ffa1e869c4566231d09357daec90e71aa93821a7347b29175ef4a1bc02a0b38120" + }, + { + "op": "delete", + "key": "0xff3cbfd03e3f083b0987f841c4372031faf655a63472518a3613e2e30ecb542b51ef2c19e3ed459de7a67968b7004609b54e8e34fef08d7f9ed3c6eedcc980488255" + }, + { + "op": "delete", + "key": "0x00e61994d31ceae39cae5feb02303eb90e0d4ae90f5b0755c3c26adc81f360ec85bf" + }, + { + "op": "set", + "key": "0xff0fb23a930b05e3bdfbdd0e6694fa9df9c5059ceabc60618bc40931ddb379f1f6065b90a89bb317ff71b1f933493a08b55f152abe9f5ae5b58400664d141c512119", + "value": "0x000000000000000000000000000000000000000000000000000000000a082d85" + }, + { + "op": "set", + "key": "0xff998f08d7054351e5fa3ccadda32ab8d6c0c86a825dd360d57c0fe6b46612297c725184c1ce99f155151a19aa14a427e1a784778319e2b193ac66bd6374b328cdff", + "value": "0x00000000000000000000000000000000000000000000000000000000a3ea3eb4" + }, + { + "op": "set", + "key": "0xff78a2812688114caf4452a5611322977e4b1000e59ef408d4c8e2c90028370bcb71e2e06bd4204550cd67cae560e42f8afcdc75e454a14f00ed2a8ec7c885869e40", + "value": "0x000000000000000000000000000000000000000000000000000000007435a9e4" + }, + { + "op": "delete", + "key": "0xff0fb23a930b05e3bdfbdd0e6694fa9df9c5059ceabc60618bc40931ddb379f1f6065b90a89bb317ff71b1f933493a08b55f152abe9f5ae5b58400664d141c512119" + }, + { + "op": "set", + "key": "0x000fb23a930b05e3bdfbdd0e6694fa9df9c5059ceabc60618bc40931ddb379f1f668", + "value": "0x000000000000000000000000000000000000000000000000000000000275abc8" + }, + { + "op": "delete", + "key": "0xff5963897639cd11d686b199366a1249d76bc89e0f7161c9eca48d9a199435952d4a28b184a4e022d38844c7967da6fbdf879ffafc6cfabf88c929ec9c9ff90040d1" + }, + { + "op": "set", + "key": "0x007293ce391f5cd652216ca8ea31069a06a82f393028dfc8786aa5efd7be734b2cbe", + "value": "0x0000000000000000000000000000000000000000000000000000000094f87f55" + }, + { + "op": "set", + "key": "0xff4c3a8541d9ec369a9fd4bcdad885cc3b166da70ed6a83066924727d9ff147fce963472d01342d91a0a268bdfee2ecf01a6d30f9021151faf2e2e6719120bf40f0a", + "value": "0x00000000000000000000000000000000000000000000000000000000fdac9fff" + }, + { + "op": "set", + "key": "0xff1df77729bb76b139342cfc86aff011590bf6bb810f47e69a6babac30af96f9f5ffa1e869c4566231d09357daec90e71aa93821a7347b29175ef4a1bc02a0b38181", + "value": "0x00000000000000000000000000000000000000000000000000000000e4d876b8" + }, + { + "op": "set", + "key": "0x00e2f41518226671332da4adddab1fdcb9903c7b895c7943b669067fe96b0baacfd5", + "value": "0x0000000000000000000000000000000000000000000000000000000019be8821" + }, + { + "op": "set", + "key": "0x0046aeb3243f905d4e0eac7f60ee8962848c87e79dfbd019ece14fe45bd720ea6b40", + "value": "0x00000000000000000000000000000000000000000000000000000000a4a3d6a1" + }, + { + "op": "delete", + "key": "0x007293ce391f5cd652216ca8ea31069a06a82f393028dfc8786aa5efd7be734b2cbe" + }, + { + "op": "set", + "key": "0x0034bb4f3468340d5f72275da92420aed94db48bbc61823e92f29f14f51590373a6e", + "value": "0x00000000000000000000000000000000000000000000000000000000f8bb0323" + }, + { + "op": "delete", + "key": "0x00e2f41518226671332da4adddab1fdcb9903c7b895c7943b669067fe96b0baacfd5" + }, + { + "op": "delete", + "key": "0x007b1eefd69ea014244937aaeaea4e8d4d296c04edd75ae47897e5612ef5a1a25289" + }, + { + "op": "set", + "key": "0xff4b83f061efb2f4708114e46b5ef50a255528c28c80bbe5fc6ad88e892bd4c4ea4b53fa472b269bd8517302932b840c9802e6edca11b366371a9b7cd8c280f791e9", + "value": "0x00000000000000000000000000000000000000000000000000000000c87c281b" + }, + { + "op": "set", + "key": "0x00e57860ba0adcdd1294caf74da8b990a80a8a6861bc01b30dfe07450d25bab38f2a", + "value": "0x00000000000000000000000000000000000000000000000000000000a34f6676" + }, + { + "op": "delete", + "key": "0xff78a2812688114caf4452a5611322977e4b1000e59ef408d4c8e2c90028370bcb71e2e06bd4204550cd67cae560e42f8afcdc75e454a14f00ed2a8ec7c885869e40" + }, + { + "op": "set", + "key": "0xff5ac76a8f23131e4ca988c8afcc313130cc95e521d2691635fc26196d63e66a1f893d3c65a10fbfddef979c778efdeaf625c4d5326e417354a396e996bacabea50e", + "value": "0x00000000000000000000000000000000000000000000000000000000cfe3e137" + }, + { + "op": "delete", + "key": "0xff998f08d7054351e5fa3ccadda32ab8d6c0c86a825dd360d57c0fe6b46612297c725184c1ce99f155151a19aa14a427e1a784778319e2b193ac66bd6374b328cdff" + }, + { + "op": "delete", + "key": "0xff4b83f061efb2f4708114e46b5ef50a255528c28c80bbe5fc6ad88e892bd4c4ea4b53fa472b269bd8517302932b840c9802e6edca11b366371a9b7cd8c280f791e9" + }, + { + "op": "set", + "key": "0x0078a2812688114caf4452a5611322977e4b1000e59ef408d4c8e2c90028370bcb61", + "value": "0x000000000000000000000000000000000000000000000000000000005fd626fb" + }, + { + "op": "set", + "key": "0xff4c1eb4996f3098c89ee461d61bd5f177587b0f4363ea73f4387e6296c66a260fb343f74f407821bc354e33da7a0ff703c3a0b8a8ba6ed25c75dddafc0182fa025f", + "value": "0x00000000000000000000000000000000000000000000000000000000e8af3939" + }, + { + "op": "set", + "key": "0x00998f08d7054351e5fa3ccadda32ab8d6c0c86a825dd360d57c0fe6b46612297c64", + "value": "0x00000000000000000000000000000000000000000000000000000000c60d0d76" + }, + { + "op": "set", + "key": "0xffe57860ba0adcdd1294caf74da8b990a80a8a6861bc01b30dfe07450d25bab38f633bf20195771cd09de04706b99eda30093d53b7c3b76cc8888c52db5ac6b223c6", + "value": "0x0000000000000000000000000000000000000000000000000000000001e813df" + }, + { + "op": "set", + "key": "0xffe2f41518226671332da4adddab1fdcb9903c7b895c7943b669067fe96b0baacf7d209e014165b283a74c97557e34dd6f49626dcfa254a7af4fc2ec4b667c55093a", + "value": "0x00000000000000000000000000000000000000000000000000000000270d998e" + }, + { + "op": "set", + "key": "0xff7293ce391f5cd652216ca8ea31069a06a82f393028dfc8786aa5efd7be734b2ca38663c8c6b9a008190c48cd4ee4fd87a0d65a077938f6b18ea2ebb292ca4b0a5d", + "value": "0x00000000000000000000000000000000000000000000000000000000d7608e7c" + }, + { + "op": "set", + "key": "0x00c28c21982a56a6cecf595bfeb78297c7432abfb487cc75c09db2856a118f441db2", + "value": "0x00000000000000000000000000000000000000000000000000000000c4224d37" + }, + { + "op": "delete", + "key": "0xff4c1eb4996f3098c89ee461d61bd5f177587b0f4363ea73f4387e6296c66a260fb343f74f407821bc354e33da7a0ff703c3a0b8a8ba6ed25c75dddafc0182fa025f" + }, + { + "op": "set", + "key": "0x00d1d141c1bed8bb4f809292e9340a0209cd167484a24a5ca993fcac4b97086d890e", + "value": "0x00000000000000000000000000000000000000000000000000000000b90275c0" + }, + { + "op": "set", + "key": "0xff998f08d7054351e5fa3ccadda32ab8d6c0c86a825dd360d57c0fe6b46612297c725184c1ce99f155151a19aa14a427e1a784778319e2b193ac66bd6374b328cd81", + "value": "0x0000000000000000000000000000000000000000000000000000000032b77bf5" + }, + { + "op": "set", + "key": "0x009c847c439d6431d36ddeb326cb553da2e967778a24afbd1d23a62412186faf01bc", + "value": "0x0000000000000000000000000000000000000000000000000000000088b6deef" + }, + { + "op": "set", + "key": "0x00c28c21982a56a6cecf595bfeb78297c7432abfb487cc75c09db2856a118f441da5", + "value": "0x0000000000000000000000000000000000000000000000000000000026fe225f" + }, + { + "op": "delete", + "key": "0xffe2f41518226671332da4adddab1fdcb9903c7b895c7943b669067fe96b0baacf7d209e014165b283a74c97557e34dd6f49626dcfa254a7af4fc2ec4b667c55093a" + }, + { + "op": "set", + "key": "0x00e61994d31ceae39cae5feb02303eb90e0d4ae90f5b0755c3c26adc81f360ec8542", + "value": "0x000000000000000000000000000000000000000000000000000000004311a76d" + }, + { + "op": "delete", + "key": "0x0078a2812688114caf4452a5611322977e4b1000e59ef408d4c8e2c90028370bcb61" + }, + { + "op": "set", + "key": "0xff46aeb3243f905d4e0eac7f60ee8962848c87e79dfbd019ece14fe45bd720ea6b919d757e67485d253a689ff08c388cf32b629d2fb6fc4c514296d9b64fb4e58d12", + "value": "0x0000000000000000000000000000000000000000000000000000000084c560be" + }, + { + "op": "set", + "key": "0xffbded9122e6171a5dad9ce2aaebd933abf4718e9bf12448cf113064c9fd8e6ad0551bc91eb18a241f3262e8f9c56018ae23b3e1e9d55d93bf499ad6f830b8447f6a", + "value": "0x0000000000000000000000000000000000000000000000000000000014a070f9" + }, + { + "op": "set", + "key": "0x00e60d91d1d11837912311124089c6b73c9d726c99f4df797eb48c2b7fec6e5e2c18", + "value": "0x00000000000000000000000000000000000000000000000000000000226a563e" + }, + { + "op": "set", + "key": "0x000fb23a930b05e3bdfbdd0e6694fa9df9c5059ceabc60618bc40931ddb379f1f625", + "value": "0x0000000000000000000000000000000000000000000000000000000011ca60ad" + }, + { + "op": "delete", + "key": "0x00998f08d7054351e5fa3ccadda32ab8d6c0c86a825dd360d57c0fe6b46612297c64" + }, + { + "op": "set", + "key": "0x00e6b6d8d98be54d4c00cee378bedf964f755b5257bf1eabf52cf39aa987686fa786", + "value": "0x00000000000000000000000000000000000000000000000000000000622a6640" + }, + { + "op": "set", + "key": "0xffc28c21982a56a6cecf595bfeb78297c7432abfb487cc75c09db2856a118f441df2a84c02b0cbaafcabc4cbb5f988d916f3287032f07e3f8bf51588b4311ed04c2e", + "value": "0x0000000000000000000000000000000000000000000000000000000021e1aa16" + }, + { + "op": "set", + "key": "0xffc28c21982a56a6cecf595bfeb78297c7432abfb487cc75c09db2856a118f441df2a84c02b0cbaafcabc4cbb5f988d916f3287032f07e3f8bf51588b4311ed04cc6", + "value": "0x0000000000000000000000000000000000000000000000000000000068067cb5" + }, + { + "op": "delete", + "key": "0xff998f08d7054351e5fa3ccadda32ab8d6c0c86a825dd360d57c0fe6b46612297c725184c1ce99f155151a19aa14a427e1a784778319e2b193ac66bd6374b328cd81" + }, + { + "op": "set", + "key": "0x0068e867f5f899fc01d641a00cea2c42d58f3cc061797135198a85301d1e29030671", + "value": "0x00000000000000000000000000000000000000000000000000000000e9f7d15a" + }, + { + "op": "delete", + "key": "0x00c28c21982a56a6cecf595bfeb78297c7432abfb487cc75c09db2856a118f441db2" + }, + { + "op": "set", + "key": "0x00e2f41518226671332da4adddab1fdcb9903c7b895c7943b669067fe96b0baacf9d", + "value": "0x0000000000000000000000000000000000000000000000000000000020f14759" + }, + { + "op": "set", + "key": "0xff5ac76a8f23131e4ca988c8afcc313130cc95e521d2691635fc26196d63e66a1f33b1e44125772918b9d44a12766eb0dad773e863d120780173c394789aa668e9ab", + "value": "0x00000000000000000000000000000000000000000000000000000000e1d034a1" + }, + { + "op": "set", + "key": "0x000861030791246737d706c0d0017de01a26278bbe4ce8a134f5e0cd6f7eb69bcf67", + "value": "0x00000000000000000000000000000000000000000000000000000000ee7dbf42" + }, + { + "op": "set", + "key": "0x00c901cfbb39e551189323bea0af6cf9e81008c90dce65cbc2ff4672e263d7bb8795", + "value": "0x00000000000000000000000000000000000000000000000000000000cb564e14" + }, + { + "op": "set", + "key": "0x00e57860ba0adcdd1294caf74da8b990a80a8a6861bc01b30dfe07450d25bab38fa4", + "value": "0x00000000000000000000000000000000000000000000000000000000cc0627e7" + }, + { + "op": "set", + "key": "0xff543907253acb230f5dc96bbafd8ea0f37dd63b30e8a4a3675bb007673a31fbc54e3628f3b56f95f4e49d0141e2d17a7a533a1aee0d22c6cc49df3fb8b348ad60ea", + "value": "0x00000000000000000000000000000000000000000000000000000000d00483f6" + }, + { + "op": "delete", + "key": "0x00e57860ba0adcdd1294caf74da8b990a80a8a6861bc01b30dfe07450d25bab38f2a" + }, + { + "op": "set", + "key": "0x0068e867f5f899fc01d641a00cea2c42d58f3cc061797135198a85301d1e2903067f", + "value": "0x000000000000000000000000000000000000000000000000000000007989dc89" + }, + { + "op": "delete", + "key": "0xff7293ce391f5cd652216ca8ea31069a06a82f393028dfc8786aa5efd7be734b2ca38663c8c6b9a008190c48cd4ee4fd87a0d65a077938f6b18ea2ebb292ca4b0a5d" + }, + { + "op": "set", + "key": "0x00bded9122e6171a5dad9ce2aaebd933abf4718e9bf12448cf113064c9fd8e6ad00e", + "value": "0x0000000000000000000000000000000000000000000000000000000094bd932f" + }, + { + "op": "set", + "key": "0xffe6b6d8d98be54d4c00cee378bedf964f755b5257bf1eabf52cf39aa987686fa7eba9c3c87289872a212b18c553d1652d395a1ec2ba73555b79fc5c8a5b880c3685", + "value": "0x00000000000000000000000000000000000000000000000000000000cc2b7c13" + }, + { + "op": "set", + "key": "0x003cbfd03e3f083b0987f841c4372031faf655a63472518a3613e2e30ecb542b5132", + "value": "0x00000000000000000000000000000000000000000000000000000000414b993e" + }, + { + "op": "set", + "key": "0xff0861030791246737d706c0d0017de01a26278bbe4ce8a134f5e0cd6f7eb69bcfd972a2766ba0be044a7342182891121b96cd6d1f005fd95f01ffc9b24311a22fed", + "value": "0x00000000000000000000000000000000000000000000000000000000353a2c08" + }, + { + "op": "set", + "key": "0x0078a2812688114caf4452a5611322977e4b1000e59ef408d4c8e2c90028370bcb59", + "value": "0x000000000000000000000000000000000000000000000000000000004ba4d334" + }, + { + "op": "set", + "key": "0xff4c1eb4996f3098c89ee461d61bd5f177587b0f4363ea73f4387e6296c66a260f84715f048d0204439e2d98c4c2e5cb144730a98fb2ee40b399d12f783c1fbd31b5", + "value": "0x0000000000000000000000000000000000000000000000000000000090d86f57" + }, + { + "op": "set", + "key": "0x00543907253acb230f5dc96bbafd8ea0f37dd63b30e8a4a3675bb007673a31fbc551", + "value": "0x00000000000000000000000000000000000000000000000000000000066f4f8c" + }, + { + "op": "set", + "key": "0x00c28c21982a56a6cecf595bfeb78297c7432abfb487cc75c09db2856a118f441d34", + "value": "0x00000000000000000000000000000000000000000000000000000000696c1855" + }, + { + "op": "set", + "key": "0xff18e25b9b05a714bd17ede1445ee7c785983a0194a3ed103783662bdc401dd7ffd358410b57fde52d1120a72e241687991ced2ec523b82168713b13b557349ab807", + "value": "0x0000000000000000000000000000000000000000000000000000000063765b8b" + }, + { + "op": "set", + "key": "0xffe60d91d1d11837912311124089c6b73c9d726c99f4df797eb48c2b7fec6e5e2c6066e8c722a574a900a59551ed956d32ca2b36e48771ed8b373da6b223bbbe9c00", + "value": "0x00000000000000000000000000000000000000000000000000000000a429e408" + }, + { + "op": "delete", + "key": "0xff543907253acb230f5dc96bbafd8ea0f37dd63b30e8a4a3675bb007673a31fbc54e3628f3b56f95f4e49d0141e2d17a7a533a1aee0d22c6cc49df3fb8b348ad60ea" + }, + { + "op": "set", + "key": "0x00c28c21982a56a6cecf595bfeb78297c7432abfb487cc75c09db2856a118f441d7b", + "value": "0x00000000000000000000000000000000000000000000000000000000b8e97a76" + }, + { + "op": "delete", + "key": "0x00e61994d31ceae39cae5feb02303eb90e0d4ae90f5b0755c3c26adc81f360ec8542" + }, + { + "op": "delete", + "key": "0x00e57860ba0adcdd1294caf74da8b990a80a8a6861bc01b30dfe07450d25bab38fa4" + }, + { + "op": "delete", + "key": "0xffe6b6d8d98be54d4c00cee378bedf964f755b5257bf1eabf52cf39aa987686fa7eba9c3c87289872a212b18c553d1652d395a1ec2ba73555b79fc5c8a5b880c3685" + }, + { + "op": "set", + "key": "0x00e57860ba0adcdd1294caf74da8b990a80a8a6861bc01b30dfe07450d25bab38f21", + "value": "0x00000000000000000000000000000000000000000000000000000000ce5959ab" + }, + { + "op": "delete", + "key": "0x000861030791246737d706c0d0017de01a26278bbe4ce8a134f5e0cd6f7eb69bcf67" + }, + { + "op": "set", + "key": "0xff1df77729bb76b139342cfc86aff011590bf6bb810f47e69a6babac30af96f9f5ffa1e869c4566231d09357daec90e71aa93821a7347b29175ef4a1bc02a0b38153", + "value": "0x000000000000000000000000000000000000000000000000000000004dcbc52d" + }, + { + "op": "delete", + "key": "0x00c901cfbb39e551189323bea0af6cf9e81008c90dce65cbc2ff4672e263d7bb8795" + }, + { + "op": "set", + "key": "0xff7293ce391f5cd652216ca8ea31069a06a82f393028dfc8786aa5efd7be734b2c5c915dbeeb53329739ac0626eb5034da5b64e4e02399423c11fa9c3245cdba5a53", + "value": "0x000000000000000000000000000000000000000000000000000000008640a4f1" + }, + { + "op": "set", + "key": "0xff0fb23a930b05e3bdfbdd0e6694fa9df9c5059ceabc60618bc40931ddb379f1f69b7b14fb2b661d561b45215872510d7417350a448e39ebefec4984fbfc5722eb07", + "value": "0x00000000000000000000000000000000000000000000000000000000609bc375" + }, + { + "op": "set", + "key": "0xff412c4d14e5bde6ea4c898a1d0f911f12a2cf775a62e3cc4c0f28331d432e8f10c065da61a7bd36e4bd11a16fd628e8bd5c6f79eb75cbc8ea1aa2ac6d870713c2f6", + "value": "0x00000000000000000000000000000000000000000000000000000000af79da23" + }, + { + "op": "set", + "key": "0x00d1d141c1bed8bb4f809292e9340a0209cd167484a24a5ca993fcac4b97086d8952", + "value": "0x000000000000000000000000000000000000000000000000000000003be4e1a3" + }, + { + "op": "set", + "key": "0x004c1eb4996f3098c89ee461d61bd5f177587b0f4363ea73f4387e6296c66a260f5e", + "value": "0x0000000000000000000000000000000000000000000000000000000061bf8586" + }, + { + "op": "delete", + "key": "0xffe57860ba0adcdd1294caf74da8b990a80a8a6861bc01b30dfe07450d25bab38f633bf20195771cd09de04706b99eda30093d53b7c3b76cc8888c52db5ac6b223c6" + }, + { + "op": "set", + "key": "0xff1df77729bb76b139342cfc86aff011590bf6bb810f47e69a6babac30af96f9f5451673405763851d69ce90fb56d8dd98d3ef3ad6e6deccec4a6cc4303c0460632f", + "value": "0x000000000000000000000000000000000000000000000000000000005f032ac1" + }, + { + "op": "set", + "key": "0xffc28c21982a56a6cecf595bfeb78297c7432abfb487cc75c09db2856a118f441d4d68c9b03da1dec117fa853416c0249fb7b863e57d51f6ac8e86e023c336f4688d", + "value": "0x00000000000000000000000000000000000000000000000000000000be4a0420" + }, + { + "op": "set", + "key": "0xff4b83f061efb2f4708114e46b5ef50a255528c28c80bbe5fc6ad88e892bd4c4ea4b53fa472b269bd8517302932b840c9802e6edca11b366371a9b7cd8c280f79190", + "value": "0x000000000000000000000000000000000000000000000000000000005f60883d" + }, + { + "op": "set", + "key": "0xff412c4d14e5bde6ea4c898a1d0f911f12a2cf775a62e3cc4c0f28331d432e8f10fb3754ee233f7964e47e5e73dc4342ee28be4dcf5e0bb64009bcbfd0c440559c06", + "value": "0x00000000000000000000000000000000000000000000000000000000d2bc3cb2" + }, + { + "op": "set", + "key": "0xffe61994d31ceae39cae5feb02303eb90e0d4ae90f5b0755c3c26adc81f360ec854d6578131d380078a46e85324e6c5548cbb1581bf0c2505cedd28736583e573019", + "value": "0x0000000000000000000000000000000000000000000000000000000074c5e9a9" + }, + { + "op": "set", + "key": "0x0068e867f5f899fc01d641a00cea2c42d58f3cc061797135198a85301d1e29030601", + "value": "0x000000000000000000000000000000000000000000000000000000006c7b5ae3" + }, + { + "op": "set", + "key": "0x0068e867f5f899fc01d641a00cea2c42d58f3cc061797135198a85301d1e2903068a", + "value": "0x000000000000000000000000000000000000000000000000000000006584a459" + }, + { + "op": "set", + "key": "0x00543907253acb230f5dc96bbafd8ea0f37dd63b30e8a4a3675bb007673a31fbc5be", + "value": "0x00000000000000000000000000000000000000000000000000000000996c5e6b" + }, + { + "op": "set", + "key": "0x00e60d91d1d11837912311124089c6b73c9d726c99f4df797eb48c2b7fec6e5e2c3e", + "value": "0x000000000000000000000000000000000000000000000000000000008887aa4b" + }, + { + "op": "set", + "key": "0x0018e25b9b05a714bd17ede1445ee7c785983a0194a3ed103783662bdc401dd7ffa0", + "value": "0x000000000000000000000000000000000000000000000000000000000b876b7d" + }, + { + "op": "set", + "key": "0x0034bb4f3468340d5f72275da92420aed94db48bbc61823e92f29f14f51590373a19", + "value": "0x0000000000000000000000000000000000000000000000000000000006c64054" + }, + { + "op": "set", + "key": "0xffc901cfbb39e551189323bea0af6cf9e81008c90dce65cbc2ff4672e263d7bb871b0daa3888412359e9769bc566073558cc37b927578bcc74743311f2e7487706d3", + "value": "0x000000000000000000000000000000000000000000000000000000006b9a2b07" + }, + { + "op": "delete", + "key": "0xff1df77729bb76b139342cfc86aff011590bf6bb810f47e69a6babac30af96f9f5ffa1e869c4566231d09357daec90e71aa93821a7347b29175ef4a1bc02a0b38181" + }, + { + "op": "set", + "key": "0x001df77729bb76b139342cfc86aff011590bf6bb810f47e69a6babac30af96f9f57c", + "value": "0x00000000000000000000000000000000000000000000000000000000881ea9ad" + }, + { + "op": "set", + "key": "0x0055a6923f7414ba482ae8a64b407f386a0bb0491b742bd4b22b5b4b44fca84e8444", + "value": "0x000000000000000000000000000000000000000000000000000000000c07da3a" + }, + { + "op": "set", + "key": "0xff998f08d7054351e5fa3ccadda32ab8d6c0c86a825dd360d57c0fe6b46612297cc290a8b5ac7a453c9cec5724a8a85ad4002fb6dc9c73e86aedfa05133317c040f6", + "value": "0x0000000000000000000000000000000000000000000000000000000094362837" + }, + { + "op": "set", + "key": "0x007b1eefd69ea014244937aaeaea4e8d4d296c04edd75ae47897e5612ef5a1a2524c", + "value": "0x00000000000000000000000000000000000000000000000000000000d0e6ee22" + }, + { + "op": "set", + "key": "0x000fb23a930b05e3bdfbdd0e6694fa9df9c5059ceabc60618bc40931ddb379f1f6fb", + "value": "0x000000000000000000000000000000000000000000000000000000005235ca9f" + }, + { + "op": "delete", + "key": "0x0018e25b9b05a714bd17ede1445ee7c785983a0194a3ed103783662bdc401dd7ffa0" + }, + { + "op": "delete", + "key": "0x004c1eb4996f3098c89ee461d61bd5f177587b0f4363ea73f4387e6296c66a260f5e" + }, + { + "op": "delete", + "key": "0xff4c3a8541d9ec369a9fd4bcdad885cc3b166da70ed6a83066924727d9ff147fce963472d01342d91a0a268bdfee2ecf01a6d30f9021151faf2e2e6719120bf40f0a" + }, + { + "op": "set", + "key": "0xffc28c21982a56a6cecf595bfeb78297c7432abfb487cc75c09db2856a118f441d0b16068d4b14101ddeb56b5b42f0587a3a4aa093b1de8fb5ecb2311977bdbcbee0", + "value": "0x00000000000000000000000000000000000000000000000000000000acf0f355" + }, + { + "op": "set", + "key": "0xff15c4813bb5b1499e0a7e0d46daab8e10c70940c008c566d5c68557b1523d1f29d090338251613ee5dbd2ea5b763cb40885f3f40530835887585c70141b84e957e3", + "value": "0x0000000000000000000000000000000000000000000000000000000028346819" + }, + { + "op": "delete", + "key": "0xffc28c21982a56a6cecf595bfeb78297c7432abfb487cc75c09db2856a118f441d0b16068d4b14101ddeb56b5b42f0587a3a4aa093b1de8fb5ecb2311977bdbcbee0" + }, + { + "op": "set", + "key": "0xff9c847c439d6431d36ddeb326cb553da2e967778a24afbd1d23a62412186faf01d11c941f9715afff547cb69563e69a920e6d5098967069e5002620d5124791d872", + "value": "0x00000000000000000000000000000000000000000000000000000000c058f48b" + }, + { + "op": "set", + "key": "0xff7293ce391f5cd652216ca8ea31069a06a82f393028dfc8786aa5efd7be734b2cea4832fe56448fc97a7b636806f7e3afef77ab0a5a99e9d5e8d12b5ba815af4a1b", + "value": "0x0000000000000000000000000000000000000000000000000000000079aa2bb6" + }, + { + "op": "set", + "key": "0x0055bbfb5b05f80543bd662a7b7a74980a2992612798087fa24e1fc86b8e8508e466", + "value": "0x000000000000000000000000000000000000000000000000000000000ec6d4ad" + }, + { + "op": "set", + "key": "0xff4b83f061efb2f4708114e46b5ef50a255528c28c80bbe5fc6ad88e892bd4c4ea37491f84eb77d352d7d8fca0484b856ceab7236e2f773d157b3fbd6d0586de7fa9", + "value": "0x0000000000000000000000000000000000000000000000000000000045e9c809" + }, + { + "op": "set", + "key": "0xffa43fb2d7beb12b34786a837467f62b7d6c5dab4ab7eecc89c0b67db76a6829272094bf023796a3d4bdc16643cddf5395ab048ba346a4f50f1cc64da78ad7dd52a4", + "value": "0x000000000000000000000000000000000000000000000000000000009b943498" + } + ], + "roots_after": [ + "0x5cdba549646b7612059692efa58d33f90059dfa9df06433f4438510c1b32e049", + "0x6ae6cd7fe0f7f9dfbff03d1d6b88c55f8c9f247584c70427a2a53612cdf202e6", + "0xcbe8e97d34728b819654a46096d4fe13abcdeed3e31cf088be21de915051259a", + "0x155969c29e159077b8a9997325ac9962fee077d64c9467f1ccd68618c9abbcdb", + "0x2439229c4decda037eb0a692540821cdd6c77df8ba51d478291d905e5de4f5e4", + "0xc252ede6498166ca351dc0346fdd1ee5df6694e3acf7f5714f2e80db687ccc79", + "0x7fcfd5973d297879b65209bb9cf1ca74c30ee717e13350d2c0447675165cb80b", + "0xdbb4e48dda8ae1f9287abccd62637085bb171f5897ad42e5019500209e89202d", + "0x14712da3d0ce63185cae268f29bf2593295476a83a8e95b3b4daf01a83274d6f", + "0x51098829074dc7618bdca7c079f0ac0577e0a08a0df5c92dcd104bf6cad43ea6", + "0x2643869090c9ca916157631cac68996dd01de67c66639eeb443f9b93ebe7df1c", + "0x9793adae9d275d569398dc631e09da40464320a0dc0c644d36d79f97d34b7288", + "0xef2bd1092bc3404855dd94e9452f97cebeb425d8027e7a56dc1c5fc0e93c38f6", + "0x85170f6f2bed9f200e8835f9d2c9bd13cb9267d77e834dcd73d43d1f897cc346", + "0x71c46fa0b2804adc2012db0d399c477429071b47f13fc31ddc125bc599e8fafb", + "0xeba1b27a56ff60fd186ede1c5ee445c59b2733f527c6ccc30b765b6917491720", + "0x2d0dcb278bc7892ad37afe2c523b73d18a60e76b1dc41d04469896c3b355d490", + "0x5aadbab832910092a62a49e78c5d76b10ffe26f3452a0bd47fd049e36eb31d63", + "0x53b8d1dab39520c882f5666b092ab47f59f4f3084c9471eefbb15d6a0c58ad5d", + "0x52ff67405b74b808b1649b64764de113df17e70ea5aebf5489bc1db7aad52422", + "0xde35ec177dd5c384cf24be794135d622ddd2a666b9d67d1d46fa149d5aa4caf9", + "0x18f08d2cfe81c6db210cd9ffcfd632acf3adc0070cca7e9af1754923edd32ae7", + "0x3361b009bb8a11272223d2b625739ffeffb7f096e2e09a22bce9fdd247322784", + "0x938df1fca555925f0c85971ca7b13842c86b161dee9056cb89f8e6e144f22bff", + "0x813308a0141f9e4eeccd35f41b439c5dfc3e8759ef516e83c01e0f1e6dc86f43", + "0x0c00cd26f4296a59ee426a5c1e262c43d56d57528832c012775a8f4735ac7471", + "0x3d9f7d81ec22374d4fbce9b0d7d9b435c6445bb86ecbe0eb69a10d22091c03a7", + "0x5eb7a6660d0856276ec22ea96d9e745e5fd7d12efbef04f39dbcb779e1adb32c", + "0x85edd30c13788e8e6fdf1c3ee5e2a24173117ce61839a76881ffc050442ba818", + "0xd6cd1175b20eba2b0158adf019376de82382c8f042cdce53b9879a76a211e518", + "0xe6aa26c153493c447c58aea50a5973e696afebefed800f84f2e829e2dd670282", + "0x7edb4484f862c0055164aa33198f81db08dd7fffb34c7bcae8fd06707191a15a", + "0x70c6c933187eaf011a83d4db61e8f022e7f94e36712371156bf0c41e4f46c42b", + "0x7073f03032107282c802817ecddd49294ce407e567465c0494386d22eb1faedc", + "0xcf5a920598934a4b21c39b3fee92586dec28802279dd709ace6cf5baf2dd11f9", + "0x9a0fcaebdd11a283f596942ff8e7cb7246458f8fb5c706d172f730ae108b20d3", + "0x7ca3a8a966a560dd237b6fdaa468152bc1617bde6cd68ce6e4ca547473b00b84", + "0xb5b7b22a573b230cbf2d04d3f6be6277d10fc262c7768a189af2426564c5257f", + "0xdca78a4aa2f85748f3a1660b22d2fde59d506a85d155f7a0d928b91339c2c85b", + "0xf348b3e47434a30a2a72f6cd67156f637bef3ba0b91803e186bda23b458312e8", + "0xd94b253d3bb77d5351177f2ff1af9d494d955fedf3614dfae18c82d2582e51af", + "0x710342a386d0270bd892c68c0f3c70acc40bccb744466a02e14dd7b47a78b28a", + "0x7de7721305a34da9a88031afae727da365c476a2f5b61fb0d8a79a64935abebb", + "0xe9523320ccaf8cefa207dab523c9eef34b6efe9ac3700e1f95b6a0752e58d432", + "0x04eb69694e545df95face8a1ed89ab7ed6f3d9598d5e0ca47607e1ceac69ecb6", + "0x608f4c6465c877ed24984c890edd30d2997202eb2d184ab759cedf2713140ba8", + "0xc13778b97187a09ef78dcf1e891562379c577a0ebbbf464398a371b8fac1adc6", + "0xe709bb96efe184d77f40bc5fcaeb1036cca00e9d99f4769f0a8dd15005321062", + "0x60c84e4635c2adb2fca77a7f7945ab04714ea254c1e8cdf5daf880f25b3ea556", + "0x25c47b0e563c5a006e063ec274cdf7626b4e8481e3e90f1f95e720335b60a168", + "0x416df08ef16042b79682a3582669f55c84d339208b121023f1b92c92858f2bc3", + "0x75fb6c30d6943ae9ff620c9b3f10df7c7e851d9d0fe5b39a880cc71ad8593e71", + "0x2dbc418daa3ea94d39b7264f4ebaa8a8414fabd5c1733cd041db901deade5fc7", + "0xd30d126c66e8d995684f056d0fa6cdfa3d74298622a74c09810f7fedfbb47a0a", + "0xc9f6d4bf69fbc09640af570c9d8a1e67020a3209ceb131531f72912e5849eee6", + "0x115b91274eba5af0ed852f198cccbadc6cca83441e4e8be6475598d15b61e90d", + "0x0632ba803f2b4f34f37a6da6be87813e5a10c6598bfb16e4dfdad6ab040f6c79", + "0x2891d8b8cd3afb1d09beefe9949971a9c5ffaa4735810fd62739f2171cf36f36", + "0x49d04f88c23d6f57e0928c694290300ac973ac2d96996472ca0d3b6743bb42c0", + "0xe0dc4ddf3c96c8107a95a9826fc96ff3f15e6d07c1abdf0647410645c4cc472d", + "0xd2845932e63e79547b546ef2c020c82d41b920769a2ff16cda86c48dd7362789", + "0xc5450ad1eed3c356da39e3a60d6745be4edbe1877eb96e6440e97d49543862fd", + "0x3a7c0da4984cac174966a1da6ed11e28ba87ceccd549c50d756964fcdf913328", + "0x721b9141b313fff8513f15cb83e2a229dbd960b99745f4bec9e6aabf42c52467", + "0xdddbde643821c34afec8702ed12561103065c9258fefa28ee78a9b411373e0cf", + "0xafcaa951c9c7eb8af986a4edcedf098d680fce7ae4c28bde28c0bdab73ad806c", + "0xd3c93b18b889b8b6807d7416eb3651bc3cb24076026f3531ce5f300e90e28d0a", + "0xabe59ae5af09518a481d24ed0be9d33aa2408a131135c43eb5a193bdb8607b7f", + "0x70df88d62fafba7dfe9c4f51347eab8d3e3e880b6bc3d748b85a9fd8f81504b4", + "0x57942b88183644f597553794cef3849a64e8b4d30b6355e3578345bf93454798", + "0xfd8c711a7666534b069ae7ce59fe462a70262a6b96cbc7f8a84ca9b51e02f4f2", + "0xae571d58d248c86182fb5f82c039a5d085ec87cdd88fb44f72e18a4d17e08462", + "0x8c2a2961b0e2af42659eaeb11d19dbfb08297ca2224d4d378bb0063477396c2e", + "0x5cd4b7183ab194c9b1e7e793ee5ba2d0458300594ee6fe8c590782fc7c3eb7b4", + "0x4729519398ef89012ca69b75a0a9f7689b7e9b80b4161b9643865069cac6363d", + "0xc50a746ceb60df8f1ff0242190a7b1652b493dec93b2a6fecda756751d50633c", + "0xc43995327da1f6d47067fbc31949360de852577b4c1548ff662f9f3040508264", + "0x8ff981e26abf25aec33f2afbb28af0742451614047283f7bb70e31e38c51a9df", + "0x99fcd88960a49a0824be2a642620020c95d5b7488bad67ab1ec1ee0db601a492", + "0x64855c99a705a385fb8aa4f4c05bb17d5c7adae93f3c04a197891ecf9b430686", + "0x0a6b3a96b3d4a4fcde8a85b3b8d76522ceb01997d884d5879b767cc8eede2237", + "0x8ea008ea01671788c07f9257c776375137361f5cb005328015d1a21a843f736f", + "0x655fba748caaa3ab8cb7e46690eb2d2121462f1e1f38d75a0261fdec62455b81", + "0x864c76a20fe992ea8956fed530dda10941723fa61dc174af9569365df5dda004", + "0xb1d791aacaabd12e3565ad9a89f7831e032d1f4d0c765c12c0346014dc1097f5", + "0x744c37d3da39ef9db08b6492d2c217df89f0e2e9c5986daa2e44f49ab41781ab", + "0x8cde4594d8311716a5b139977268ab0084ac8bc23650bef3c24200742532b93e", + "0x8279bcc9c549e772762ef59318ba63c056dc8b8ac3568481f43bf8592c87ea42", + "0x2a03117b65aefbe812d8e5d5dfbc21cafd186a150cfb71e4310705c4e2f7b221", + "0x64124ad4a041176d1d99405c0662d2546482a36c20a95aaafced0df4b96375b6", + "0xb12379d77600f4c80a8e3092c6f1df31b546163af05be011c755fb07d707cb02", + "0xc0e5483bfef982691db8d431d74ecc0c7f6325ccdd955da00ba31903f6f80174", + "0x12c810d02b7807a79e29085dfd6246506d7a76a20e857a0501d6bf7f92b0d719", + "0x0f47737a2686703b19045bd5d1c98e9b2f6f989cde9a6eeb5ab2d959165c35d6", + "0x1fa31221ea50b19060c07eb3d9583fb4040674d085acefcf604a824842fc87e2", + "0x27492b69b3669a3388b16acea6eb010af60d1eb4e892a5b5f2190be8137e5f06", + "0x1940b21301c53319e34cbe54bad39c3c48f229ad2780a9121429f9863fa1a384", + "0x7ce8e6ff56c62ebf9172490eb9fdeeede0f46b8aea7bc24ec435511f071e18e9", + "0x7c570ab7810ef6c96503a503521c9d7a683eb032d234770c9c0a5ba4ef80cb39", + "0x212ce3d1f8a7a80a583d7841a813b123bc6ece2189c57949f4a40135f594b279", + "0x45e5541d748eba70d7a246cf6fa0a0db2edb0d4ad912c5fcde39c7a48350b1e3", + "0x220942366fb7a9b76412d91179d77dbbf30d2e659485b021d7d2296e377add42", + "0xab3b810ed5f6b42a32cfa29ade1fe14d42d1c080941ca2feec6782a4260e1819", + "0x5c599030220325ae60db3fd89485c575349f01c66d8bedee2e3c8912fa611f3f", + "0xb4d39480c8b7e32680ab30be4f766d99a7df7cc3d3bc584387a1bf8a2c5ee9ea", + "0x0a56dff343f24da8e6a702265d25da852eb385c0ea385f49594dd68d22d31d32", + "0x90e521dc6503b2ddc37fcf4e71aae22c8d996ba292316e1986f2699aff1fda53", + "0xa92a89954dcdd759a54d3439a6e6ddd985542123d13e15f2a709ca41ecbfc4d2", + "0xe501ac34a1d59da80219272f28fbd1704219c12072596b8bab7139d2bc91a586", + "0x805caae702b7ef3f71e77490595eb3539fffb8595e1d17345e77aea5cb0f025b", + "0x77c082e75dbc39c6e3a474c776f38e44e83863eb9c218c41cad70cd99492c213", + "0xecc65146f292e965138644ba4dccd0fe044dcc8953765eca89335a9287043467", + "0x267fe34c53aff1d596f12b931940c206a2a889e4d21236151f6c46ba0b46f5d1", + "0xc1bf3b0ecf8ad1fb72f847e17855833f578b6ecd6803d509bdd87244c4b76c6d", + "0x2845e63ae4b3e4bb0b1bc4979c108b90ff0189997f06d9958bf1800bf8a8d164", + "0xe2a246d4da5fe62cde8cf4ca8b687d182117b9e1131b817c7a8484ec4b79c92e", + "0xd52ced602f33b718493b514b2ffb0f028c134b57ba918ce4e0928c7d7df8c395", + "0x55ac0ef97885e52596284c93bb673cfaa1f2f2a049b206a5779a52f1e6ecd7d1", + "0x7ae69a4c2194e79d883376de224df0cddc2254a0882cf0cd3c039c8c5e2bfc03", + "0x729168791ebce2fedddf26fbdbb59bc4c49b42e4a71e203af6e090783c1a20ab" + ] + }, + { + "seed": 3102, + "ops": [ + { + "op": "set", + "key": "0xff2cebacb183e2f51a3b6cb9f7fe2bd11c399bf1bdb8ce95ed457d3e46190197a02a6a55293936c1e1b6894fbb3b2873c993bd580ec835889bf0f211410e368fc199", + "value": "0x000000000000000000000000000000000000000000000000000000002e422f9a" + }, + { + "op": "delete", + "key": "0xff2cebacb183e2f51a3b6cb9f7fe2bd11c399bf1bdb8ce95ed457d3e46190197a02a6a55293936c1e1b6894fbb3b2873c993bd580ec835889bf0f211410e368fc199" + }, + { + "op": "set", + "key": "0xff29e4e6bbd168fe5b46b7d34c34b628f95d54c4e75f9335a7e96282d1173b07dfd0f9925799d37553e0f34b2d7a49a135fc34a90b6fb99bfe70f2e43c28e758854d", + "value": "0x000000000000000000000000000000000000000000000000000000002ecaa733" + }, + { + "op": "set", + "key": "0x00d1afd6427b13f2ce7beb82a2cd6bbfc58eac8b2d46bc87bfbc78fbf440e18a635f", + "value": "0x0000000000000000000000000000000000000000000000000000000076fe3750" + }, + { + "op": "set", + "key": "0x000861030791246737d706c0d0017de01a26278bbe4ce8a134f5e0cd6f7eb69bcff9", + "value": "0x0000000000000000000000000000000000000000000000000000000035fd5ae2" + }, + { + "op": "set", + "key": "0xff998f08d7054351e5fa3ccadda32ab8d6c0c86a825dd360d57c0fe6b46612297cc290a8b5ac7a453c9cec5724a8a85ad4002fb6dc9c73e86aedfa05133317c0402e", + "value": "0x00000000000000000000000000000000000000000000000000000000be9e2390" + }, + { + "op": "set", + "key": "0x005963897639cd11d686b199366a1249d76bc89e0f7161c9eca48d9a199435952d33", + "value": "0x00000000000000000000000000000000000000000000000000000000b3e90b26" + }, + { + "op": "set", + "key": "0xffe57860ba0adcdd1294caf74da8b990a80a8a6861bc01b30dfe07450d25bab38f20483a0da833c6de846c07a8214bee6ee90bd1da5d9b9553f74784602fdd83dfb9", + "value": "0x0000000000000000000000000000000000000000000000000000000051dcd3af" + }, + { + "op": "set", + "key": "0x00e6b6d8d98be54d4c00cee378bedf964f755b5257bf1eabf52cf39aa987686fa743", + "value": "0x0000000000000000000000000000000000000000000000000000000083a3dad3" + }, + { + "op": "set", + "key": "0xff0fb23a930b05e3bdfbdd0e6694fa9df9c5059ceabc60618bc40931ddb379f1f6065b90a89bb317ff71b1f933493a08b55f152abe9f5ae5b58400664d141c512161", + "value": "0x00000000000000000000000000000000000000000000000000000000939e31a5" + }, + { + "op": "set", + "key": "0xff2cebacb183e2f51a3b6cb9f7fe2bd11c399bf1bdb8ce95ed457d3e46190197a018eb952465e3dd252f35c719bb6d7d14f62bf363e0afa74100d3046f81539e08b9", + "value": "0x000000000000000000000000000000000000000000000000000000002da16542" + }, + { + "op": "set", + "key": "0xff4b83f061efb2f4708114e46b5ef50a255528c28c80bbe5fc6ad88e892bd4c4ea37491f84eb77d352d7d8fca0484b856ceab7236e2f773d157b3fbd6d0586de7f48", + "value": "0x000000000000000000000000000000000000000000000000000000003b1510f6" + }, + { + "op": "set", + "key": "0x00c901cfbb39e551189323bea0af6cf9e81008c90dce65cbc2ff4672e263d7bb87f3", + "value": "0x0000000000000000000000000000000000000000000000000000000087d0f3c4" + }, + { + "op": "set", + "key": "0x0055a6923f7414ba482ae8a64b407f386a0bb0491b742bd4b22b5b4b44fca84e848f", + "value": "0x000000000000000000000000000000000000000000000000000000008cfbc63e" + }, + { + "op": "set", + "key": "0xff7b1eefd69ea014244937aaeaea4e8d4d296c04edd75ae47897e5612ef5a1a25254d51e5b726c0f3cfd1b03f07f25907b18767b99fef9eb727475858c458e1c7087", + "value": "0x00000000000000000000000000000000000000000000000000000000af70ae1b" + }, + { + "op": "set", + "key": "0xff29e4e6bbd168fe5b46b7d34c34b628f95d54c4e75f9335a7e96282d1173b07df754b0f807e1dc46306d4cb12e85e18d008678fa80cd9e6fa118c34e179bd412a87", + "value": "0x00000000000000000000000000000000000000000000000000000000d15c3b16" + }, + { + "op": "set", + "key": "0xff29e4e6bbd168fe5b46b7d34c34b628f95d54c4e75f9335a7e96282d1173b07df754b0f807e1dc46306d4cb12e85e18d008678fa80cd9e6fa118c34e179bd412a57", + "value": "0x000000000000000000000000000000000000000000000000000000003e5f6e17" + }, + { + "op": "set", + "key": "0x0055bbfb5b05f80543bd662a7b7a74980a2992612798087fa24e1fc86b8e8508e4b6", + "value": "0x000000000000000000000000000000000000000000000000000000002a25f39d" + }, + { + "op": "delete", + "key": "0xff0fb23a930b05e3bdfbdd0e6694fa9df9c5059ceabc60618bc40931ddb379f1f6065b90a89bb317ff71b1f933493a08b55f152abe9f5ae5b58400664d141c512161" + }, + { + "op": "set", + "key": "0x00e61994d31ceae39cae5feb02303eb90e0d4ae90f5b0755c3c26adc81f360ec85de", + "value": "0x00000000000000000000000000000000000000000000000000000000fd3f724c" + }, + { + "op": "set", + "key": "0xff543907253acb230f5dc96bbafd8ea0f37dd63b30e8a4a3675bb007673a31fbc54e3628f3b56f95f4e49d0141e2d17a7a533a1aee0d22c6cc49df3fb8b348ad608b", + "value": "0x0000000000000000000000000000000000000000000000000000000077d80388" + }, + { + "op": "set", + "key": "0x0018e25b9b05a714bd17ede1445ee7c785983a0194a3ed103783662bdc401dd7ff6a", + "value": "0x00000000000000000000000000000000000000000000000000000000fd3b61da" + }, + { + "op": "delete", + "key": "0xff29e4e6bbd168fe5b46b7d34c34b628f95d54c4e75f9335a7e96282d1173b07df754b0f807e1dc46306d4cb12e85e18d008678fa80cd9e6fa118c34e179bd412a87" + }, + { + "op": "set", + "key": "0x004c3a8541d9ec369a9fd4bcdad885cc3b166da70ed6a83066924727d9ff147fce0e", + "value": "0x0000000000000000000000000000000000000000000000000000000028a51712" + }, + { + "op": "set", + "key": "0x0015c4813bb5b1499e0a7e0d46daab8e10c70940c008c566d5c68557b1523d1f297c", + "value": "0x00000000000000000000000000000000000000000000000000000000e0e8d974" + }, + { + "op": "delete", + "key": "0xffe57860ba0adcdd1294caf74da8b990a80a8a6861bc01b30dfe07450d25bab38f20483a0da833c6de846c07a8214bee6ee90bd1da5d9b9553f74784602fdd83dfb9" + }, + { + "op": "delete", + "key": "0x004c3a8541d9ec369a9fd4bcdad885cc3b166da70ed6a83066924727d9ff147fce0e" + }, + { + "op": "delete", + "key": "0x0055a6923f7414ba482ae8a64b407f386a0bb0491b742bd4b22b5b4b44fca84e848f" + }, + { + "op": "set", + "key": "0xff2eb90959edcd0b6a8a7922b8b8d37cd9664001c217b4c8e6d2df4b8ede329a71386c0526d8dbab547c20a9e4f76e5cb3823def2a6851f17a6623ca792d40c20af9", + "value": "0x00000000000000000000000000000000000000000000000000000000aa623352" + }, + { + "op": "delete", + "key": "0x0015c4813bb5b1499e0a7e0d46daab8e10c70940c008c566d5c68557b1523d1f297c" + }, + { + "op": "set", + "key": "0xff998f08d7054351e5fa3ccadda32ab8d6c0c86a825dd360d57c0fe6b46612297c36c8cce2938e709d43205827ddebecc780d7e631bb1acb24046af6e5b23e445a99", + "value": "0x0000000000000000000000000000000000000000000000000000000045bfc591" + }, + { + "op": "delete", + "key": "0xff29e4e6bbd168fe5b46b7d34c34b628f95d54c4e75f9335a7e96282d1173b07dfd0f9925799d37553e0f34b2d7a49a135fc34a90b6fb99bfe70f2e43c28e758854d" + }, + { + "op": "set", + "key": "0xffc28c21982a56a6cecf595bfeb78297c7432abfb487cc75c09db2856a118f441d4d68c9b03da1dec117fa853416c0249fb7b863e57d51f6ac8e86e023c336f468d4", + "value": "0x000000000000000000000000000000000000000000000000000000006569ec1a" + }, + { + "op": "set", + "key": "0xffe57860ba0adcdd1294caf74da8b990a80a8a6861bc01b30dfe07450d25bab38f633bf20195771cd09de04706b99eda30093d53b7c3b76cc8888c52db5ac6b22388", + "value": "0x000000000000000000000000000000000000000000000000000000005110212d" + }, + { + "op": "set", + "key": "0xff5ac76a8f23131e4ca988c8afcc313130cc95e521d2691635fc26196d63e66a1f893d3c65a10fbfddef979c778efdeaf625c4d5326e417354a396e996bacabea561", + "value": "0x0000000000000000000000000000000000000000000000000000000054bfda98" + }, + { + "op": "delete", + "key": "0x00e61994d31ceae39cae5feb02303eb90e0d4ae90f5b0755c3c26adc81f360ec85de" + }, + { + "op": "set", + "key": "0xff2eb90959edcd0b6a8a7922b8b8d37cd9664001c217b4c8e6d2df4b8ede329a71715725eb2030fcfeb52b18f6f9d8245268cfdc4361b456b193745cf6a187bdcb4c", + "value": "0x00000000000000000000000000000000000000000000000000000000f3ea16f3" + }, + { + "op": "set", + "key": "0x0078a2812688114caf4452a5611322977e4b1000e59ef408d4c8e2c90028370bcbb2", + "value": "0x0000000000000000000000000000000000000000000000000000000036fb95d8" + }, + { + "op": "set", + "key": "0x0078a2812688114caf4452a5611322977e4b1000e59ef408d4c8e2c90028370bcb82", + "value": "0x00000000000000000000000000000000000000000000000000000000109f55b9" + }, + { + "op": "set", + "key": "0x004c1eb4996f3098c89ee461d61bd5f177587b0f4363ea73f4387e6296c66a260f08", + "value": "0x000000000000000000000000000000000000000000000000000000007f6ea99d" + }, + { + "op": "set", + "key": "0x0015c4813bb5b1499e0a7e0d46daab8e10c70940c008c566d5c68557b1523d1f2951", + "value": "0x0000000000000000000000000000000000000000000000000000000073e47b48" + }, + { + "op": "delete", + "key": "0x0055bbfb5b05f80543bd662a7b7a74980a2992612798087fa24e1fc86b8e8508e4b6" + }, + { + "op": "set", + "key": "0xff4c1eb4996f3098c89ee461d61bd5f177587b0f4363ea73f4387e6296c66a260f6ec3a127efd23185b926d47d1bd096f711c14587e91f703452a6f9499afe2a6ade", + "value": "0x000000000000000000000000000000000000000000000000000000001da721b0" + }, + { + "op": "delete", + "key": "0x004c1eb4996f3098c89ee461d61bd5f177587b0f4363ea73f4387e6296c66a260f08" + }, + { + "op": "delete", + "key": "0xff4c1eb4996f3098c89ee461d61bd5f177587b0f4363ea73f4387e6296c66a260f6ec3a127efd23185b926d47d1bd096f711c14587e91f703452a6f9499afe2a6ade" + }, + { + "op": "delete", + "key": "0xff4b83f061efb2f4708114e46b5ef50a255528c28c80bbe5fc6ad88e892bd4c4ea37491f84eb77d352d7d8fca0484b856ceab7236e2f773d157b3fbd6d0586de7f48" + }, + { + "op": "set", + "key": "0x0015c4813bb5b1499e0a7e0d46daab8e10c70940c008c566d5c68557b1523d1f2969", + "value": "0x0000000000000000000000000000000000000000000000000000000063a406d6" + }, + { + "op": "set", + "key": "0xff412c4d14e5bde6ea4c898a1d0f911f12a2cf775a62e3cc4c0f28331d432e8f10fb3754ee233f7964e47e5e73dc4342ee28be4dcf5e0bb64009bcbfd0c440559c0e", + "value": "0x00000000000000000000000000000000000000000000000000000000e350d607" + }, + { + "op": "set", + "key": "0x001df77729bb76b139342cfc86aff011590bf6bb810f47e69a6babac30af96f9f576", + "value": "0x0000000000000000000000000000000000000000000000000000000088ec24d4" + }, + { + "op": "set", + "key": "0x0018e25b9b05a714bd17ede1445ee7c785983a0194a3ed103783662bdc401dd7ffe4", + "value": "0x000000000000000000000000000000000000000000000000000000004e868a1b" + }, + { + "op": "set", + "key": "0x00e6b6d8d98be54d4c00cee378bedf964f755b5257bf1eabf52cf39aa987686fa73d", + "value": "0x0000000000000000000000000000000000000000000000000000000098928b05" + }, + { + "op": "delete", + "key": "0x00e6b6d8d98be54d4c00cee378bedf964f755b5257bf1eabf52cf39aa987686fa743" + }, + { + "op": "delete", + "key": "0x0018e25b9b05a714bd17ede1445ee7c785983a0194a3ed103783662bdc401dd7ffe4" + }, + { + "op": "set", + "key": "0x00e2f41518226671332da4adddab1fdcb9903c7b895c7943b669067fe96b0baacf6e", + "value": "0x000000000000000000000000000000000000000000000000000000008acb81b2" + }, + { + "op": "set", + "key": "0x00ec64b2de1fc1d53b795f49a1c84ca4172bc3292472c3e244e4f8c225fa786f56df", + "value": "0x00000000000000000000000000000000000000000000000000000000328f7460" + }, + { + "op": "delete", + "key": "0xff543907253acb230f5dc96bbafd8ea0f37dd63b30e8a4a3675bb007673a31fbc54e3628f3b56f95f4e49d0141e2d17a7a533a1aee0d22c6cc49df3fb8b348ad608b" + }, + { + "op": "delete", + "key": "0xff2cebacb183e2f51a3b6cb9f7fe2bd11c399bf1bdb8ce95ed457d3e46190197a018eb952465e3dd252f35c719bb6d7d14f62bf363e0afa74100d3046f81539e08b9" + }, + { + "op": "delete", + "key": "0x00ec64b2de1fc1d53b795f49a1c84ca4172bc3292472c3e244e4f8c225fa786f56df" + }, + { + "op": "set", + "key": "0xff29e4e6bbd168fe5b46b7d34c34b628f95d54c4e75f9335a7e96282d1173b07dfd0f9925799d37553e0f34b2d7a49a135fc34a90b6fb99bfe70f2e43c28e75885e4", + "value": "0x00000000000000000000000000000000000000000000000000000000bb996469" + }, + { + "op": "set", + "key": "0x007b1eefd69ea014244937aaeaea4e8d4d296c04edd75ae47897e5612ef5a1a25262", + "value": "0x00000000000000000000000000000000000000000000000000000000ea04efcd" + }, + { + "op": "set", + "key": "0x00412c4d14e5bde6ea4c898a1d0f911f12a2cf775a62e3cc4c0f28331d432e8f103b", + "value": "0x00000000000000000000000000000000000000000000000000000000a1afc0b8" + }, + { + "op": "delete", + "key": "0xff2eb90959edcd0b6a8a7922b8b8d37cd9664001c217b4c8e6d2df4b8ede329a71715725eb2030fcfeb52b18f6f9d8245268cfdc4361b456b193745cf6a187bdcb4c" + }, + { + "op": "set", + "key": "0x001df77729bb76b139342cfc86aff011590bf6bb810f47e69a6babac30af96f9f515", + "value": "0x0000000000000000000000000000000000000000000000000000000006c72b7d" + }, + { + "op": "set", + "key": "0x001df77729bb76b139342cfc86aff011590bf6bb810f47e69a6babac30af96f9f5f5", + "value": "0x0000000000000000000000000000000000000000000000000000000089929d12" + }, + { + "op": "set", + "key": "0x00c28c21982a56a6cecf595bfeb78297c7432abfb487cc75c09db2856a118f441def", + "value": "0x0000000000000000000000000000000000000000000000000000000070efd653" + }, + { + "op": "set", + "key": "0x00e2f41518226671332da4adddab1fdcb9903c7b895c7943b669067fe96b0baacf05", + "value": "0x00000000000000000000000000000000000000000000000000000000bcccc339" + }, + { + "op": "set", + "key": "0x00e57860ba0adcdd1294caf74da8b990a80a8a6861bc01b30dfe07450d25bab38fd3", + "value": "0x0000000000000000000000000000000000000000000000000000000017d57e02" + }, + { + "op": "set", + "key": "0x00e2f41518226671332da4adddab1fdcb9903c7b895c7943b669067fe96b0baacf8c", + "value": "0x000000000000000000000000000000000000000000000000000000004c32ab2f" + }, + { + "op": "delete", + "key": "0x007b1eefd69ea014244937aaeaea4e8d4d296c04edd75ae47897e5612ef5a1a25262" + }, + { + "op": "set", + "key": "0x00d1d141c1bed8bb4f809292e9340a0209cd167484a24a5ca993fcac4b97086d897e", + "value": "0x000000000000000000000000000000000000000000000000000000002dfa839a" + }, + { + "op": "set", + "key": "0x00e60d91d1d11837912311124089c6b73c9d726c99f4df797eb48c2b7fec6e5e2ca2", + "value": "0x000000000000000000000000000000000000000000000000000000004d4df975" + }, + { + "op": "set", + "key": "0xff998f08d7054351e5fa3ccadda32ab8d6c0c86a825dd360d57c0fe6b46612297cc290a8b5ac7a453c9cec5724a8a85ad4002fb6dc9c73e86aedfa05133317c04050", + "value": "0x00000000000000000000000000000000000000000000000000000000b1eed0c4" + }, + { + "op": "set", + "key": "0x0055a6923f7414ba482ae8a64b407f386a0bb0491b742bd4b22b5b4b44fca84e8454", + "value": "0x0000000000000000000000000000000000000000000000000000000046f2e508" + }, + { + "op": "delete", + "key": "0xff29e4e6bbd168fe5b46b7d34c34b628f95d54c4e75f9335a7e96282d1173b07dfd0f9925799d37553e0f34b2d7a49a135fc34a90b6fb99bfe70f2e43c28e75885e4" + }, + { + "op": "set", + "key": "0x000861030791246737d706c0d0017de01a26278bbe4ce8a134f5e0cd6f7eb69bcff9", + "value": "0x00000000000000000000000000000000000000000000000000000000ba9f1917" + }, + { + "op": "set", + "key": "0xff0ed33feae99b046218c56bec56750a98beb5fd6a51ec13218024cb2455586b43ce51c5b488a38762ddd335b3b0f645fefed9e6d3f01457ac62638b1dcc5e4e7644", + "value": "0x00000000000000000000000000000000000000000000000000000000c99448d2" + }, + { + "op": "set", + "key": "0xff0ed33feae99b046218c56bec56750a98beb5fd6a51ec13218024cb2455586b43b0ff3483ae67aaf30bffac5b9caf32340fc0265d2a99202b2d883fa94657351112", + "value": "0x000000000000000000000000000000000000000000000000000000008d8a0c92" + }, + { + "op": "set", + "key": "0x00e57860ba0adcdd1294caf74da8b990a80a8a6861bc01b30dfe07450d25bab38fb4", + "value": "0x000000000000000000000000000000000000000000000000000000001aff4bd6" + }, + { + "op": "set", + "key": "0x0018e25b9b05a714bd17ede1445ee7c785983a0194a3ed103783662bdc401dd7ff88", + "value": "0x00000000000000000000000000000000000000000000000000000000b3c0a241" + }, + { + "op": "delete", + "key": "0x00e2f41518226671332da4adddab1fdcb9903c7b895c7943b669067fe96b0baacf8c" + }, + { + "op": "delete", + "key": "0xff7b1eefd69ea014244937aaeaea4e8d4d296c04edd75ae47897e5612ef5a1a25254d51e5b726c0f3cfd1b03f07f25907b18767b99fef9eb727475858c458e1c7087" + }, + { + "op": "set", + "key": "0x00bded9122e6171a5dad9ce2aaebd933abf4718e9bf12448cf113064c9fd8e6ad0b9", + "value": "0x00000000000000000000000000000000000000000000000000000000bbef6005" + }, + { + "op": "set", + "key": "0xff34bb4f3468340d5f72275da92420aed94db48bbc61823e92f29f14f51590373a1c7f3420575f2956d219af851645b44537345b7c4f9199195b77d0edde2a379e4f", + "value": "0x0000000000000000000000000000000000000000000000000000000037d02280" + }, + { + "op": "set", + "key": "0x000ed33feae99b046218c56bec56750a98beb5fd6a51ec13218024cb2455586b435b", + "value": "0x0000000000000000000000000000000000000000000000000000000029fb46e0" + }, + { + "op": "delete", + "key": "0x00412c4d14e5bde6ea4c898a1d0f911f12a2cf775a62e3cc4c0f28331d432e8f103b" + }, + { + "op": "delete", + "key": "0x00d1afd6427b13f2ce7beb82a2cd6bbfc58eac8b2d46bc87bfbc78fbf440e18a635f" + }, + { + "op": "set", + "key": "0x00d1d141c1bed8bb4f809292e9340a0209cd167484a24a5ca993fcac4b97086d89af", + "value": "0x000000000000000000000000000000000000000000000000000000002b268a11" + }, + { + "op": "set", + "key": "0x004c1eb4996f3098c89ee461d61bd5f177587b0f4363ea73f4387e6296c66a260fba", + "value": "0x00000000000000000000000000000000000000000000000000000000c45a7015" + }, + { + "op": "delete", + "key": "0xff998f08d7054351e5fa3ccadda32ab8d6c0c86a825dd360d57c0fe6b46612297cc290a8b5ac7a453c9cec5724a8a85ad4002fb6dc9c73e86aedfa05133317c0402e" + }, + { + "op": "set", + "key": "0x00c901cfbb39e551189323bea0af6cf9e81008c90dce65cbc2ff4672e263d7bb87fe", + "value": "0x00000000000000000000000000000000000000000000000000000000203ca99f" + }, + { + "op": "set", + "key": "0x004b83f061efb2f4708114e46b5ef50a255528c28c80bbe5fc6ad88e892bd4c4ea77", + "value": "0x00000000000000000000000000000000000000000000000000000000c4e48d7d" + }, + { + "op": "set", + "key": "0x0015c4813bb5b1499e0a7e0d46daab8e10c70940c008c566d5c68557b1523d1f294f", + "value": "0x000000000000000000000000000000000000000000000000000000000374a3c5" + }, + { + "op": "set", + "key": "0xff15c4813bb5b1499e0a7e0d46daab8e10c70940c008c566d5c68557b1523d1f29d090338251613ee5dbd2ea5b763cb40885f3f40530835887585c70141b84e9572f", + "value": "0x00000000000000000000000000000000000000000000000000000000103e7f8b" + }, + { + "op": "set", + "key": "0x00e6b6d8d98be54d4c00cee378bedf964f755b5257bf1eabf52cf39aa987686fa704", + "value": "0x00000000000000000000000000000000000000000000000000000000c1eb2b88" + }, + { + "op": "set", + "key": "0xff55bbfb5b05f80543bd662a7b7a74980a2992612798087fa24e1fc86b8e8508e45ae026013e95f59126ce308964ea4e1d100de3fd7063ce06c5b1fcfd055b9dfa26", + "value": "0x00000000000000000000000000000000000000000000000000000000d5540993" + }, + { + "op": "set", + "key": "0xff2cebacb183e2f51a3b6cb9f7fe2bd11c399bf1bdb8ce95ed457d3e46190197a0a0a0268860a87b73c5d023e0181059e1d49f8ed387a132e90d5ccf7b7589f952c4", + "value": "0x000000000000000000000000000000000000000000000000000000001dade8e2" + }, + { + "op": "delete", + "key": "0x00c901cfbb39e551189323bea0af6cf9e81008c90dce65cbc2ff4672e263d7bb87f3" + }, + { + "op": "set", + "key": "0x002cebacb183e2f51a3b6cb9f7fe2bd11c399bf1bdb8ce95ed457d3e46190197a0de", + "value": "0x000000000000000000000000000000000000000000000000000000001b88b0bb" + }, + { + "op": "delete", + "key": "0x00bded9122e6171a5dad9ce2aaebd933abf4718e9bf12448cf113064c9fd8e6ad0b9" + }, + { + "op": "set", + "key": "0xffa43fb2d7beb12b34786a837467f62b7d6c5dab4ab7eecc89c0b67db76a6829272094bf023796a3d4bdc16643cddf5395ab048ba346a4f50f1cc64da78ad7dd528e", + "value": "0x000000000000000000000000000000000000000000000000000000003043f2e3" + }, + { + "op": "set", + "key": "0xffec64b2de1fc1d53b795f49a1c84ca4172bc3292472c3e244e4f8c225fa786f56d6f2c814b54068196e618750cac42223b5b384269904d6bc65960c2e623f4ee844", + "value": "0x000000000000000000000000000000000000000000000000000000009ac18f7e" + }, + { + "op": "set", + "key": "0xff1df77729bb76b139342cfc86aff011590bf6bb810f47e69a6babac30af96f9f5451673405763851d69ce90fb56d8dd98d3ef3ad6e6deccec4a6cc4303c046063a5", + "value": "0x000000000000000000000000000000000000000000000000000000005b7ee846" + }, + { + "op": "set", + "key": "0xff78a2812688114caf4452a5611322977e4b1000e59ef408d4c8e2c90028370bcbee4f7f4b500980d1dbb987a5b7dea34a042fca8da2a3671e71d626da6e5b381555", + "value": "0x00000000000000000000000000000000000000000000000000000000c844b5ba" + }, + { + "op": "set", + "key": "0x0046aeb3243f905d4e0eac7f60ee8962848c87e79dfbd019ece14fe45bd720ea6b72", + "value": "0x00000000000000000000000000000000000000000000000000000000cf548087" + }, + { + "op": "set", + "key": "0x00e57860ba0adcdd1294caf74da8b990a80a8a6861bc01b30dfe07450d25bab38f00", + "value": "0x00000000000000000000000000000000000000000000000000000000991126ef" + }, + { + "op": "set", + "key": "0x004b83f061efb2f4708114e46b5ef50a255528c28c80bbe5fc6ad88e892bd4c4ea9d", + "value": "0x000000000000000000000000000000000000000000000000000000000e56e2bc" + }, + { + "op": "set", + "key": "0xff9c847c439d6431d36ddeb326cb553da2e967778a24afbd1d23a62412186faf01d11c941f9715afff547cb69563e69a920e6d5098967069e5002620d5124791d805", + "value": "0x00000000000000000000000000000000000000000000000000000000ed64d79b" + }, + { + "op": "delete", + "key": "0x0046aeb3243f905d4e0eac7f60ee8962848c87e79dfbd019ece14fe45bd720ea6b72" + }, + { + "op": "delete", + "key": "0x0078a2812688114caf4452a5611322977e4b1000e59ef408d4c8e2c90028370bcb82" + }, + { + "op": "set", + "key": "0x0055bbfb5b05f80543bd662a7b7a74980a2992612798087fa24e1fc86b8e8508e487", + "value": "0x00000000000000000000000000000000000000000000000000000000e0f0c857" + }, + { + "op": "set", + "key": "0xff55a6923f7414ba482ae8a64b407f386a0bb0491b742bd4b22b5b4b44fca84e8455b7a5e4231b6e2b3ab80bf426d0ac93576b707e774c56731ed615e56edbb33a8d", + "value": "0x000000000000000000000000000000000000000000000000000000009ccf69a7" + }, + { + "op": "set", + "key": "0xffe6b6d8d98be54d4c00cee378bedf964f755b5257bf1eabf52cf39aa987686fa7ebf533e3e385b221b921f03b253af4ca27802584fa9d4ed08843417da394befa00", + "value": "0x00000000000000000000000000000000000000000000000000000000fd5052c1" + }, + { + "op": "set", + "key": "0xff46aeb3243f905d4e0eac7f60ee8962848c87e79dfbd019ece14fe45bd720ea6bc7d37b238b059b3f39fb34f03f189fc8c596ad3bc45f7231d1a8e723510074013d", + "value": "0x00000000000000000000000000000000000000000000000000000000943e0cfc" + }, + { + "op": "delete", + "key": "0x00e6b6d8d98be54d4c00cee378bedf964f755b5257bf1eabf52cf39aa987686fa73d" + }, + { + "op": "set", + "key": "0xff5ac76a8f23131e4ca988c8afcc313130cc95e521d2691635fc26196d63e66a1f893d3c65a10fbfddef979c778efdeaf625c4d5326e417354a396e996bacabea596", + "value": "0x00000000000000000000000000000000000000000000000000000000d7875175" + }, + { + "op": "set", + "key": "0x0046aeb3243f905d4e0eac7f60ee8962848c87e79dfbd019ece14fe45bd720ea6bb8", + "value": "0x000000000000000000000000000000000000000000000000000000001d328761" + }, + { + "op": "set", + "key": "0xffd1afd6427b13f2ce7beb82a2cd6bbfc58eac8b2d46bc87bfbc78fbf440e18a63cfc13d302b5e8c3d7f15057e1676e80d1556d030824a2893354a8c8c083bb9173b", + "value": "0x00000000000000000000000000000000000000000000000000000000de9dcb28" + }, + { + "op": "delete", + "key": "0xff55a6923f7414ba482ae8a64b407f386a0bb0491b742bd4b22b5b4b44fca84e8455b7a5e4231b6e2b3ab80bf426d0ac93576b707e774c56731ed615e56edbb33a8d" + }, + { + "op": "set", + "key": "0x004c3a8541d9ec369a9fd4bcdad885cc3b166da70ed6a83066924727d9ff147fceeb", + "value": "0x00000000000000000000000000000000000000000000000000000000802e3a74" + }, + { + "op": "set", + "key": "0xff3cbfd03e3f083b0987f841c4372031faf655a63472518a3613e2e30ecb542b5111589c57f4d8ad55d2c4ece4c6b63961c45be7479bfd08d04fab2d26d0acb7a07e", + "value": "0x00000000000000000000000000000000000000000000000000000000ec6c633a" + } + ], + "roots_after": [ + "0xdd16f2b8e1c012d8ed988df1cd9e31fc06bceee33304ec90fdee4c9e0d6f2522", + "0x0000000000000000000000000000000000000000000000000000000000000000", + "0xe7c887160ebfbf178105a04157a5b67d669ac6899fbf632526152fc5cb82cd80", + "0xd77cc71cfc52b7a272f47bb93cf6e3b5ae4a2d7fc991e4e2b76586018620570b", + "0xfb180e479837451381e4ebbf7a21be6937dbab9b830eeb798cf1854b48bc82ad", + "0x663475566fe0b6afa6fb71c73802d98b37948ccc16237229d8722a66b31c48f0", + "0xe26dac98c3f044695b7bda1e7bab4e9e0422d41b367b3fb873d50734d15f8de8", + "0x90263ca13708230b2ae2d6413b76995a7c177ed239310a9628fe4aac1e2d51fe", + "0xd7893e542d99ae8c18bd7a495190a58e2e1e25252d4fa9378ef2694db0e751ee", + "0x4df5042f26d44caf9d655bda9bb46284498628761474d302392daff333e5b610", + "0x1113ff21db5505e243f390d5df0fc11549690813ff7a25e8bc60b5209d39ed72", + "0xe14995d380c1552d2901cacc440b51fed58732d5eb6b6adb202e891f3f53b6d6", + "0x5d3fd815f8282caf95aab8105212fb98b6f5516beb9fba2ed4f4ee502b85ed74", + "0x09b380d8286220693a4a9e4b85e074ea94cf0dadbccacd05d6f8dc5470438087", + "0x957d9940952d058f2523c52603f8cc27537ee0e40b8c452d9691e28ff0f9d782", + "0x7cc8b1cbe418de642947381e3fd9251730dff6165bb7d48de4388cce893d8aea", + "0x5ea579760671e854abc2adfcb037bd6f1470c26e5f4ba6e9392d05e1af830b6d", + "0xb4281adb36825d1f7dd6b7f0536a81ad9950fb727124828f0a94a79c32885052", + "0xa90bdbe39f8833c02f914c2ca37a5d5b91be83ba6a4721923b1ffe8899c40d9e", + "0x8c3fff51ed5749c6147c9bb35b5ac0f20f0f0ff92604b0d7cd0f83bcaecf52b4", + "0xd9d49e4766df0f28b1f78bdbfbc5a2adc12210b96a31e940ba2199c0e66d6810", + "0xa65ed667638a312e9020a833f124c44271d24a3f169318feec746a9c60f859b3", + "0xdaa6ca892827647f4a980b5f506b586baca1e081c8b16fabee12f1f39f192810", + "0x7fe58a68430e077c8ed6e8db50b193f1dd1bcc7ec9868dab5e1af465772eab4d", + "0xda0cd545154385e98833c492823a54a3da062dd2cf63d83edb5cec020769bbfd", + "0x047d9f5bae78aef591535881ecb07ce1a67c6696f5cb25f2dd758d9e24c98304", + "0x0e2ec849aff3ee826b994f897ee5804340a0201019ee1c194b4d1d1e29ea5a47", + "0x36a917bd38446c14d6e5fcce57d12668e3b599eb96cd81b5311e3ef66d369728", + "0x3d0b731a242eef052000febd10cb32e67b9b4332aac81fc3e55f311d5b7bc472", + "0xa1a8a8809091387195b843815e469d148bfa6759c6ed5cb4108c8927959a9880", + "0xebdbaafdd80533829f04616c168dbc66673b4300b541575ecd1fe8db6ea57e18", + "0x94787be76bedcd4bc27a64efbdde1b661081c2e968c007fc5279ed727147e3e7", + "0xd993fb38d1d908f6de3817cb3379958f14ab89b1667fb3a7c78a94d84f940400", + "0xe8d0c2564928aafabff90160758a2585374eee8ecc1d95d5ea14f4bf7e5d5b40", + "0xb4d5bd5daec9b48b19a8f1dc669261a2bf2cec5f1e7943c3929e32115488bcbb", + "0x83a8addb264fc499155e6e4e24596f9ac2ed2cec6566ef37a1f12801a630bc16", + "0xbbac6afa6e7fbd3aa6a1023df32c0b5d33ce8e47f5d1886c84ede5a344c96f4c", + "0x4a8765a66b4ca5d413ec1804806aada4f1b599edca258e9592274022c8e04f79", + "0x2a775af5374764ec81b822547c0ff214ff5f311cae5ef348a0e58079a5c9c293", + "0xb25f2df22a29746d74c445e565937e0799e4f7c85d7f5501a42379fd1d7e2ab6", + "0x8ba3e472d63de597bf2b998a6cb0ecf658ab5975b16f227d763ee58b323091c9", + "0x3537f27c415564b1e091a2dae2f4dda6fb9d5bc7db8fdb8acc19b0d281255f77", + "0x5d3400b400ea8664fd7166a8823fd9ea5975da18949c3f01116f363177f7e558", + "0x2f355dca787be5cca0dc1453de5180af9646566353147c433b491a8ceff10d29", + "0xb578ea4fc20249141685b16bc86622f14bbefea626a3042d3963e0aacc388168", + "0x604344fdb408c344f12b6f3ae45cc40ff2796e97a77194c0236d9e758e4ad2a8", + "0x2578864f41294e06a3ee1d786b7522c2d58dad6fa06037dfa2ccdf793b417b0e", + "0x2d71d6815fbc8190739d87357b5b46f591ed488ab64e6e85674eab95010a4fa6", + "0xc24244541fc2ad742ac6ffe8d4de3313ce280f99c1ae2652c5039e9f39ec62d4", + "0x81385d7fecb9e98d72f5fee87afea8db4859279771f123503064109e30255045", + "0x517f7cb7c5a872e0bbd8e78a3700d39e7e8f8b4045148ddc1e8adeaed4ef31b2", + "0x5f54783d181ede5fa7be7831ed8a1e638bcc9b86c7c976fe1df7894f5e20c533", + "0x1195b09383681ca4d154c9471e97c365b57d0008652d83b8a3451a6314646b96", + "0x194dad20176a4cec03b075fac850504e2ac8fa72803c4eda51e69421190c9b95", + "0x759f81d2c0a37955c1e88f66befd2a5f223a93a22ac7802e77b3f11e5c88f3ff", + "0x75749205d848f106433cc8dfef8677d3ddf49084868d6899fd0c503f2e90a3bc", + "0xf0ba3ab2546cef4f649d643f219bb18253b322793db4d9a9c2f8b6b80b103fb4", + "0x7605cfa9b033e38d1d34456d0378123d5352ace0aeadd602bb8eb845149b6ad3", + "0x4e27fae9d4541f254407e92516d8fd8fa21dd56ee10dc1eb044b51343d8ec706", + "0x792ee7cc1a2e2b89581a1dd7f197a704c8e19098ffa813fcbe477f95f1c226af", + "0xd4c3771bc7dd4808f143bf5578e18121f0c81997fef4b2f45dd24d772d64bbad", + "0x2e29adeb0f6362bc1b332c53cf773ebe430849b993312819b4d977ade8bfa151", + "0xc1003ac3d52752efdb4b469d6c09a737658e26b15903f0b5439c9f98b1e5088f", + "0x4ca9c3dbfc854dc81935fbbe9116bd83517fa3262c7664cd4bff73323258746f", + "0xda8fa4114a9f82cf873ac14c6105aa652cb5fb9534b315edd15fa765aa2df5e6", + "0xf600fccde574311940532aad1305ea7a74f4199744632ace534f29b1fc415733", + "0x9ffe5a5f256ad89b96ec8f8163dbd654e431d382aea21cbd60e54428218a22d1", + "0x0a5da34634a379ce4f936cc85fda7ea26442c1f4bd9120d14a377f055571bddf", + "0xa3cda68f6d3c8f85a9e960e49a5ba5796ba02211140a7c558002ef7977f29efa", + "0x7a42d5dab0d1343afb5cecd47384eab0a292d640d9413b1f3bbdd85003452615", + "0xa633e7f717423349cc4dcacd3c8b9fecd9b7668e9a38d3b1f7b34fe373c9033c", + "0xfd76c56f441715c5144b20ace1c2bef2754b2c6a6afde6358c47e9897003c2d9", + "0x9e2a00917cb42a69d568ed7aa397d2a08e169f913119a4a38a4d72fb94db66cc", + "0x96bc7565f138a55bbccc3ce2e4f1814b107fd95fcd7a1382b9692ca5861edb32", + "0x0d1d83d02a9f1ffa44a6c7dca6165f11076eb4ce3415ccc2a01172f5db455f17", + "0x3d7540bfd48bafe57e6b31a969a21df950905c5509837fd0bc249ccc978008aa", + "0xb5d4733f38c70b18531cb18361a55b9adc5f162150b03aaf2a795c0e3b9323fb", + "0x9bb7ea59c81e23f335d912e26b40a573b82ad13d26e512dd3bdf2234bbc11480", + "0x71190b7b68a9169279213e0d02c157564aed464cc9063f175c8780ce79e99136", + "0xc5d0ba2d5e453c949d26003cf656efe1dc904973db3e5d6c7a63009871083d93", + "0xb5f3f98c140e84b13751dc582334187536b9c57b2a86c79dac48568e10e23b12", + "0x495cdb7c8de836a1a46f9619d9755a9ab28cbb22149abb3ee52431b20fea3425", + "0xc00a3dc5cccf04ed31311515539e0f7cd3f7e852217496c1c13c36de7ea61287", + "0x29dfd41e74eb695ea128b1c3b3fd25610da939d255b10f0e0eb32b19684ac0ba", + "0x40ecae6715cda9f5759160f088ad15d164f678064cb289549d347d505a2b2ed5", + "0x1fddae87aace1d5b183dc019c976b6a1813d146eae8016059f89a5725ed95ba2", + "0x485bfc7ed0acc02e45f0e04450653e7788394ee126834f9e3d485c3cf684cec4", + "0xeb624656580f026104a20b69bf78e6dfcc774055c28c549e9cb8243fa515893f", + "0x1bf7a3151de44574b5404a08e55abb120a6fdbc7406a54a2cdb4d7901a94fe7f", + "0xb527d21e47385ca2142b2621be656f97854f528541fd706c8ee1135dbd06e386", + "0xdc4b0385e67f81daad986e2d49ccf07f8d6c88e1401283dc27c382ed79692ba8", + "0x752261197c970c67bd0b4566e9da57916afed725610ab287ed4c6f6d5210260c", + "0x61f2e6383008b28722612e215b1ed77bfa533803d787c16a373e5973e4ca5faa", + "0x20e918b0baab5e6dadc9963ecb3dc76c43ffe2a26e448f4b3f47a4e8b29ba00d", + "0x31520401bd25f84b0259bc99cf8f89d7d9367a095ef91e5573f423fa427295e3", + "0xcc5dc730bd5e6e6724e3ff334bfb6cb6bbb88b0a310d9c3fbc588a20bd8fb92c", + "0x1a3980997f6379724775e9767a21f8624a2b78a6ffe97524d1ff68c78100f249", + "0x846b4829e8e13667fce2bade085cabf4dcb0c218b22d8113714ec485cc1fd505", + "0x13f1834a6327901509d59706173183c7eb14edf3073bfd00526226e9f786bac6", + "0xa278c52b411b868367a65491fbf920efa9bd927104e8a695abc1a607288667d6", + "0xea5c739baa8f710c47cc1e61144b7023c88aa089d310cd4de9cdce5dc0e8b780", + "0x4f20c6919d878ac275c79bafaee073b3024d975e4ecf3f65108a0467242138eb", + "0x5bbbb454fe9c9318af76bfd5af240e1bf3b48e4e05d84941a3aae8463dbf9cce", + "0xec3d24a6265012cbe5f48e43908defce13b719ca0ef059ec9e6a7c924afa28d9", + "0x0b7b16844e2e38c2268f8d2c0ef7bbf5f0308b745e78d665806b3d29d80c8f81", + "0xde6ff0c5c22ca1fa14afef7c36452d0b5ac37aa8e010ea67553cd8bb1d14be5d", + "0x8996181b1df4c1e066b822eb778f16120748ed2abcde43ef4bdbc410948fc8b4", + "0xdc6d16497c9c1db1edf7b2880413613de1ff9103af4f23c3574dcc57cf332085", + "0x6e6eca38cc5687d043bb80e34d8a540cb3e45cf2b14d47527dc5f39524632226", + "0x41fd1da147aa5932caa08f342d7445d491b0dcd6144bf2691003f11e5425a40a", + "0xf05a71d105eb642fe786c8be04b0c9c2e0195707104b262aa785a78e1f56c4bf", + "0xb98718d5012439008639b7de49fd0bd4ad76f97db2d89c3fccb43aae87e9f1ff", + "0xc99a13119f7c1bc3ea434c84971ce4c4317eb3075517e8df6b2565098c47312d", + "0x68fc3dc2b7232f49e642210deda26dd9d7a1b6c2138476f25abe93bf96bf73a9", + "0xbbc50e576b671232dec276051e26f3882fa4f6062a16354f61c518b7577f5835", + "0x2dfdc171bc11ac1e25d9f71c3e50aa005878d48dc19fffdd9d0d0d333c2c5425", + "0x65b52b3633be0666459906c62baf06fce506236e188bf6005123616d82095f47", + "0xb149c9df31a9fc861878e74102ba3ad83b0e451631737e6a74da8ec49f9548d3", + "0xe5c7847bb607a5ca7070b7e243fbbbb49c95b1cad8e95dfab7bb7a942cd3af66", + "0xc911c60ebf20b097aa3ea07699669d7365005ff0a215aec20d4b814789a283cd" + ] + }, + { + "seed": 90210, + "ops": [ + { + "op": "set", + "key": "0xffd1afd6427b13f2ce7beb82a2cd6bbfc58eac8b2d46bc87bfbc78fbf440e18a63aeb0aca39c8f6ba440835a8e371b8b2719dec5b24a57f1da0e7519c3cd74beff52", + "value": "0x00000000000000000000000000000000000000000000000000000000cec06895" + }, + { + "op": "set", + "key": "0x00e61994d31ceae39cae5feb02303eb90e0d4ae90f5b0755c3c26adc81f360ec8587", + "value": "0x0000000000000000000000000000000000000000000000000000000026a125de" + }, + { + "op": "delete", + "key": "0x00e61994d31ceae39cae5feb02303eb90e0d4ae90f5b0755c3c26adc81f360ec8587" + }, + { + "op": "delete", + "key": "0xffd1afd6427b13f2ce7beb82a2cd6bbfc58eac8b2d46bc87bfbc78fbf440e18a63aeb0aca39c8f6ba440835a8e371b8b2719dec5b24a57f1da0e7519c3cd74beff52" + }, + { + "op": "set", + "key": "0x00c901cfbb39e551189323bea0af6cf9e81008c90dce65cbc2ff4672e263d7bb871a", + "value": "0x0000000000000000000000000000000000000000000000000000000038f9aacc" + }, + { + "op": "set", + "key": "0x005963897639cd11d686b199366a1249d76bc89e0f7161c9eca48d9a199435952d0f", + "value": "0x0000000000000000000000000000000000000000000000000000000053b3bca6" + }, + { + "op": "set", + "key": "0x0068e867f5f899fc01d641a00cea2c42d58f3cc061797135198a85301d1e290306c3", + "value": "0x0000000000000000000000000000000000000000000000000000000058e273d9" + }, + { + "op": "set", + "key": "0x00e61994d31ceae39cae5feb02303eb90e0d4ae90f5b0755c3c26adc81f360ec85b3", + "value": "0x000000000000000000000000000000000000000000000000000000008debe84f" + }, + { + "op": "delete", + "key": "0x00c901cfbb39e551189323bea0af6cf9e81008c90dce65cbc2ff4672e263d7bb871a" + }, + { + "op": "set", + "key": "0x0055bbfb5b05f80543bd662a7b7a74980a2992612798087fa24e1fc86b8e8508e4b2", + "value": "0x0000000000000000000000000000000000000000000000000000000052fbeee9" + }, + { + "op": "set", + "key": "0xff68e867f5f899fc01d641a00cea2c42d58f3cc061797135198a85301d1e290306efdc84373704529ffcbc3d4ed318de0ec2cdf5f61dd800c143f44a741af2f838a0", + "value": "0x0000000000000000000000000000000000000000000000000000000012acb6e5" + }, + { + "op": "set", + "key": "0xffe60d91d1d11837912311124089c6b73c9d726c99f4df797eb48c2b7fec6e5e2c3b9034b692f2597d8c9c9ad6921ecd112d2f69cc50a6e065034e2ace540e8928ab", + "value": "0x0000000000000000000000000000000000000000000000000000000088a67fe9" + }, + { + "op": "set", + "key": "0xffe60d91d1d11837912311124089c6b73c9d726c99f4df797eb48c2b7fec6e5e2c9c921b846417391ff7e9525580b53876b30e3f6cf563e5849525f6fa88bf230f1a", + "value": "0x0000000000000000000000000000000000000000000000000000000075b67af1" + }, + { + "op": "set", + "key": "0xff78a2812688114caf4452a5611322977e4b1000e59ef408d4c8e2c90028370bcb279b454b0f4a6026943322996543a3cbb6c41d7c467067d9bc010242feef3c2c4b", + "value": "0x00000000000000000000000000000000000000000000000000000000fd6d065d" + }, + { + "op": "set", + "key": "0x00bded9122e6171a5dad9ce2aaebd933abf4718e9bf12448cf113064c9fd8e6ad0a4", + "value": "0x000000000000000000000000000000000000000000000000000000001c92d573" + }, + { + "op": "delete", + "key": "0xffe60d91d1d11837912311124089c6b73c9d726c99f4df797eb48c2b7fec6e5e2c9c921b846417391ff7e9525580b53876b30e3f6cf563e5849525f6fa88bf230f1a" + }, + { + "op": "set", + "key": "0x003cbfd03e3f083b0987f841c4372031faf655a63472518a3613e2e30ecb542b514a", + "value": "0x00000000000000000000000000000000000000000000000000000000617ad32c" + }, + { + "op": "set", + "key": "0xffec64b2de1fc1d53b795f49a1c84ca4172bc3292472c3e244e4f8c225fa786f56b7bce92ed9ae70f9ca2c10d21fe857f18cd2b64d54c70193d8a0dd772967c2580d", + "value": "0x00000000000000000000000000000000000000000000000000000000565e29f9" + }, + { + "op": "delete", + "key": "0xff78a2812688114caf4452a5611322977e4b1000e59ef408d4c8e2c90028370bcb279b454b0f4a6026943322996543a3cbb6c41d7c467067d9bc010242feef3c2c4b" + }, + { + "op": "set", + "key": "0xffd1afd6427b13f2ce7beb82a2cd6bbfc58eac8b2d46bc87bfbc78fbf440e18a635bd91a1bb3fea09cd07c1f6f0b5b0928ec465c3d5409c3dc4d9f3b384fd276b0e3", + "value": "0x00000000000000000000000000000000000000000000000000000000f03eb650" + }, + { + "op": "delete", + "key": "0x00bded9122e6171a5dad9ce2aaebd933abf4718e9bf12448cf113064c9fd8e6ad0a4" + }, + { + "op": "set", + "key": "0xffc901cfbb39e551189323bea0af6cf9e81008c90dce65cbc2ff4672e263d7bb871b0daa3888412359e9769bc566073558cc37b927578bcc74743311f2e7487706f3", + "value": "0x000000000000000000000000000000000000000000000000000000004fe9739b" + }, + { + "op": "delete", + "key": "0x0068e867f5f899fc01d641a00cea2c42d58f3cc061797135198a85301d1e290306c3" + }, + { + "op": "set", + "key": "0x003cbfd03e3f083b0987f841c4372031faf655a63472518a3613e2e30ecb542b5188", + "value": "0x00000000000000000000000000000000000000000000000000000000cb3d4cf3" + }, + { + "op": "set", + "key": "0x0046aeb3243f905d4e0eac7f60ee8962848c87e79dfbd019ece14fe45bd720ea6bf1", + "value": "0x0000000000000000000000000000000000000000000000000000000031d31f66" + }, + { + "op": "set", + "key": "0x0015c4813bb5b1499e0a7e0d46daab8e10c70940c008c566d5c68557b1523d1f2974", + "value": "0x000000000000000000000000000000000000000000000000000000007d5ea0f1" + }, + { + "op": "set", + "key": "0xff29e4e6bbd168fe5b46b7d34c34b628f95d54c4e75f9335a7e96282d1173b07dfd0f9925799d37553e0f34b2d7a49a135fc34a90b6fb99bfe70f2e43c28e7588553", + "value": "0x00000000000000000000000000000000000000000000000000000000a389ad08" + }, + { + "op": "set", + "key": "0xff5ac76a8f23131e4ca988c8afcc313130cc95e521d2691635fc26196d63e66a1f3e2bba64771273ebc79252548c60f34e55d39cd7d0084b6e45722cab3f7021e333", + "value": "0x000000000000000000000000000000000000000000000000000000009ffa1deb" + }, + { + "op": "delete", + "key": "0xff29e4e6bbd168fe5b46b7d34c34b628f95d54c4e75f9335a7e96282d1173b07dfd0f9925799d37553e0f34b2d7a49a135fc34a90b6fb99bfe70f2e43c28e7588553" + }, + { + "op": "set", + "key": "0xff5ac76a8f23131e4ca988c8afcc313130cc95e521d2691635fc26196d63e66a1f1dc0339709def5b953c8f87f8f5316c6ea67241e73291c53d9d020d11b9f19e976", + "value": "0x000000000000000000000000000000000000000000000000000000003a91fad5" + }, + { + "op": "set", + "key": "0x00c28c21982a56a6cecf595bfeb78297c7432abfb487cc75c09db2856a118f441d10", + "value": "0x00000000000000000000000000000000000000000000000000000000112c6343" + }, + { + "op": "set", + "key": "0x00998f08d7054351e5fa3ccadda32ab8d6c0c86a825dd360d57c0fe6b46612297c62", + "value": "0x000000000000000000000000000000000000000000000000000000003b456d06" + }, + { + "op": "delete", + "key": "0xff5ac76a8f23131e4ca988c8afcc313130cc95e521d2691635fc26196d63e66a1f3e2bba64771273ebc79252548c60f34e55d39cd7d0084b6e45722cab3f7021e333" + }, + { + "op": "set", + "key": "0xffe60d91d1d11837912311124089c6b73c9d726c99f4df797eb48c2b7fec6e5e2c8ff835410b84cea50f38a8d78746de71c5e0179f2354107cbabfefdd7bdfc0e1bb", + "value": "0x000000000000000000000000000000000000000000000000000000007cf1b2dd" + }, + { + "op": "set", + "key": "0xff5ac76a8f23131e4ca988c8afcc313130cc95e521d2691635fc26196d63e66a1f3e2bba64771273ebc79252548c60f34e55d39cd7d0084b6e45722cab3f7021e337", + "value": "0x0000000000000000000000000000000000000000000000000000000097519cec" + }, + { + "op": "set", + "key": "0x002eb90959edcd0b6a8a7922b8b8d37cd9664001c217b4c8e6d2df4b8ede329a71ac", + "value": "0x00000000000000000000000000000000000000000000000000000000243042d6" + }, + { + "op": "set", + "key": "0xff543907253acb230f5dc96bbafd8ea0f37dd63b30e8a4a3675bb007673a31fbc5985ffe04dbd5489766385a2bbb382923670eb0e2bb3aaf46231bb53efc33320431", + "value": "0x000000000000000000000000000000000000000000000000000000001a9aedd3" + }, + { + "op": "set", + "key": "0x0015c4813bb5b1499e0a7e0d46daab8e10c70940c008c566d5c68557b1523d1f2946", + "value": "0x000000000000000000000000000000000000000000000000000000001c19f56c" + }, + { + "op": "set", + "key": "0x0046aeb3243f905d4e0eac7f60ee8962848c87e79dfbd019ece14fe45bd720ea6ba3", + "value": "0x00000000000000000000000000000000000000000000000000000000b6c4df3f" + }, + { + "op": "delete", + "key": "0x00c28c21982a56a6cecf595bfeb78297c7432abfb487cc75c09db2856a118f441d10" + }, + { + "op": "set", + "key": "0xff2eb90959edcd0b6a8a7922b8b8d37cd9664001c217b4c8e6d2df4b8ede329a71806e6656ad617b45d8e28cb6bd39e7244735a104a3ba4dd21833624b3c7afc179e", + "value": "0x0000000000000000000000000000000000000000000000000000000040a78667" + }, + { + "op": "delete", + "key": "0x00e61994d31ceae39cae5feb02303eb90e0d4ae90f5b0755c3c26adc81f360ec85b3" + }, + { + "op": "delete", + "key": "0x0015c4813bb5b1499e0a7e0d46daab8e10c70940c008c566d5c68557b1523d1f2946" + }, + { + "op": "set", + "key": "0x0034bb4f3468340d5f72275da92420aed94db48bbc61823e92f29f14f51590373acc", + "value": "0x00000000000000000000000000000000000000000000000000000000a3a1004d" + }, + { + "op": "set", + "key": "0x00543907253acb230f5dc96bbafd8ea0f37dd63b30e8a4a3675bb007673a31fbc5ac", + "value": "0x00000000000000000000000000000000000000000000000000000000623bdd27" + }, + { + "op": "delete", + "key": "0xff5ac76a8f23131e4ca988c8afcc313130cc95e521d2691635fc26196d63e66a1f1dc0339709def5b953c8f87f8f5316c6ea67241e73291c53d9d020d11b9f19e976" + }, + { + "op": "set", + "key": "0x009c847c439d6431d36ddeb326cb553da2e967778a24afbd1d23a62412186faf01a4", + "value": "0x0000000000000000000000000000000000000000000000000000000072373c82" + }, + { + "op": "set", + "key": "0x001df77729bb76b139342cfc86aff011590bf6bb810f47e69a6babac30af96f9f5e6", + "value": "0x000000000000000000000000000000000000000000000000000000003ad8ea87" + }, + { + "op": "set", + "key": "0xffd1afd6427b13f2ce7beb82a2cd6bbfc58eac8b2d46bc87bfbc78fbf440e18a63f1c284d88e488ae8ee1cab2420e5f5c90b4e243c1a8ed32228b22b4974400d5982", + "value": "0x0000000000000000000000000000000000000000000000000000000098b77420" + }, + { + "op": "set", + "key": "0xff29e4e6bbd168fe5b46b7d34c34b628f95d54c4e75f9335a7e96282d1173b07df754b0f807e1dc46306d4cb12e85e18d008678fa80cd9e6fa118c34e179bd412a60", + "value": "0x0000000000000000000000000000000000000000000000000000000067da7aa1" + }, + { + "op": "delete", + "key": "0xffd1afd6427b13f2ce7beb82a2cd6bbfc58eac8b2d46bc87bfbc78fbf440e18a63f1c284d88e488ae8ee1cab2420e5f5c90b4e243c1a8ed32228b22b4974400d5982" + }, + { + "op": "set", + "key": "0x00c901cfbb39e551189323bea0af6cf9e81008c90dce65cbc2ff4672e263d7bb875e", + "value": "0x000000000000000000000000000000000000000000000000000000004360379b" + }, + { + "op": "set", + "key": "0x004c3a8541d9ec369a9fd4bcdad885cc3b166da70ed6a83066924727d9ff147fce2a", + "value": "0x0000000000000000000000000000000000000000000000000000000088bedbe4" + }, + { + "op": "set", + "key": "0xff34bb4f3468340d5f72275da92420aed94db48bbc61823e92f29f14f51590373aef2032e9c5c80ba9048f874aaea79ab7ce9e0f910b0e98955e60542e3a7f44645e", + "value": "0x00000000000000000000000000000000000000000000000000000000951aef03" + }, + { + "op": "delete", + "key": "0xffe60d91d1d11837912311124089c6b73c9d726c99f4df797eb48c2b7fec6e5e2c3b9034b692f2597d8c9c9ad6921ecd112d2f69cc50a6e065034e2ace540e8928ab" + }, + { + "op": "delete", + "key": "0xff543907253acb230f5dc96bbafd8ea0f37dd63b30e8a4a3675bb007673a31fbc5985ffe04dbd5489766385a2bbb382923670eb0e2bb3aaf46231bb53efc33320431" + }, + { + "op": "set", + "key": "0xff15c4813bb5b1499e0a7e0d46daab8e10c70940c008c566d5c68557b1523d1f29cac055e3c06fef0ce191be15524a246c11d1103158d4f8b49d31c629e5520053ce", + "value": "0x00000000000000000000000000000000000000000000000000000000f4c28a25" + }, + { + "op": "delete", + "key": "0x0034bb4f3468340d5f72275da92420aed94db48bbc61823e92f29f14f51590373acc" + }, + { + "op": "set", + "key": "0x0029e4e6bbd168fe5b46b7d34c34b628f95d54c4e75f9335a7e96282d1173b07dfae", + "value": "0x000000000000000000000000000000000000000000000000000000005085c081" + }, + { + "op": "set", + "key": "0xff0861030791246737d706c0d0017de01a26278bbe4ce8a134f5e0cd6f7eb69bcfbf2761d1a4e27ad313f45a816d320676aa7ee1cb19958f32f02c32fa7ed16c3753", + "value": "0x00000000000000000000000000000000000000000000000000000000297c61be" + }, + { + "op": "delete", + "key": "0x005963897639cd11d686b199366a1249d76bc89e0f7161c9eca48d9a199435952d0f" + }, + { + "op": "delete", + "key": "0xffd1afd6427b13f2ce7beb82a2cd6bbfc58eac8b2d46bc87bfbc78fbf440e18a635bd91a1bb3fea09cd07c1f6f0b5b0928ec465c3d5409c3dc4d9f3b384fd276b0e3" + }, + { + "op": "set", + "key": "0x0034bb4f3468340d5f72275da92420aed94db48bbc61823e92f29f14f51590373a69", + "value": "0x0000000000000000000000000000000000000000000000000000000084f5ccf0" + }, + { + "op": "set", + "key": "0xff15c4813bb5b1499e0a7e0d46daab8e10c70940c008c566d5c68557b1523d1f294495e8d51dcafaa894ea80097c7ad006d6b75b9eb85fad2698d1e9e7026e25924c", + "value": "0x00000000000000000000000000000000000000000000000000000000382c3c12" + }, + { + "op": "set", + "key": "0xff18e25b9b05a714bd17ede1445ee7c785983a0194a3ed103783662bdc401dd7ff2fb776a093982841cfb2c6c0a1fca9bc72a0ac907d132b7e3c6ac33b402bfba00f", + "value": "0x0000000000000000000000000000000000000000000000000000000056888c29" + }, + { + "op": "set", + "key": "0x00e61994d31ceae39cae5feb02303eb90e0d4ae90f5b0755c3c26adc81f360ec85b0", + "value": "0x00000000000000000000000000000000000000000000000000000000e1920236" + }, + { + "op": "set", + "key": "0x00998f08d7054351e5fa3ccadda32ab8d6c0c86a825dd360d57c0fe6b46612297cce", + "value": "0x0000000000000000000000000000000000000000000000000000000012f2403d" + }, + { + "op": "set", + "key": "0xff2eb90959edcd0b6a8a7922b8b8d37cd9664001c217b4c8e6d2df4b8ede329a71806e6656ad617b45d8e28cb6bd39e7244735a104a3ba4dd21833624b3c7afc17c3", + "value": "0x00000000000000000000000000000000000000000000000000000000988e6a70" + }, + { + "op": "set", + "key": "0x0055a6923f7414ba482ae8a64b407f386a0bb0491b742bd4b22b5b4b44fca84e8420", + "value": "0x00000000000000000000000000000000000000000000000000000000c34d354f" + }, + { + "op": "set", + "key": "0xffc901cfbb39e551189323bea0af6cf9e81008c90dce65cbc2ff4672e263d7bb871b0daa3888412359e9769bc566073558cc37b927578bcc74743311f2e7487706a2", + "value": "0x00000000000000000000000000000000000000000000000000000000f5506557" + }, + { + "op": "delete", + "key": "0xff29e4e6bbd168fe5b46b7d34c34b628f95d54c4e75f9335a7e96282d1173b07df754b0f807e1dc46306d4cb12e85e18d008678fa80cd9e6fa118c34e179bd412a60" + }, + { + "op": "delete", + "key": "0x001df77729bb76b139342cfc86aff011590bf6bb810f47e69a6babac30af96f9f5e6" + }, + { + "op": "set", + "key": "0xff34bb4f3468340d5f72275da92420aed94db48bbc61823e92f29f14f51590373aeaa08ae14db4be9f284878fd074dce4d3ac11a41a69fde2feb4b3aaaa37da25b3a", + "value": "0x000000000000000000000000000000000000000000000000000000004a39fc3e" + }, + { + "op": "set", + "key": "0xffd1d141c1bed8bb4f809292e9340a0209cd167484a24a5ca993fcac4b97086d89fc2fb3a4956876528a21d73d01500adb051b92012a19d7aadb3b3ef7900625d1f8", + "value": "0x000000000000000000000000000000000000000000000000000000003dcc0aa0" + }, + { + "op": "delete", + "key": "0xff18e25b9b05a714bd17ede1445ee7c785983a0194a3ed103783662bdc401dd7ff2fb776a093982841cfb2c6c0a1fca9bc72a0ac907d132b7e3c6ac33b402bfba00f" + }, + { + "op": "set", + "key": "0x002cebacb183e2f51a3b6cb9f7fe2bd11c399bf1bdb8ce95ed457d3e46190197a0b0", + "value": "0x0000000000000000000000000000000000000000000000000000000039770101" + }, + { + "op": "set", + "key": "0x00e61994d31ceae39cae5feb02303eb90e0d4ae90f5b0755c3c26adc81f360ec850a", + "value": "0x000000000000000000000000000000000000000000000000000000002c55bae8" + }, + { + "op": "set", + "key": "0xff55a6923f7414ba482ae8a64b407f386a0bb0491b742bd4b22b5b4b44fca84e8426e25f0b1203d26d74a17012d28956de0586bf66f21665af66b3b0447aa0c5be33", + "value": "0x000000000000000000000000000000000000000000000000000000000151a01d" + }, + { + "op": "delete", + "key": "0x002eb90959edcd0b6a8a7922b8b8d37cd9664001c217b4c8e6d2df4b8ede329a71ac" + }, + { + "op": "set", + "key": "0xffd1afd6427b13f2ce7beb82a2cd6bbfc58eac8b2d46bc87bfbc78fbf440e18a63aeb0aca39c8f6ba440835a8e371b8b2719dec5b24a57f1da0e7519c3cd74beff5f", + "value": "0x0000000000000000000000000000000000000000000000000000000025f33c5c" + }, + { + "op": "set", + "key": "0x00c28c21982a56a6cecf595bfeb78297c7432abfb487cc75c09db2856a118f441d6a", + "value": "0x000000000000000000000000000000000000000000000000000000002b43a7ea" + }, + { + "op": "set", + "key": "0xff46aeb3243f905d4e0eac7f60ee8962848c87e79dfbd019ece14fe45bd720ea6b919d757e67485d253a689ff08c388cf32b629d2fb6fc4c514296d9b64fb4e58d46", + "value": "0x0000000000000000000000000000000000000000000000000000000092b6620d" + }, + { + "op": "set", + "key": "0xff29e4e6bbd168fe5b46b7d34c34b628f95d54c4e75f9335a7e96282d1173b07df754b0f807e1dc46306d4cb12e85e18d008678fa80cd9e6fa118c34e179bd412a28", + "value": "0x00000000000000000000000000000000000000000000000000000000dc940eaf" + }, + { + "op": "delete", + "key": "0xff55a6923f7414ba482ae8a64b407f386a0bb0491b742bd4b22b5b4b44fca84e8426e25f0b1203d26d74a17012d28956de0586bf66f21665af66b3b0447aa0c5be33" + }, + { + "op": "delete", + "key": "0x003cbfd03e3f083b0987f841c4372031faf655a63472518a3613e2e30ecb542b514a" + }, + { + "op": "delete", + "key": "0xff2eb90959edcd0b6a8a7922b8b8d37cd9664001c217b4c8e6d2df4b8ede329a71806e6656ad617b45d8e28cb6bd39e7244735a104a3ba4dd21833624b3c7afc179e" + }, + { + "op": "set", + "key": "0x004b83f061efb2f4708114e46b5ef50a255528c28c80bbe5fc6ad88e892bd4c4ea44", + "value": "0x00000000000000000000000000000000000000000000000000000000ee265e9b" + }, + { + "op": "delete", + "key": "0x0055a6923f7414ba482ae8a64b407f386a0bb0491b742bd4b22b5b4b44fca84e8420" + }, + { + "op": "set", + "key": "0xffec64b2de1fc1d53b795f49a1c84ca4172bc3292472c3e244e4f8c225fa786f56b7bce92ed9ae70f9ca2c10d21fe857f18cd2b64d54c70193d8a0dd772967c2581b", + "value": "0x000000000000000000000000000000000000000000000000000000008e764b06" + }, + { + "op": "set", + "key": "0x0068e867f5f899fc01d641a00cea2c42d58f3cc061797135198a85301d1e2903064d", + "value": "0x00000000000000000000000000000000000000000000000000000000975fb41f" + }, + { + "op": "delete", + "key": "0x00e61994d31ceae39cae5feb02303eb90e0d4ae90f5b0755c3c26adc81f360ec850a" + }, + { + "op": "set", + "key": "0x002eb90959edcd0b6a8a7922b8b8d37cd9664001c217b4c8e6d2df4b8ede329a7199", + "value": "0x000000000000000000000000000000000000000000000000000000005e2f9796" + }, + { + "op": "set", + "key": "0xffd1d141c1bed8bb4f809292e9340a0209cd167484a24a5ca993fcac4b97086d894d1ea4867b18a8e6a71ba19163c8fe25b3e543a8e6ab30e503bcb8af1a9189b757", + "value": "0x00000000000000000000000000000000000000000000000000000000ed5bec78" + }, + { + "op": "delete", + "key": "0xff34bb4f3468340d5f72275da92420aed94db48bbc61823e92f29f14f51590373aeaa08ae14db4be9f284878fd074dce4d3ac11a41a69fde2feb4b3aaaa37da25b3a" + }, + { + "op": "set", + "key": "0x0046aeb3243f905d4e0eac7f60ee8962848c87e79dfbd019ece14fe45bd720ea6b7f", + "value": "0x00000000000000000000000000000000000000000000000000000000963e1ccc" + }, + { + "op": "set", + "key": "0xff412c4d14e5bde6ea4c898a1d0f911f12a2cf775a62e3cc4c0f28331d432e8f10fb3754ee233f7964e47e5e73dc4342ee28be4dcf5e0bb64009bcbfd0c440559c39", + "value": "0x000000000000000000000000000000000000000000000000000000000ba6fe9e" + }, + { + "op": "set", + "key": "0x005963897639cd11d686b199366a1249d76bc89e0f7161c9eca48d9a199435952df4", + "value": "0x0000000000000000000000000000000000000000000000000000000017844180" + }, + { + "op": "set", + "key": "0xffe2f41518226671332da4adddab1fdcb9903c7b895c7943b669067fe96b0baacfaa95ffdfb787027e946ffafdb95a26702b197af7fb203ed9c203a4c7faa5016b33", + "value": "0x000000000000000000000000000000000000000000000000000000003c4466d6" + }, + { + "op": "set", + "key": "0xff0861030791246737d706c0d0017de01a26278bbe4ce8a134f5e0cd6f7eb69bcfa71b8622781be6cace7f8de1f1aaa611f095226899a85a29a996b9cb36bdc5899a", + "value": "0x000000000000000000000000000000000000000000000000000000008e7018f2" + }, + { + "op": "set", + "key": "0x000ed33feae99b046218c56bec56750a98beb5fd6a51ec13218024cb2455586b439e", + "value": "0x000000000000000000000000000000000000000000000000000000009ffbe4be" + }, + { + "op": "delete", + "key": "0x003cbfd03e3f083b0987f841c4372031faf655a63472518a3613e2e30ecb542b5188" + }, + { + "op": "set", + "key": "0x002cebacb183e2f51a3b6cb9f7fe2bd11c399bf1bdb8ce95ed457d3e46190197a0b0", + "value": "0x00000000000000000000000000000000000000000000000000000000e03763ae" + }, + { + "op": "set", + "key": "0x00e57860ba0adcdd1294caf74da8b990a80a8a6861bc01b30dfe07450d25bab38f2f", + "value": "0x000000000000000000000000000000000000000000000000000000005b187368" + }, + { + "op": "set", + "key": "0x004c1eb4996f3098c89ee461d61bd5f177587b0f4363ea73f4387e6296c66a260fcf", + "value": "0x00000000000000000000000000000000000000000000000000000000a33c6a30" + }, + { + "op": "delete", + "key": "0xff5ac76a8f23131e4ca988c8afcc313130cc95e521d2691635fc26196d63e66a1f3e2bba64771273ebc79252548c60f34e55d39cd7d0084b6e45722cab3f7021e337" + }, + { + "op": "set", + "key": "0x0078a2812688114caf4452a5611322977e4b1000e59ef408d4c8e2c90028370bcb29", + "value": "0x00000000000000000000000000000000000000000000000000000000a3d74d25" + }, + { + "op": "delete", + "key": "0xff0861030791246737d706c0d0017de01a26278bbe4ce8a134f5e0cd6f7eb69bcfbf2761d1a4e27ad313f45a816d320676aa7ee1cb19958f32f02c32fa7ed16c3753" + }, + { + "op": "delete", + "key": "0xff412c4d14e5bde6ea4c898a1d0f911f12a2cf775a62e3cc4c0f28331d432e8f10fb3754ee233f7964e47e5e73dc4342ee28be4dcf5e0bb64009bcbfd0c440559c39" + }, + { + "op": "set", + "key": "0x00e57860ba0adcdd1294caf74da8b990a80a8a6861bc01b30dfe07450d25bab38f00", + "value": "0x0000000000000000000000000000000000000000000000000000000006ebdda1" + }, + { + "op": "set", + "key": "0x000fb23a930b05e3bdfbdd0e6694fa9df9c5059ceabc60618bc40931ddb379f1f698", + "value": "0x00000000000000000000000000000000000000000000000000000000451e0697" + }, + { + "op": "set", + "key": "0xffc901cfbb39e551189323bea0af6cf9e81008c90dce65cbc2ff4672e263d7bb87b6b9a84c441401ccd95da2860ed9d8280486af5808c4c4451016344482faa97969", + "value": "0x0000000000000000000000000000000000000000000000000000000092464772" + }, + { + "op": "set", + "key": "0x002eb90959edcd0b6a8a7922b8b8d37cd9664001c217b4c8e6d2df4b8ede329a7111", + "value": "0x00000000000000000000000000000000000000000000000000000000b715b9b5" + }, + { + "op": "delete", + "key": "0x002eb90959edcd0b6a8a7922b8b8d37cd9664001c217b4c8e6d2df4b8ede329a7199" + }, + { + "op": "set", + "key": "0xffd1d141c1bed8bb4f809292e9340a0209cd167484a24a5ca993fcac4b97086d89c3be5dbd9bb86ee98903d86eb770fe1c69b3d13353d7843180f2fe702bb019d631", + "value": "0x0000000000000000000000000000000000000000000000000000000002d25007" + }, + { + "op": "set", + "key": "0x0055a6923f7414ba482ae8a64b407f386a0bb0491b742bd4b22b5b4b44fca84e84c4", + "value": "0x00000000000000000000000000000000000000000000000000000000d8a2831d" + }, + { + "op": "set", + "key": "0x007b1eefd69ea014244937aaeaea4e8d4d296c04edd75ae47897e5612ef5a1a2521d", + "value": "0x00000000000000000000000000000000000000000000000000000000bf0e71f6" + }, + { + "op": "set", + "key": "0xffd1d141c1bed8bb4f809292e9340a0209cd167484a24a5ca993fcac4b97086d89c3be5dbd9bb86ee98903d86eb770fe1c69b3d13353d7843180f2fe702bb019d6cc", + "value": "0x0000000000000000000000000000000000000000000000000000000009603b04" + }, + { + "op": "set", + "key": "0xff1df77729bb76b139342cfc86aff011590bf6bb810f47e69a6babac30af96f9f5e7d6ce6d6a1c26b63a60e8b794be3719768baf49ed0d8a610f1fc3768675f31eff", + "value": "0x00000000000000000000000000000000000000000000000000000000967aea19" + }, + { + "op": "set", + "key": "0xff1df77729bb76b139342cfc86aff011590bf6bb810f47e69a6babac30af96f9f5ffa1e869c4566231d09357daec90e71aa93821a7347b29175ef4a1bc02a0b3819a", + "value": "0x000000000000000000000000000000000000000000000000000000004ed13c61" + }, + { + "op": "delete", + "key": "0xffd1d141c1bed8bb4f809292e9340a0209cd167484a24a5ca993fcac4b97086d89fc2fb3a4956876528a21d73d01500adb051b92012a19d7aadb3b3ef7900625d1f8" + } + ], + "roots_after": [ + "0xe4a4cc81bad5ec263e1470b6766f0e48c44cb6de9edfafe42ce6a742e7c0de35", + "0x628d4752f5a37f73d798da0fd84216c8c96d376e81ca8c011a1ef0e5b293037d", + "0xe4a4cc81bad5ec263e1470b6766f0e48c44cb6de9edfafe42ce6a742e7c0de35", + "0x0000000000000000000000000000000000000000000000000000000000000000", + "0x36d0ac2cbaa8daac84e3abb0711753ab2803d39fb7def6e34b95e607b5177c92", + "0xcdd2d3edb22a14874aa1b8adc01bad8e80dd4f6f0c603788bd28705981409ba7", + "0x0438e8ad308f977fe36353cde64fef7f1210dd3820b5275c3fffe712e342f125", + "0xe8514dc5fbf6794122aad2a058ad95df0835731e49061dfc5238fe8673b7cb57", + "0xfa1bed5d0c8187d176cdabee2a0b6d96ec72791326c1d9d00555e2752fa4ae06", + "0x388f561f8cd9b274f5c9cd3154ab49c7586c9cae7b28c0b7471be9be98cba3be", + "0x36fca86e1497e245753b5662e45cac10ce68278494adef2b1219b534d63411e2", + "0x98ebe09efe3ff35ee2f8b20732a3f30408119a25e620107c33f503cd39e55143", + "0xcd53e141a34d3c96464d66bc027b4cc0731739f1e645aced12c51072463f8fc4", + "0xbba122989fb7ec24899f02a1aa37fcf967d84f6c5207d843ac9a754b1eaa7ba3", + "0xb219a14dd80c8cc69152edbcec566fe773892d5e615e310b063e99def0f6aa79", + "0x3531deb75570f3a160ba9b471a10be04750c59ed54984186a46d4521e5058145", + "0xd3e835c31b1c00c8f864d7da97f035c6147452c3762a7f69b174751b815737a8", + "0xd74280c2db61bd2bd9d739ae4c38053ce6192f683907b95c6c0fed0ddf6b1b66", + "0x24a2abd1e5f37cbb1e104406be5ee8b6c4989c27bb6ec117ae4e18c0b66dfc9c", + "0x3a7c4099580d9b766538946c88846c95d7822fd4d6121a20e57e1791026600f5", + "0xf84fc76ce622a6933cebc9540fcb022e151a7c4c31a79eaa5be6ffd0d20cd68a", + "0x34cddc1590a8c17711ca84362e563a067b2b7ad70c1921f75fe2d220fd75d465", + "0xb3d8bde73b3332d3cb4ddc466011a4cceaae38bc39d1c33bad6995571047863b", + "0xf685cf0a94e0bac75afeb7397fb89635115e02166e94ce7d3219930ee684b7aa", + "0xdc07f0e78a0411352056d13b4ccd4b06745536c085eafecef9ddab91b9e6f0ec", + "0x43354ad010af4ae17379ccbad6e33039963b86efc10494cf4a7da5348a82cce3", + "0xf05ba16b614295a0687836cb157331aa02fbab9b6458277d9ed6d5659a586b77", + "0xd4f10dbd728c2a8ee6f4863d865d689d8ff229ff616c3d157d449667ea41cbef", + "0x6697b3f6f789029b04581af31b1c70925c38fa1ccfa3be1e0af3ef0be56ada02", + "0x0916316805ae030068246d81a7c9f83faba0393fdb158b2d3322d33e3b99b3f1", + "0x8f460c4cfa508a08de9f592ba68be56ef2f459944807d664ff4860055d341778", + "0x17b98089c48e35a0e2fad4732551346ab3720b6397fc94438ba2685184d5d5cf", + "0x10745294edf5a205225b306851c19f21a677d36875784526cf4c33fd04b5965d", + "0xe96e76f22ae166564705f77db0d1f67df31bf0cca73918e5aa7d510592e7d26c", + "0xb0059099dacec9e2eaa10817ef063d845f606b5bf3da9f6b2b95f38b2cb828f8", + "0x7d48bf3f3dadf2f5d299b5db3d4256e7af01acf2dd68ebd9fcc2c6e4dcb34931", + "0x00b6772ae1bedc77b377240ec00fd3cbbc2ab562c7cf043c5828b51495e9c8eb", + "0xa96401326fd9a4f0397988cb3170ca4b99c85eae5c50c9a373ae08684506ae6e", + "0x473dca4e608cc4103adbb59779dd4a2eb414dacd4da4043ee76a764dd4c2c4c1", + "0xa883f89ca7f388c82a0ef4a20dae822a2d9d486af8a57c690cef42d344e9d068", + "0x455d8f1b93c1a54715dd32179efef0efbde084e5253e4b4986f09581fe44aa34", + "0x5fe5322c6fb9a568924a518319e71a4dd8880d52e02d7714f870b713f1e44f19", + "0xf1660dee83406bd7010044d77d4906bc320c32efde101af9303fb2a8614c090d", + "0x1062401a0d50ea2216cbb77eb944a1c744dfa017b8b692b09b0c02e7d256fbc2", + "0x2c3710cf55cb465e6e5655d3f49ca4f0f114d63f5af052a1a09591dfc137ccf2", + "0xfe8bfaad67203350c7c7f848704424cb433454ef6a430871bd1826e44f587dfb", + "0x48705b693bc63fae4ec2591320c9136ea8b4716dbe12076463419e5276272091", + "0x73d7c247b742add3a39176f85a1bbb43cf935b938dc593f1a2261a37fff10fdd", + "0x0928445cf8ba29f383de766efab11ea09f43cc1b8b85488e4c6f9c735a95a505", + "0x2061ee6e89dd8cd77cade4361ba6b42f547b75a77cfcfd8ca24913374b45011a", + "0xcbbe2daa7666d29abacecdede49ec1a8493a1322f7858f54feebcdb783b983c0", + "0x309114c5600aefd10c61b468f20e8704e7348e8a0f81a9679c9c2feadf9fa045", + "0x78179d80cdd06fddd31c9564b6a05e9b61ce68af301c021691e38badb57439ba", + "0x5c56ebafe3115002fbd0407d0f68bfe5eaa11f59ae9c147db3c7dcbf58ed6933", + "0x74a61b583f5ed143c5fff4ad3f06c086dcfa4b3f81d8868ad3402a16e4e5ad61", + "0xc159413554e131b1b771a43ba0b81794cc0623f011847fee13fd0257b37e4bc9", + "0xa692607dd1c8d9d9c21a457256e717f4a8862e68d8829ab4bb30b1bde3ff77ad", + "0x66ad930c29af810ddfaed2fb9fab745c4cd8f275f24d8d5802306c83aaaaa187", + "0xfc5d2d911b277aad35c310352d8552c4c02285f78273899be5d362f57b24ff0f", + "0x6e1b8ec9bfd2348b47a1076da4af2bfd28d6a3f1fe440b77223fc02aff233e3b", + "0x0ab3b476e01d3d4a8d92e9e92cb21424aa79ff00c8aa67d66118df0853fd1aa8", + "0x02a179e8653fcd5c4a091af100a099f5a8daedf15829470ab42c93b1cd0052d0", + "0x26d58d5aee3469d83c2fab012feef23b0fc21cc1b1d8503fa3ee57aadf24fa27", + "0x5b4fb8fab835c618c266acd82f15f222808a0fdbc3a1b17710eb07c68ee77238", + "0x62a2efaf3e03b45251b3cd2e410f62479cf70e3eb76a8b8fd2e20ca435608f71", + "0x533163db2f23acfc392d7c5d4904506c8ec80bab454bffb8e6249025490c3c24", + "0x5940a36786e1918a942076cc9bf4aa7dd231f76fca798dbad3abc49a793e86b4", + "0x6ab82da71213fb78645123fa9640c75cf9cac72e4f875efc85b330b61e3ee457", + "0x74be56f45537f975b3f984dbe6b57d44d403171f58acc0381199374b12ccd716", + "0x800d6ad3bfd7dca5cb3221e7fce585981ff95828b169814ea19f00a6e857179a", + "0xb284fe5afd4f52bf43c3179955169afb24d8a971b240400029780db1666a30f8", + "0xf5680518cc857872694fb6d24a1b05f998a77e16a565fff86407ce47ca36e6e2", + "0x4254f5a644a680f16d3759c0cc009b8d72b3db268889684c8ef6142357ec0f37", + "0xb4589863140a51c6a79921cc67f1ebfe5d880d2e298da31f30164cb339ef120e", + "0xdf9b6039ad535a07df1d4ec7f98545cdfeb7be56e3fbc336734a64acd03756bb", + "0x28530cdd71a24116b5b491b85fadc81bdd2bf3bbcefc4524f92c8c79a3ff52c5", + "0xc59a155b824a2d1b748cc8598ba316f649c9e19f693199d053cd1ea13bc1739b", + "0xfd3134a639bf799aacd3da7699049a5d98e8f82b4a0ae77857cd95272375f45b", + "0xfa1f552ff3a234c78ced77795ab1cc9d21032cd441e4b32298a3413e6d9a7bd7", + "0x0f825e88b6c02f4c2861deafdece5b9ec2510c6f4cfbb7aa90ec69f3ac51bd8d", + "0xceb55f8bd19bf9be41e1191ac399ef5f3bb136b78a33cb3d79567ff58b880486", + "0x6800397b7a74844edcdf2a69660e8806952b1f985b87d1f91dc97a1b5b29e02f", + "0x55cd9f91c8afc3c515a56dd4b66dcebe61dabc180b543e84b9b304bde020bebe", + "0xa23cca1ebf002b00da78f1edca6222f29d3bb9362a802346f8be9f3805447c0b", + "0xf199828ecf1bfa0ea2e0fc155664044feb11d3b85584ce8d3914613dfacf7c63", + "0x8964f205fd6fc07ef01ef8c7054d10feb7e48a94dd32dbfe9399e5dadae98f2d", + "0x629157eae49544b71670b81dcefabcbf11bf4f36a678fb79f2c5f7aebcc2f6f2", + "0x9db599af3a0c0f67950a21c41e1b42f656fd8acb92468f66071a175239d75e1a", + "0x1429f012d2b3a739b3dd2d01b6588eec54156942bf7f15d803dc115e859df10a", + "0x82a88763b6f8be194a68db2f4a00d8851d64e5ff1f148db8c94628c23cdb383f", + "0x6e6e9c59bd19f242fb2c98088074174e5dba96273870a8c56e377bb0315625db", + "0xabb423d1467c7106349d6b49e55036a83c631f99f55a913973ef148bca0e2110", + "0x1a6acd3760f5e5034b9e07498c489c02beff79e53ffdfc0fd0ce4c80f79da243", + "0xde3e17da380803759ef54eccca593c1e34911b6fc94eef5c0d71d93e71c26239", + "0x6271971bf8389ef7325dcf1613409f5b0080473964f2f022132ffcb78e5888eb", + "0x4453ab7cb93660c10dfa508f853d08cc0ae48749d9b4117d5e3d9d25d055bad6", + "0x54cb98722c2e0d36fdcee552650c8dca3e24f9facf270f719d9d2a2929725ed7", + "0x19a55d1d6f124e490a3833b71f5f8e6612393af922475ee9ca61b9ccf9654d38", + "0x563dd892856e5a4ea1945c5c5d5796076e7021a50543d9f618716be5d31a3c78", + "0xb2c79611944dd7ba2517cd56bf20d23f95f9021bf3f554e83aa81d4e08f50cfc", + "0xedfdbc86c72543bacde2dabc8f6fd189d803eb500dfc973c6403132804cfd5ee", + "0x3614c14b8a9e85ced687272e3753a281b34557de3995edf5f1add5077fbe4b62", + "0xbc9e363bd3f39a9fda1a273101ce0f53f01543e066da803252b6840c1ec2af33", + "0x91e4b5c526dfe9fa0c2b77ae807b53b6ae668614879455278600fc6328f511c4", + "0x49e2520ba83d39ed15c04bd21ceaf86b22fd782589223cf6b289f9036fc212ff", + "0x7735f713872008b61a37dab679f3a86b817f9201d461d2c524307ac8473726ed", + "0x582de5014feebda942b686f32a846b74fc2c0439d1fa685544cb51b2ecf7caf2", + "0xaad81ca1c4469678e4bb6d427c7865000f7345b14a8b95dc9486eccd363452bb", + "0x5a682033d72dd9135286624f43e70346635b73cd69624d224311b5f9112f9ae8", + "0x0cff541bd8dfb5375ba7623de4d05bf3eef5bcd9869aebe3aff348daab8b460c", + "0xbb2d25d7656f789dd29b78132479e9975afef139ba6516c691b8e1525b70d8e5", + "0x73e5d4ad8e6d554f8f497c6a3489995f2c29b9ba028c837b5900ccd4b4aa2871", + "0x3be8d4b932299459a57f214970b120a85aa0c57ab9cd5e76f9320ac9a6dcc02a", + "0x17755de46c02d2e9750b7320c678cd08f84d4c89e075063f2be85c705fce00f0", + "0xc2a1c481fc1491eb4c7f4e59823f81ea704c04cd3e2da51f0e52a3516bc57f89", + "0xd2b40ce291f5b9216f391402c92a679c3fe0920f9042039fdae3ecee10e2a6e3", + "0xff016b6951ce61766f9ec1907176d3f58b0f9263d69469730a5b847b41740528", + "0x65aee0fbadc249e3a4384191a1ad5aba6d453a5de5bbf6101be7db9451203104", + "0x858540442e239355d7605797cdaf1ff9bb5daa18aa11ff05a638e8877b4d82bb", + "0x426144d7b7feba9b6d220fc8063c937bf23c17a67585ccb733ed811240af9c06" + ] + }, + { + "seed": 20260727, + "ops": [ + { + "op": "set", + "key": "0x002eb90959edcd0b6a8a7922b8b8d37cd9664001c217b4c8e6d2df4b8ede329a7170", + "value": "0x0000000000000000000000000000000000000000000000000000000068535e9a" + }, + { + "op": "set", + "key": "0xff3cbfd03e3f083b0987f841c4372031faf655a63472518a3613e2e30ecb542b5111589c57f4d8ad55d2c4ece4c6b63961c45be7479bfd08d04fab2d26d0acb7a091", + "value": "0x0000000000000000000000000000000000000000000000000000000056756dfe" + }, + { + "op": "set", + "key": "0x007b1eefd69ea014244937aaeaea4e8d4d296c04edd75ae47897e5612ef5a1a2521a", + "value": "0x000000000000000000000000000000000000000000000000000000005959a793" + }, + { + "op": "delete", + "key": "0x002eb90959edcd0b6a8a7922b8b8d37cd9664001c217b4c8e6d2df4b8ede329a7170" + }, + { + "op": "delete", + "key": "0x007b1eefd69ea014244937aaeaea4e8d4d296c04edd75ae47897e5612ef5a1a2521a" + }, + { + "op": "set", + "key": "0x0055a6923f7414ba482ae8a64b407f386a0bb0491b742bd4b22b5b4b44fca84e8468", + "value": "0x000000000000000000000000000000000000000000000000000000003c2b7202" + }, + { + "op": "delete", + "key": "0xff3cbfd03e3f083b0987f841c4372031faf655a63472518a3613e2e30ecb542b5111589c57f4d8ad55d2c4ece4c6b63961c45be7479bfd08d04fab2d26d0acb7a091" + }, + { + "op": "delete", + "key": "0x0055a6923f7414ba482ae8a64b407f386a0bb0491b742bd4b22b5b4b44fca84e8468" + }, + { + "op": "set", + "key": "0xffe60d91d1d11837912311124089c6b73c9d726c99f4df797eb48c2b7fec6e5e2c6066e8c722a574a900a59551ed956d32ca2b36e48771ed8b373da6b223bbbe9c8f", + "value": "0x000000000000000000000000000000000000000000000000000000009bb7df73" + }, + { + "op": "set", + "key": "0x009c847c439d6431d36ddeb326cb553da2e967778a24afbd1d23a62412186faf0130", + "value": "0x00000000000000000000000000000000000000000000000000000000def11b80" + }, + { + "op": "set", + "key": "0x007b1eefd69ea014244937aaeaea4e8d4d296c04edd75ae47897e5612ef5a1a2524d", + "value": "0x00000000000000000000000000000000000000000000000000000000f05708e7" + }, + { + "op": "set", + "key": "0x00998f08d7054351e5fa3ccadda32ab8d6c0c86a825dd360d57c0fe6b46612297cba", + "value": "0x00000000000000000000000000000000000000000000000000000000c433224b" + }, + { + "op": "set", + "key": "0xff5ac76a8f23131e4ca988c8afcc313130cc95e521d2691635fc26196d63e66a1f33b1e44125772918b9d44a12766eb0dad773e863d120780173c394789aa668e925", + "value": "0x00000000000000000000000000000000000000000000000000000000abbc594e" + }, + { + "op": "set", + "key": "0xff998f08d7054351e5fa3ccadda32ab8d6c0c86a825dd360d57c0fe6b46612297c36c8cce2938e709d43205827ddebecc780d7e631bb1acb24046af6e5b23e445aa2", + "value": "0x00000000000000000000000000000000000000000000000000000000219ea23a" + }, + { + "op": "set", + "key": "0x00d1d141c1bed8bb4f809292e9340a0209cd167484a24a5ca993fcac4b97086d89af", + "value": "0x00000000000000000000000000000000000000000000000000000000e015951e" + }, + { + "op": "delete", + "key": "0x00d1d141c1bed8bb4f809292e9340a0209cd167484a24a5ca993fcac4b97086d89af" + }, + { + "op": "set", + "key": "0xffc28c21982a56a6cecf595bfeb78297c7432abfb487cc75c09db2856a118f441df2a84c02b0cbaafcabc4cbb5f988d916f3287032f07e3f8bf51588b4311ed04c72", + "value": "0x00000000000000000000000000000000000000000000000000000000973ab40a" + }, + { + "op": "set", + "key": "0x00e2f41518226671332da4adddab1fdcb9903c7b895c7943b669067fe96b0baacfdb", + "value": "0x000000000000000000000000000000000000000000000000000000000c8a8e64" + }, + { + "op": "delete", + "key": "0x007b1eefd69ea014244937aaeaea4e8d4d296c04edd75ae47897e5612ef5a1a2524d" + }, + { + "op": "set", + "key": "0xff15c4813bb5b1499e0a7e0d46daab8e10c70940c008c566d5c68557b1523d1f29cac055e3c06fef0ce191be15524a246c11d1103158d4f8b49d31c629e552005366", + "value": "0x000000000000000000000000000000000000000000000000000000001a5e6148" + }, + { + "op": "delete", + "key": "0x00998f08d7054351e5fa3ccadda32ab8d6c0c86a825dd360d57c0fe6b46612297cba" + }, + { + "op": "set", + "key": "0xffe57860ba0adcdd1294caf74da8b990a80a8a6861bc01b30dfe07450d25bab38f63b4527cc539651483c1b056383959f91cb98fdbd3c1799a854a2781ddb3229037", + "value": "0x000000000000000000000000000000000000000000000000000000008734797f" + }, + { + "op": "set", + "key": "0x000861030791246737d706c0d0017de01a26278bbe4ce8a134f5e0cd6f7eb69bcff4", + "value": "0x00000000000000000000000000000000000000000000000000000000386408da" + }, + { + "op": "set", + "key": "0xff5963897639cd11d686b199366a1249d76bc89e0f7161c9eca48d9a199435952d359cb4900b950c37e7546543cb5a55b8a4e32316f9ddc7cb39743747f93009f45d", + "value": "0x00000000000000000000000000000000000000000000000000000000b10b1b96" + }, + { + "op": "set", + "key": "0x00543907253acb230f5dc96bbafd8ea0f37dd63b30e8a4a3675bb007673a31fbc571", + "value": "0x000000000000000000000000000000000000000000000000000000007bda4a3e" + }, + { + "op": "delete", + "key": "0xff15c4813bb5b1499e0a7e0d46daab8e10c70940c008c566d5c68557b1523d1f29cac055e3c06fef0ce191be15524a246c11d1103158d4f8b49d31c629e552005366" + }, + { + "op": "set", + "key": "0x00d1afd6427b13f2ce7beb82a2cd6bbfc58eac8b2d46bc87bfbc78fbf440e18a632f", + "value": "0x00000000000000000000000000000000000000000000000000000000381c8a4a" + }, + { + "op": "set", + "key": "0xff2cebacb183e2f51a3b6cb9f7fe2bd11c399bf1bdb8ce95ed457d3e46190197a0a0a0268860a87b73c5d023e0181059e1d49f8ed387a132e90d5ccf7b7589f952f2", + "value": "0x000000000000000000000000000000000000000000000000000000000c2e34b8" + }, + { + "op": "set", + "key": "0x0029e4e6bbd168fe5b46b7d34c34b628f95d54c4e75f9335a7e96282d1173b07dfc7", + "value": "0x00000000000000000000000000000000000000000000000000000000b4025b23" + }, + { + "op": "set", + "key": "0xff55a6923f7414ba482ae8a64b407f386a0bb0491b742bd4b22b5b4b44fca84e843a068760d000d05d6c87419289f5e5adb67a586f14948a8dac8873ed19f3d6ef65", + "value": "0x0000000000000000000000000000000000000000000000000000000055789247" + }, + { + "op": "delete", + "key": "0x009c847c439d6431d36ddeb326cb553da2e967778a24afbd1d23a62412186faf0130" + }, + { + "op": "set", + "key": "0xffe2f41518226671332da4adddab1fdcb9903c7b895c7943b669067fe96b0baacfed266f135d9b962dee7803d8894e76641c4aeb45c9d87a11b132682e19724ea737", + "value": "0x0000000000000000000000000000000000000000000000000000000076d452b6" + }, + { + "op": "delete", + "key": "0xffe2f41518226671332da4adddab1fdcb9903c7b895c7943b669067fe96b0baacfed266f135d9b962dee7803d8894e76641c4aeb45c9d87a11b132682e19724ea737" + }, + { + "op": "set", + "key": "0x000fb23a930b05e3bdfbdd0e6694fa9df9c5059ceabc60618bc40931ddb379f1f624", + "value": "0x0000000000000000000000000000000000000000000000000000000048332348" + }, + { + "op": "set", + "key": "0x00412c4d14e5bde6ea4c898a1d0f911f12a2cf775a62e3cc4c0f28331d432e8f100e", + "value": "0x0000000000000000000000000000000000000000000000000000000034e8d1dd" + }, + { + "op": "set", + "key": "0x001df77729bb76b139342cfc86aff011590bf6bb810f47e69a6babac30af96f9f5c2", + "value": "0x000000000000000000000000000000000000000000000000000000001904c8c3" + }, + { + "op": "set", + "key": "0xffe60d91d1d11837912311124089c6b73c9d726c99f4df797eb48c2b7fec6e5e2c6066e8c722a574a900a59551ed956d32ca2b36e48771ed8b373da6b223bbbe9c2f", + "value": "0x00000000000000000000000000000000000000000000000000000000d1033f40" + }, + { + "op": "set", + "key": "0x00998f08d7054351e5fa3ccadda32ab8d6c0c86a825dd360d57c0fe6b46612297c20", + "value": "0x00000000000000000000000000000000000000000000000000000000d11ac616" + }, + { + "op": "set", + "key": "0x00c28c21982a56a6cecf595bfeb78297c7432abfb487cc75c09db2856a118f441d01", + "value": "0x000000000000000000000000000000000000000000000000000000009e8da098" + }, + { + "op": "delete", + "key": "0x00e2f41518226671332da4adddab1fdcb9903c7b895c7943b669067fe96b0baacfdb" + }, + { + "op": "set", + "key": "0x007b1eefd69ea014244937aaeaea4e8d4d296c04edd75ae47897e5612ef5a1a252fc", + "value": "0x000000000000000000000000000000000000000000000000000000003e436d7f" + }, + { + "op": "set", + "key": "0x00e57860ba0adcdd1294caf74da8b990a80a8a6861bc01b30dfe07450d25bab38f0c", + "value": "0x000000000000000000000000000000000000000000000000000000002c32286a" + }, + { + "op": "delete", + "key": "0xffe60d91d1d11837912311124089c6b73c9d726c99f4df797eb48c2b7fec6e5e2c6066e8c722a574a900a59551ed956d32ca2b36e48771ed8b373da6b223bbbe9c8f" + }, + { + "op": "delete", + "key": "0x00d1afd6427b13f2ce7beb82a2cd6bbfc58eac8b2d46bc87bfbc78fbf440e18a632f" + }, + { + "op": "set", + "key": "0xffc28c21982a56a6cecf595bfeb78297c7432abfb487cc75c09db2856a118f441dc855e1f0f687b464e5d31a72fec3317fe80974db611da9609c42c92987525ea901", + "value": "0x00000000000000000000000000000000000000000000000000000000381c77f7" + }, + { + "op": "set", + "key": "0x00998f08d7054351e5fa3ccadda32ab8d6c0c86a825dd360d57c0fe6b46612297ce2", + "value": "0x0000000000000000000000000000000000000000000000000000000017ab1191" + }, + { + "op": "set", + "key": "0x00c28c21982a56a6cecf595bfeb78297c7432abfb487cc75c09db2856a118f441d5f", + "value": "0x000000000000000000000000000000000000000000000000000000008c47c001" + }, + { + "op": "set", + "key": "0xff18e25b9b05a714bd17ede1445ee7c785983a0194a3ed103783662bdc401dd7ff361b271b7deb81ee7027749296cc9369ae699ca1ef815575a5c0bd3b5b0b0cf8a6", + "value": "0x0000000000000000000000000000000000000000000000000000000078038801" + }, + { + "op": "set", + "key": "0xffd1afd6427b13f2ce7beb82a2cd6bbfc58eac8b2d46bc87bfbc78fbf440e18a6338206d654c672ec71e308a2a76d9c171e5b436fa022e52e5b908a23b352437094e", + "value": "0x00000000000000000000000000000000000000000000000000000000b1a30ba3" + }, + { + "op": "set", + "key": "0xffc901cfbb39e551189323bea0af6cf9e81008c90dce65cbc2ff4672e263d7bb87b6b9a84c441401ccd95da2860ed9d8280486af5808c4c4451016344482faa9794b", + "value": "0x000000000000000000000000000000000000000000000000000000001000fb6e" + }, + { + "op": "set", + "key": "0x004c1eb4996f3098c89ee461d61bd5f177587b0f4363ea73f4387e6296c66a260fc7", + "value": "0x0000000000000000000000000000000000000000000000000000000086cb5d38" + }, + { + "op": "set", + "key": "0x000ed33feae99b046218c56bec56750a98beb5fd6a51ec13218024cb2455586b43ff", + "value": "0x00000000000000000000000000000000000000000000000000000000aae2734a" + }, + { + "op": "set", + "key": "0x00c28c21982a56a6cecf595bfeb78297c7432abfb487cc75c09db2856a118f441d99", + "value": "0x00000000000000000000000000000000000000000000000000000000d35b8891" + }, + { + "op": "set", + "key": "0x00ec64b2de1fc1d53b795f49a1c84ca4172bc3292472c3e244e4f8c225fa786f561c", + "value": "0x00000000000000000000000000000000000000000000000000000000dd9bc05c" + }, + { + "op": "set", + "key": "0x0034bb4f3468340d5f72275da92420aed94db48bbc61823e92f29f14f51590373a00", + "value": "0x000000000000000000000000000000000000000000000000000000007fe863ca" + }, + { + "op": "set", + "key": "0x00a43fb2d7beb12b34786a837467f62b7d6c5dab4ab7eecc89c0b67db76a68292754", + "value": "0x0000000000000000000000000000000000000000000000000000000085af5a94" + }, + { + "op": "delete", + "key": "0x00998f08d7054351e5fa3ccadda32ab8d6c0c86a825dd360d57c0fe6b46612297ce2" + }, + { + "op": "delete", + "key": "0x00c28c21982a56a6cecf595bfeb78297c7432abfb487cc75c09db2856a118f441d5f" + }, + { + "op": "set", + "key": "0xff5963897639cd11d686b199366a1249d76bc89e0f7161c9eca48d9a199435952d4a28b184a4e022d38844c7967da6fbdf879ffafc6cfabf88c929ec9c9ff9004096", + "value": "0x000000000000000000000000000000000000000000000000000000009ac44455" + }, + { + "op": "delete", + "key": "0x0034bb4f3468340d5f72275da92420aed94db48bbc61823e92f29f14f51590373a00" + }, + { + "op": "set", + "key": "0x00e2f41518226671332da4adddab1fdcb9903c7b895c7943b669067fe96b0baacfda", + "value": "0x0000000000000000000000000000000000000000000000000000000042bbddff" + }, + { + "op": "set", + "key": "0x000fb23a930b05e3bdfbdd0e6694fa9df9c5059ceabc60618bc40931ddb379f1f6aa", + "value": "0x0000000000000000000000000000000000000000000000000000000045686f32" + }, + { + "op": "set", + "key": "0x001df77729bb76b139342cfc86aff011590bf6bb810f47e69a6babac30af96f9f57e", + "value": "0x00000000000000000000000000000000000000000000000000000000c7e4a178" + }, + { + "op": "set", + "key": "0x000ed33feae99b046218c56bec56750a98beb5fd6a51ec13218024cb2455586b4307", + "value": "0x00000000000000000000000000000000000000000000000000000000c6550c69" + }, + { + "op": "set", + "key": "0xff4c1eb4996f3098c89ee461d61bd5f177587b0f4363ea73f4387e6296c66a260fed383baf24ff33220d52e65d0501f90eee41f67e456df12e8379bb13167e30a6e7", + "value": "0x00000000000000000000000000000000000000000000000000000000ca881fef" + }, + { + "op": "set", + "key": "0x000fb23a930b05e3bdfbdd0e6694fa9df9c5059ceabc60618bc40931ddb379f1f6bc", + "value": "0x00000000000000000000000000000000000000000000000000000000a0d8701e" + }, + { + "op": "set", + "key": "0x009c847c439d6431d36ddeb326cb553da2e967778a24afbd1d23a62412186faf018c", + "value": "0x00000000000000000000000000000000000000000000000000000000ccb22b18" + }, + { + "op": "set", + "key": "0x007b1eefd69ea014244937aaeaea4e8d4d296c04edd75ae47897e5612ef5a1a252e2", + "value": "0x00000000000000000000000000000000000000000000000000000000a0d0e3b1" + }, + { + "op": "set", + "key": "0xff1df77729bb76b139342cfc86aff011590bf6bb810f47e69a6babac30af96f9f5ffa1e869c4566231d09357daec90e71aa93821a7347b29175ef4a1bc02a0b38152", + "value": "0x00000000000000000000000000000000000000000000000000000000ddb5065b" + }, + { + "op": "set", + "key": "0xffe2f41518226671332da4adddab1fdcb9903c7b895c7943b669067fe96b0baacfab2dcf341b3549396a13ac48963cf4aac70fda23d99a2c2a599e0efe17278e795a", + "value": "0x00000000000000000000000000000000000000000000000000000000f5d11d62" + }, + { + "op": "set", + "key": "0xffbded9122e6171a5dad9ce2aaebd933abf4718e9bf12448cf113064c9fd8e6ad064269e16de5ef26ae08b5660f7eaa784399543edd7bf8fbe04c13ac444cb4e61a6", + "value": "0x00000000000000000000000000000000000000000000000000000000d83e7fab" + }, + { + "op": "set", + "key": "0x004c3a8541d9ec369a9fd4bcdad885cc3b166da70ed6a83066924727d9ff147fce26", + "value": "0x000000000000000000000000000000000000000000000000000000004ef6829a" + }, + { + "op": "delete", + "key": "0xffd1afd6427b13f2ce7beb82a2cd6bbfc58eac8b2d46bc87bfbc78fbf440e18a6338206d654c672ec71e308a2a76d9c171e5b436fa022e52e5b908a23b352437094e" + }, + { + "op": "set", + "key": "0xff0ed33feae99b046218c56bec56750a98beb5fd6a51ec13218024cb2455586b437ef4a13c0f00718e5b1d19138c7f9db57ed5b04d19dca824d5e61fc17dc47ff923", + "value": "0x000000000000000000000000000000000000000000000000000000005743f9a8" + }, + { + "op": "delete", + "key": "0x00c28c21982a56a6cecf595bfeb78297c7432abfb487cc75c09db2856a118f441d01" + }, + { + "op": "delete", + "key": "0x000ed33feae99b046218c56bec56750a98beb5fd6a51ec13218024cb2455586b43ff" + }, + { + "op": "set", + "key": "0xff0861030791246737d706c0d0017de01a26278bbe4ce8a134f5e0cd6f7eb69bcfa71b8622781be6cace7f8de1f1aaa611f095226899a85a29a996b9cb36bdc5892c", + "value": "0x000000000000000000000000000000000000000000000000000000005f7bb192" + }, + { + "op": "delete", + "key": "0x00e2f41518226671332da4adddab1fdcb9903c7b895c7943b669067fe96b0baacfda" + }, + { + "op": "set", + "key": "0xff1df77729bb76b139342cfc86aff011590bf6bb810f47e69a6babac30af96f9f5ffa1e869c4566231d09357daec90e71aa93821a7347b29175ef4a1bc02a0b38114", + "value": "0x00000000000000000000000000000000000000000000000000000000c885cdce" + }, + { + "op": "set", + "key": "0x007293ce391f5cd652216ca8ea31069a06a82f393028dfc8786aa5efd7be734b2c7e", + "value": "0x00000000000000000000000000000000000000000000000000000000403292a8" + }, + { + "op": "set", + "key": "0x0018e25b9b05a714bd17ede1445ee7c785983a0194a3ed103783662bdc401dd7ffc3", + "value": "0x00000000000000000000000000000000000000000000000000000000f61897b9" + }, + { + "op": "delete", + "key": "0x009c847c439d6431d36ddeb326cb553da2e967778a24afbd1d23a62412186faf018c" + }, + { + "op": "set", + "key": "0xff0ed33feae99b046218c56bec56750a98beb5fd6a51ec13218024cb2455586b437ef4a13c0f00718e5b1d19138c7f9db57ed5b04d19dca824d5e61fc17dc47ff971", + "value": "0x000000000000000000000000000000000000000000000000000000002c85a3c9" + }, + { + "op": "delete", + "key": "0x00412c4d14e5bde6ea4c898a1d0f911f12a2cf775a62e3cc4c0f28331d432e8f100e" + }, + { + "op": "delete", + "key": "0xff4c1eb4996f3098c89ee461d61bd5f177587b0f4363ea73f4387e6296c66a260fed383baf24ff33220d52e65d0501f90eee41f67e456df12e8379bb13167e30a6e7" + }, + { + "op": "set", + "key": "0x0034bb4f3468340d5f72275da92420aed94db48bbc61823e92f29f14f51590373a4b", + "value": "0x000000000000000000000000000000000000000000000000000000005a0e27c1" + }, + { + "op": "set", + "key": "0x00e6b6d8d98be54d4c00cee378bedf964f755b5257bf1eabf52cf39aa987686fa725", + "value": "0x00000000000000000000000000000000000000000000000000000000831d5b5f" + }, + { + "op": "set", + "key": "0x0055bbfb5b05f80543bd662a7b7a74980a2992612798087fa24e1fc86b8e8508e4f2", + "value": "0x000000000000000000000000000000000000000000000000000000005f00f860" + }, + { + "op": "set", + "key": "0x00e57860ba0adcdd1294caf74da8b990a80a8a6861bc01b30dfe07450d25bab38fd7", + "value": "0x000000000000000000000000000000000000000000000000000000000e8e281a" + }, + { + "op": "set", + "key": "0x00e57860ba0adcdd1294caf74da8b990a80a8a6861bc01b30dfe07450d25bab38f0e", + "value": "0x0000000000000000000000000000000000000000000000000000000069f1fc26" + }, + { + "op": "delete", + "key": "0x00e6b6d8d98be54d4c00cee378bedf964f755b5257bf1eabf52cf39aa987686fa725" + }, + { + "op": "set", + "key": "0x003cbfd03e3f083b0987f841c4372031faf655a63472518a3613e2e30ecb542b51c0", + "value": "0x000000000000000000000000000000000000000000000000000000001200f8f6" + }, + { + "op": "set", + "key": "0xff46aeb3243f905d4e0eac7f60ee8962848c87e79dfbd019ece14fe45bd720ea6b61214a55d06d98ad4c6ea565d0f88368bc538d91abad58d513e9bcf56ceb156575", + "value": "0x0000000000000000000000000000000000000000000000000000000076a8cd24" + }, + { + "op": "delete", + "key": "0x000ed33feae99b046218c56bec56750a98beb5fd6a51ec13218024cb2455586b4307" + }, + { + "op": "set", + "key": "0xff34bb4f3468340d5f72275da92420aed94db48bbc61823e92f29f14f51590373a648ee142c16b327f9c3d09cc065560a309b3819c017803778f2b8c150cfe486a5e", + "value": "0x00000000000000000000000000000000000000000000000000000000032c6329" + }, + { + "op": "set", + "key": "0xff4b83f061efb2f4708114e46b5ef50a255528c28c80bbe5fc6ad88e892bd4c4ea37491f84eb77d352d7d8fca0484b856ceab7236e2f773d157b3fbd6d0586de7f96", + "value": "0x0000000000000000000000000000000000000000000000000000000073ed547b" + }, + { + "op": "delete", + "key": "0xffe2f41518226671332da4adddab1fdcb9903c7b895c7943b669067fe96b0baacfab2dcf341b3549396a13ac48963cf4aac70fda23d99a2c2a599e0efe17278e795a" + }, + { + "op": "delete", + "key": "0xff5963897639cd11d686b199366a1249d76bc89e0f7161c9eca48d9a199435952d359cb4900b950c37e7546543cb5a55b8a4e32316f9ddc7cb39743747f93009f45d" + }, + { + "op": "set", + "key": "0xff4c1eb4996f3098c89ee461d61bd5f177587b0f4363ea73f4387e6296c66a260f84715f048d0204439e2d98c4c2e5cb144730a98fb2ee40b399d12f783c1fbd31e7", + "value": "0x0000000000000000000000000000000000000000000000000000000038aa3fcb" + }, + { + "op": "set", + "key": "0x00ec64b2de1fc1d53b795f49a1c84ca4172bc3292472c3e244e4f8c225fa786f56e4", + "value": "0x000000000000000000000000000000000000000000000000000000000a42e584" + }, + { + "op": "set", + "key": "0x005ac76a8f23131e4ca988c8afcc313130cc95e521d2691635fc26196d63e66a1fc6", + "value": "0x00000000000000000000000000000000000000000000000000000000a828d5b7" + }, + { + "op": "delete", + "key": "0xffbded9122e6171a5dad9ce2aaebd933abf4718e9bf12448cf113064c9fd8e6ad064269e16de5ef26ae08b5660f7eaa784399543edd7bf8fbe04c13ac444cb4e61a6" + }, + { + "op": "set", + "key": "0x007b1eefd69ea014244937aaeaea4e8d4d296c04edd75ae47897e5612ef5a1a25230", + "value": "0x00000000000000000000000000000000000000000000000000000000297aaea4" + }, + { + "op": "set", + "key": "0x0055a6923f7414ba482ae8a64b407f386a0bb0491b742bd4b22b5b4b44fca84e8403", + "value": "0x000000000000000000000000000000000000000000000000000000001943ef67" + }, + { + "op": "set", + "key": "0xff543907253acb230f5dc96bbafd8ea0f37dd63b30e8a4a3675bb007673a31fbc5047362ff01233f0839180435b0ef58ba27ec43176ea05859f1535020e7ea58cbac", + "value": "0x00000000000000000000000000000000000000000000000000000000e22b6692" + }, + { + "op": "set", + "key": "0xff5ac76a8f23131e4ca988c8afcc313130cc95e521d2691635fc26196d63e66a1f6f3ed9ab4dceab32c8dee7d62338019a5f29043f855967151e0dc6468ab0237da4", + "value": "0x00000000000000000000000000000000000000000000000000000000b6c69c67" + }, + { + "op": "delete", + "key": "0x00e57860ba0adcdd1294caf74da8b990a80a8a6861bc01b30dfe07450d25bab38f0c" + }, + { + "op": "set", + "key": "0x00a43fb2d7beb12b34786a837467f62b7d6c5dab4ab7eecc89c0b67db76a6829270d", + "value": "0x0000000000000000000000000000000000000000000000000000000090cd692a" + }, + { + "op": "set", + "key": "0x005963897639cd11d686b199366a1249d76bc89e0f7161c9eca48d9a199435952d26", + "value": "0x00000000000000000000000000000000000000000000000000000000dfd40afd" + }, + { + "op": "set", + "key": "0xffe57860ba0adcdd1294caf74da8b990a80a8a6861bc01b30dfe07450d25bab38ff0e402bb5b5f30e2141ac064e95a9b5d86bbeab68daeb62a188811213392891f31", + "value": "0x000000000000000000000000000000000000000000000000000000001f5f4611" + }, + { + "op": "set", + "key": "0xff18e25b9b05a714bd17ede1445ee7c785983a0194a3ed103783662bdc401dd7ffd358410b57fde52d1120a72e241687991ced2ec523b82168713b13b557349ab84a", + "value": "0x00000000000000000000000000000000000000000000000000000000cf2387d4" + }, + { + "op": "set", + "key": "0x00e57860ba0adcdd1294caf74da8b990a80a8a6861bc01b30dfe07450d25bab38f3f", + "value": "0x00000000000000000000000000000000000000000000000000000000a55c3e7f" + }, + { + "op": "set", + "key": "0xff5963897639cd11d686b199366a1249d76bc89e0f7161c9eca48d9a199435952dfaf15a1ae0460906134a099130644594d04402f7e11b9ce07e2994acd12b870257", + "value": "0x000000000000000000000000000000000000000000000000000000008292d374" + }, + { + "op": "delete", + "key": "0x00e57860ba0adcdd1294caf74da8b990a80a8a6861bc01b30dfe07450d25bab38f0e" + }, + { + "op": "set", + "key": "0x00998f08d7054351e5fa3ccadda32ab8d6c0c86a825dd360d57c0fe6b46612297cc2", + "value": "0x000000000000000000000000000000000000000000000000000000004e4778f9" + }, + { + "op": "delete", + "key": "0x00e57860ba0adcdd1294caf74da8b990a80a8a6861bc01b30dfe07450d25bab38fd7" + }, + { + "op": "set", + "key": "0x004c1eb4996f3098c89ee461d61bd5f177587b0f4363ea73f4387e6296c66a260ffc", + "value": "0x0000000000000000000000000000000000000000000000000000000042d5f040" + }, + { + "op": "set", + "key": "0xff0ed33feae99b046218c56bec56750a98beb5fd6a51ec13218024cb2455586b437c0a8ed54087e88eba7c638f4ae6c57295dafe6c27063966f0c3238dae958d77d6", + "value": "0x000000000000000000000000000000000000000000000000000000007759aa37" + }, + { + "op": "set", + "key": "0x00ec64b2de1fc1d53b795f49a1c84ca4172bc3292472c3e244e4f8c225fa786f56ca", + "value": "0x0000000000000000000000000000000000000000000000000000000014f8a61a" + }, + { + "op": "delete", + "key": "0xff2cebacb183e2f51a3b6cb9f7fe2bd11c399bf1bdb8ce95ed457d3e46190197a0a0a0268860a87b73c5d023e0181059e1d49f8ed387a132e90d5ccf7b7589f952f2" + } + ], + "roots_after": [ + "0x8a7b32f0179f8591d8d29bbc5ce478d2d1cca5ceebddb5c6880e6682fd1a7458", + "0x63e670849972d58347a67cb04d30b369145ea59ac06fd411c231b46c05b534c8", + "0xeedfc71a50202763dc37c74f994b52783a5e3ac19ff7c3d818c9bd2c95b562f7", + "0x1cfbe1c8b87bb9ecafd506ee003ddaeef688eaa24a564cd7147270130425709e", + "0xf8daed5ef73e34987561ea33d980ed96600f48700aa75ea5706c74f79babdb3d", + "0xcf348977fb91b672c061404e5bc6f063a444d19478b4aba7450531d48ae5b8f8", + "0xfa4116de24b5dce1e1ebb9736c78a00ec1eb957aaee7e9f9af3b74f68e43a73e", + "0x0000000000000000000000000000000000000000000000000000000000000000", + "0x5f2fc0f08e034ab42e6b4b8eec8038618707b8f2ae9722e14a6048d1935a438c", + "0xbc0812ac0dbf193721b4e90a5f23d702399df82d1e1da571c9e2388155b5409f", + "0xe105f7395ae6845ec524e9dcdf9a33600d7eece3b9dacaf7fee43deca2ad0957", + "0x5f30927f1062975cf9ad72910c99386ffc609b4dac5e1633a0a7a051ff221ca1", + "0x6f8469d0d9572b492c6c170c43f734aea5714da7d2e47ac9c10a01938439a1cb", + "0x7a75f5784c851040e436de70bafabe4abddc0baab0db46583879fdc1a7bcbd01", + "0xaa03aca82638a0c50233cb98152c122b0c21c4efb2a47035a690b5a65a2e54ed", + "0x7a75f5784c851040e436de70bafabe4abddc0baab0db46583879fdc1a7bcbd01", + "0xf6d984c0ed4b562ad15789d556f759f72b44627fb197780e88d36f450c60cd4e", + "0x52ae36bcfdb7559ffaf993e3d23b7727ada4f1403f12a9f16ba0324610852a8e", + "0x7d48f61dc2656ca3fc52efbb16bf80b4806cf43aaef56397c9d526d739ab2ad4", + "0x867a92986423254fd9f362a682695d3eb986bd5106cadabbd72dd0faf70c0cf9", + "0xdd8d43ad115e1d5602e4a0f7cbfccd29da88f2284373e37278ac2ce5f28131e2", + "0x51ac92480dcdb4b73e8719cc66828fb8432c4f3e95b46bfd6188c95a57274841", + "0x14009bff462d71b74de902e736e1778e890b278e4431efe45214929624bb481d", + "0xced79329ddc4ae49289ee09b8f62294f1f58ad05d083bf234ab13940aebb9107", + "0x917f45ace41a685469e386217064d3e0f0a3a032cea58693662477712677db41", + "0x41ee495051942cc30f2917f35bd8c47e6410db43a72349251bb28c07bcc8f7c0", + "0x5386f094b07d2f66e2ba710c22adf75d674e5480ce8eb7d9ce00562b7ccdbbda", + "0x932786f0f35419fa30390e38d105809200011124fa0b6d5f8637b9b94487d9c3", + "0xb2b13dd966509e731383fd5e711ef659424c6a3a2be5e5340106030c9f519d5f", + "0x2462473775c4fdaef9a43b9cfdb52de90adbbc1bbe9b83f52486ac21730668bc", + "0xdf310f60d7c0f7098478fdc1555ff8da49e7a8f8c2a72d5f8c131413f7f1e3d3", + "0x2020b692ff595ae9314cde1600fc0bc2b873005510b9f08c8d337d6722d70a0e", + "0xdf310f60d7c0f7098478fdc1555ff8da49e7a8f8c2a72d5f8c131413f7f1e3d3", + "0xc7228f7de8004b8a65ca83153be3e81879f06d2d6cd892cba1b1de9aba11614b", + "0x3feeb22432b94e3606acede5ca9746bef4f66e9cdefb20f33f74067127b0a410", + "0xd34ed045b9a3328b8ec8eec5ebdada6a7fd96f53a440f8936b8dd04b03af5cc6", + "0x1b678937a3a948e9ce99fade1a5d0dc150940113738943156f10774ae45f47ce", + "0x1fe1978bef4bc11e93d0a05934465817ebc45300017f2932193522245de21654", + "0x171f50e2cefe8e1be7b3a23f4a20784399dbe23a1c739f3969949601f1aae309", + "0xdb90b4c3ce181ad457acb4793acf3c96948787b70af018dafa0c7bab14f39a8a", + "0x1322106c7281a64177a3dac41747ca47cd91b4a38a8ea205b1fb2194e0fe7ca3", + "0x0221eb75c0dc23bf40b5188be6b92d19a0e53caf128844124a9172d80f1b2725", + "0xb20b2aba4193b97f68469528a50b13a5f7a144b58b76cd264fbb6126eea66caa", + "0x4be94b29b40ae37b08e286933b5750eb17386823de830d42f7fd7ae82fb80212", + "0x3739fae016f7cbdd579fa2e23272918307447402ef643d81498743db90d313b2", + "0x842465452bd071e6e1f4e41e4552c5612705e7410f81fd17cc63f328a344bc0d", + "0xf746f4b36fb573b026e8bd423db2cfb6e6e7f2ab90991e0f9c1107d673bf7c47", + "0xf65214fc8cb48d01e283effa100f78cfb3047c184fcf22747844163b5ca68f35", + "0xac4073afc5233967fa44dbf0d717c61bf5c646593432c00ec7bef7de47ef5148", + "0x577a76c8fe61bbf59c1b75f4094b4d46014963bf3e3158ec626448f8478d8a76", + "0x5f7af52cb7684df6c01564adab0c0156b71c1a38b3ea56588227e72e3a163487", + "0xfb0618f4c70280db25a8701cebca666b38789966de14153a7110a421c2b48ff1", + "0xbf53d3b4f608fb5526a6a3b2efa79bac939c7b3103fb37e6a5902f3c78e35139", + "0x5facbaf4086e047085733d0b9c7fdb06d3480ccce1644b6fa9f05f857fafe11d", + "0x59f709233c06f71ecd6d8cf51c19016a61c7f6f292fdddc07d6cbd8f0d94fa4f", + "0xf983bc0c064b283f60e9271be36e08eb9a5bb571b6a782b16e40437cf1b277bb", + "0x1caa8a43868e55fdc665d578049ae2366bbd0f318b7ebc2767fc126db9bb2201", + "0x75afded3b32a06e02374a73111c3b5ade73cc4c70aac934063249d347415b4fd", + "0xfa8575b9894f1680e02b02a8c61f36e2e52b891d465cf024b7781333bebd84c5", + "0xa9d5eaa996a13f1e5bc624c337f7b074f88e429a8f3e6bea8a6103dfc7297310", + "0x29d66013eb0e1d6d1019bb1f6c17b94c4b2601c5a31c074581a001dcb1b3de5f", + "0xea4ae741440e0ae8d17c6fb256c866a81d8ef5ffe5b3879d7a0f2cad6090bc83", + "0x91f0332b7b3c046dbdcf5df5d2fde7b2fcf396d066c301ac168136984d900acf", + "0xde834346818b07a71de99175727b41e806510a7c047f55b8ae9454523edef70f", + "0x6af4615582e45a60cc1202dfb98da0b93bdd20629741120fa617c5999886ed85", + "0x9867e8f7bcf9017543ddaf6d048fc64c7428678278dffe7524fb825ce926e483", + "0x01e68be3c4441bd42d76e9bdbeaed3e4487160f1dcc48bd6300032945a41178d", + "0x91abb96562994918dafac8a092d5980c5441d5c37ddaf2352ab13dd21a7c85d3", + "0xdfb62a7c0cfc7411122412d434d5b94ed73a2727f8ee78f1d58acb5288efa174", + "0xafed8a54526bd8f6ce3495ea0f7fdee4c19df5840d5161cc11f9168dc941c21a", + "0xfc2ad7e489591b7543d5649beb07a5d5b2d2ca3fe038eaabbf3f8a247f556830", + "0x9bfc12a4cc0111a48bebf15b2c6562df93ed27eaf10387e34f416f34db7bbf49", + "0x82dc9cd4de2d35630b703499f115a14ddc70abcd86308eaced3aa5346e941f9b", + "0xa9d39a8860d003f370ee04708164638fe0a1f63735b6c7061edb4548affd065a", + "0xdab9d0b5901745fa368b0817baa8b4073220412dad4c366363433adc01dabe9a", + "0x030c73731e872cfe146a7f65f571064aedcc68580b4e9dd5b8175651ca15812f", + "0xf21f6d27d9e74362225ebedfda1757f45e9f7fd2753020e00cd0624f372add26", + "0xc236e92ae41f64008b3859ca59e886509ac20edba5160f6f8d86f8a177dd49b6", + "0x67fc41ed6bd66bbd47633607fa5efbd7011f041bdde718c52031d654fb1ff151", + "0xed616c4e45d6daf19f41f6f8e6be1e4a6e78d5482cb2349627cfed27b40220d7", + "0x5920100f2b7d3fdd8daaaf382632bf947ac1778883d29dc52d7b3b27b8322ca0", + "0x8e532a0ddfaf3275cf424330e9e28d127c7d4b7ba85336e4d7670d8b6a51d89e", + "0x6408ac7fb35a580b444a9eca3a0cdaf0ea80f9d28c6f2fa2b36bd5645dc934ca", + "0xe086cb5fcd0296229f346e58643654260380a1b5d2669099527d82b1a2c887e5", + "0xa655d19ec4e965e2ee228b99106cceedf07003a6becd8d9aad9c998a99fa287f", + "0x0e42e29aeb5388d65880a46e5c2fed00a2e295a7940b4fd4c8dd1201fe4021aa", + "0xc3ccd5e787d5068a6a7bc3ade783f4d00d3dc327f460c65b862514bb6bca7c5f", + "0x594af2c7d5cbc00a2fad3e8f18fa9388a7fbb67eb2437cc30b53f81d1c93e6b2", + "0x2ed46b1eaf76e89f682b9dfbc25418293d0fc20e31f6a8e8aa2e5845a00d1950", + "0xe399fc2d7afdee74b6df09f3efa449e58161fc519827ce443b8d3ac57c44b8d3", + "0x798e93dd492fc434a20c4d308971053a422dcd0b4bc9edd89b975de463267f2f", + "0xd9bb71ae1746bd82e10cca262fdac1faeda58873c0d9de075a9c3c1f383bc858", + "0xbbd2b74b988517a668ac462803252cbcaffde0db93bcefab724f8402dffba5bd", + "0x4570cab3731f10a05c2716de55913730dbc26d6f6feedd6d5cdc972fd6dd2cbc", + "0x23388945017b27a621948d87d133b093552c07bd3b6f2b8e3c6ca7835968114f", + "0x260cb6770d72773f4bc459d1667ac3b61c0f369da18020873a09341c9dec46b9", + "0x197ca877b5028bb013c9c2fb6148d67952c84ba735229575884542e851f3b7e7", + "0xbe6fc89c1a1d2d8168902b4df0048f1131d55f5005b995acbcf6520cc56588b0", + "0xdcc783c7a321e4a25ff1c01dc76f4cd181a51bdf63da44482a4cbf558ec685c3", + "0x47b4242152028429ee980756d40f0a95cfba0a74ff81feb5eb2fd51c4dedba39", + "0xa75f30073712bf5cfeb82c970cd31c36786957208cdb523319508de0698d9a6b", + "0xc6768062ef64adc6c13ec45533cc572b1bc5e0a53d8c7de89c278a835eaf084b", + "0xcb8be0789b86fd77269317ca0ab45433e21a34ff93a7ccb40ea574321a0cd2c0", + "0x97f6b18446e834d19f4497767121f2958c4c0ac339032d5e22b5d06aff83f95e", + "0x1ca598ac8d7443f59616a6c63f8de685b0669e5c6235a2eb3f47a70eeff645de", + "0x167aea5319ec96368f15c71072f452ed9015d596181fd49bf08fa0f47eb10d85", + "0xb9b580ab6672e02107f34acc6e0eb7bae701d2983ceac909173e266e66e8259a", + "0xd9a234e59234c79087c138af5f3eb61d4cb4170efd38635d37c8e759041e73fe", + "0x5b5a60d80b39452f9becb79c199ab04c54bcd261c2e2d1ebc44922518611d60d", + "0xe6c6cab4030aff1b796a1c5d6ae059e1d5f236cc8ce5fafa0866618a136fccbd", + "0xc0d78b67dfb2f0b86ce05922a83a4098b57efd8eb7ef5f05b70cb079437f4647", + "0x25ea70ac1e50d4896bc15b3645f48bef2d704e4017ad25134bd332dcf3646ca3", + "0xe74f0e7d2643c724a0cbacb67e3c19921b32e5708363125c1482b8bee682282c", + "0x70d1b0558b82f83937605a190f1f291470cd311048b66073549200dd6e63406c", + "0xfd5dfa769c89b01540599dc1eac8aae8954a3f5661409bb4b93e793595704ec3", + "0x089c9d9d68a5eacde9efe25c09222683403ee921b8b2aabd8479ec7c12c69106", + "0x26f25564073873edf0f929acc35900b5564acbbfeeae417ea18ef1954a067d17", + "0x8be2cd628bdfac918e03e2201586ffe6528bd810dfbe45f6ede931e540c22700", + "0x8a5b325fc93b1919769121e118c05a4915122554089554497f01e2b66026cf6d", + "0x43fecbe443116f26fbbe022f4474d977467d6cece3cbc53b579ca840f6cd4f67" + ] + } + ], + "embedding_vectors": { + "address": "0xaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa", + "basic_data_key": "0x00d9ae2d236f8713a5bf808cda488167a56cc97e4b83006f42b1c06c0c3f053bbf00", + "code_hash_key": "0x00d9ae2d236f8713a5bf808cda488167a56cc97e4b83006f42b1c06c0c3f053bbf01", + "slots": [ + { + "slot": 0, + "key": "0x00d9ae2d236f8713a5bf808cda488167a56cc97e4b83006f42b1c06c0c3f053bbf40" + }, + { + "slot": 5, + "key": "0x00d9ae2d236f8713a5bf808cda488167a56cc97e4b83006f42b1c06c0c3f053bbf45" + }, + { + "slot": 63, + "key": "0x00d9ae2d236f8713a5bf808cda488167a56cc97e4b83006f42b1c06c0c3f053bbf7f" + }, + { + "slot": 64, + "key": "0xffd9ae2d236f8713a5bf808cda488167a56cc97e4b83006f42b1c06c0c3f053bbf181495ef75f8d2005a4a5bd3c5139cf6b5cdd1a02574832253cc9932687b332a40" + }, + { + "slot": 255, + "key": "0xffd9ae2d236f8713a5bf808cda488167a56cc97e4b83006f42b1c06c0c3f053bbf181495ef75f8d2005a4a5bd3c5139cf6b5cdd1a02574832253cc9932687b332aff" + }, + { + "slot": 256, + "key": "0xffd9ae2d236f8713a5bf808cda488167a56cc97e4b83006f42b1c06c0c3f053bbfe717382bfeaa9f4c014431fb7bbc3c6946dd510a82d49c925d9df7735c26b16f00" + }, + { + "slot": 1000, + "key": "0xffd9ae2d236f8713a5bf808cda488167a56cc97e4b83006f42b1c06c0c3f053bbf2b1a6c18962d993b9da5ed300ce5696bdcad6e09f08b9c0735fa749d417650f9e8" + }, + { + "slot": 57896044618658097711785492504343953926634992332820282019728792003956564819968, + "key": "0xffd9ae2d236f8713a5bf808cda488167a56cc97e4b83006f42b1c06c0c3f053bbf23d37150867b80a99fea951c62d43f974e7d2f53089c5f5683b4fc1d4387286c00" + } + ], + "chunks": [ + { + "chunk": 0, + "key": "0x00d9ae2d236f8713a5bf808cda488167a56cc97e4b83006f42b1c06c0c3f053bbf80" + }, + { + "chunk": 5, + "key": "0x00d9ae2d236f8713a5bf808cda488167a56cc97e4b83006f42b1c06c0c3f053bbf85" + }, + { + "chunk": 127, + "key": "0x00d9ae2d236f8713a5bf808cda488167a56cc97e4b83006f42b1c06c0c3f053bbfff" + }, + { + "chunk": 128, + "key": "0x01465f911ccfe7707c51a877fc621cffa8e700a5828cf92df27b142de919c0f6c800" + }, + { + "chunk": 300, + "key": "0x01465f911ccfe7707c51a877fc621cffa8e700a5828cf92df27b142de919c0f6c8ac" + }, + { + "chunk": 383, + "key": "0x01465f911ccfe7707c51a877fc621cffa8e700a5828cf92df27b142de919c0f6c8ff" + }, + { + "chunk": 384, + "key": "0x0171ad9e407a8d200ab2858bcf828d9016a0b4bc54e6e39dd3e31847b6220a63ce00" + } + ] + }, + "basic_data_vectors": [ + { + "code_size": 0, + "nonce": 0, + "balance": "0", + "value": "0x0000000000000000000000000000000000000000000000000000000000000000" + }, + { + "code_size": 0, + "nonce": 1, + "balance": "1000000000000000000", + "value": "0x0000000000000000000000000000000100000000000000000de0b6b3a7640000" + }, + { + "code_size": 287454020, + "nonce": 6153737369425722316, + "balance": "1512366075204170929049582354406559215", + "value": "0x00000000112233445566778899aabbcc0123456789abcdef0123456789abcdef" + }, + { + "code_size": 24576, + "nonce": 1, + "balance": "1", + "value": "0x0000000000006000000000000000000100000000000000000000000000000001" + } + ], + "chunkify_vectors": [ + { + "name": "empty", + "code": "0x", + "chunks": [] + }, + { + "name": "short", + "code": "0x6001", + "chunks": [ + "0x0060010000000000000000000000000000000000000000000000000000000000" + ] + }, + { + "name": "push_boundary", + "code": "0x6060606060606060606060606060606060606060606060606060606060606060606060606060606060606060606060606060606060606060606060606060", + "chunks": [ + "0x0060606060606060606060606060606060606060606060606060606060606060", + "0x0160606060606060606060606060606060606060606060606060606060606060" + ] + }, + { + "name": "push32_tail", + "code": "0x7feeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeee", + "chunks": [ + "0x007feeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeee", + "0x02eeeeeeeeeeeeeeeeeeee000000000000000000000000000000000000000000" + ] + }, + { + "name": "zeros62", + "code": "0x0000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000", + "chunks": [ + "0x0000000000000000000000000000000000000000000000000000000000000000", + "0x0000000000000000000000000000000000000000000000000000000000000000" + ] + } + ] +} \ No newline at end of file From 61759ca9a0d7fdd7081f8a6e2f6f8d6bd3635311 Mon Sep 17 00:00:00 2001 From: awskii Date: Wed, 29 Jul 2026 23:08:11 +0700 Subject: [PATCH 21/56] execution/commitment: replay EIP-8297 reference roots against the PBin oracle MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit H was applied at four sites — two node preimages and the two key digests — so the previous claim that the hash lived in pbinHasher alone was wrong. All four now route through pbinHashFn, defaulting to Keccak-256, which leaves production behaviour unchanged and lets a suite be substituted whole. The reference implementation in ethereum/execution-specs hashes with BLAKE3. Replaying its vectors under BLAKE3 reproduces all 7 fixed roots and all 600 sequence roots. That reference builds the tree by canonical rebuild while the oracle inserts incrementally as the EIP's pseudocode does, so agreement across that difference is what rules out a shared misreading of the spec. Deletes in the sequences are applied by rebuilding from the surviving key set: the EIP's insert has no removal and the reference's removal semantics are still open, so nothing here depends on a delete algorithm. --- execution/commitment/pbin_hash.go | 25 +++- execution/commitment/pbin_keys.go | 13 +- execution/commitment/pbin_oracle_test.go | 24 ++++ execution/commitment/pbin_specroots_test.go | 131 ++++++++++++++++++++ go.mod | 2 +- 5 files changed, 187 insertions(+), 8 deletions(-) create mode 100644 execution/commitment/pbin_specroots_test.go diff --git a/execution/commitment/pbin_hash.go b/execution/commitment/pbin_hash.go index 3bf488a092e..e5f66480666 100644 --- a/execution/commitment/pbin_hash.go +++ b/execution/commitment/pbin_hash.go @@ -45,11 +45,26 @@ var pbinEmptyTreeHash common.Hash var errPBinCellHash = errors.New("pbin: cell cannot be hashed") -// pbinHasher is the one place H is applied, so swapping the hash function is a -// change to this type alone. Every preimage fits its single scratch buffer, so -// each node costs one hash call and no allocation. Its zero value is ready. +// pbinHashFn is H. EIP-8297 leaves the hash open and names Keccak-256 among the +// candidates (eip:511-513); the execution-specs reference hashes with BLAKE3, so +// tests substitute it to compare roots against that reference. Key derivation +// hashes too, so a suite is only fully swapped when pbinDigestCache is swapped +// with it. +type pbinHashFn func([]byte) common.Hash + +// pbinHasher applies H to node preimages. Every preimage fits its single scratch +// buffer, so each node costs one hash call and no allocation. Its zero value is +// ready and hashes with Keccak-256. type pbinHasher struct { buf [pbinHashBufLen]byte + sum pbinHashFn +} + +func (h *pbinHasher) hash(preimage []byte) common.Hash { + if h.sum != nil { + return h.sum(preimage) + } + return keccak.Sum256(preimage) } // pbinAppendBitPrefix is the spec's encode_bit_prefix (eip:196-201): a two-byte @@ -66,7 +81,7 @@ func (h *pbinHasher) branchHash(prefix *pbinBitpath, left, right *common.Hash) c buf := pbinAppendBitPrefix(append(h.buf[:0], pbinBranchTag), prefix) buf = append(buf, left[:]...) buf = append(buf, right[:]...) - return keccak.Sum256(buf) + return h.hash(buf) } // cellHash is the only way a cell becomes a hash. Keeping it single is what @@ -108,7 +123,7 @@ func (h *pbinHasher) leafCellHash(c *pbinCell, path *pbinBitpath) (common.Hash, if err != nil { return common.Hash{}, err } - return keccak.Sum256(append(buf, value[:]...)), nil + return h.hash(append(buf, value[:]...)), nil } // pbinLeafValue picks the encoding the key's own position names: the zone byte diff --git a/execution/commitment/pbin_keys.go b/execution/commitment/pbin_keys.go index 3d55b6cc7cf..f5e691ebbe9 100644 --- a/execution/commitment/pbin_keys.go +++ b/execution/commitment/pbin_keys.go @@ -111,6 +111,8 @@ func pbinKeyHasher() keyHasher { // correct; changing address invalidates the group entry, which is bound to the // address as well as the index (eip:411-414). type pbinDigestCache struct { + sum pbinHashFn + addr32 [32]byte stem [32]byte valid bool @@ -122,11 +124,18 @@ type pbinDigestCache struct { buf [64]byte } +func (c *pbinDigestCache) hash(preimage []byte) [32]byte { + if c.sum != nil { + return c.sum(preimage) + } + return keccak.Sum256(preimage) +} + func (c *pbinDigestCache) stemDigest(addr32 *[32]byte) *[32]byte { if c.valid && c.addr32 == *addr32 { return &c.stem } - c.stem = keccak.Sum256(addr32[:]) + c.stem = c.hash(addr32[:]) c.addr32 = *addr32 c.valid = true c.groupValid = false @@ -143,7 +152,7 @@ func (c *pbinDigestCache) groupDigest(addr32, slot32 *[32]byte) *[32]byte { copy(c.buf[:32], addr32[:]) c.buf[32] = 0 copy(c.buf[33:], idx[:]) - c.groupHash = keccak.Sum256(c.buf[:]) + c.groupHash = c.hash(c.buf[:]) c.groupIndex = *idx c.groupValid = true return &c.groupHash diff --git a/execution/commitment/pbin_oracle_test.go b/execution/commitment/pbin_oracle_test.go index 01b1692f961..4feb94757a9 100644 --- a/execution/commitment/pbin_oracle_test.go +++ b/execution/commitment/pbin_oracle_test.go @@ -164,10 +164,34 @@ func pbinOracleEncodeBitPrefix(prefix []byte) []byte { } func pbinOracleMerkelize(node pbinOracleNode) [32]byte { + return pbinOracleMerkelizeWith(node, nil) +} + +// pbinOracleMerkelizeWith merkelizes under an explicit H. A nil sum means +// Keccak-256; the execution-specs reference uses BLAKE3, so its vectors are +// replayed by passing blake3 here. +func pbinOracleMerkelizeWith(node pbinOracleNode, sum func([]byte) [32]byte) [32]byte { var out [32]byte if node == nil { return out } + if sum != nil { + var pre []byte + switch n := node.(type) { + case *pbinOracleLeaf: + pre = append(pre, pbinOracleLeafTag) + pre = append(pre, n.key...) + pre = append(pre, n.value...) + case *pbinOracleBranch: + left := pbinOracleMerkelizeWith(n.left, sum) + right := pbinOracleMerkelizeWith(n.right, sum) + pre = append(pre, pbinOracleBranchTag) + pre = append(pre, pbinOracleEncodeBitPrefix(n.prefix)...) + pre = append(pre, left[:]...) + pre = append(pre, right[:]...) + } + return sum(pre) + } h := sha3.NewLegacyKeccak256() switch n := node.(type) { case *pbinOracleLeaf: diff --git a/execution/commitment/pbin_specroots_test.go b/execution/commitment/pbin_specroots_test.go new file mode 100644 index 00000000000..b8e0b06bc48 --- /dev/null +++ b/execution/commitment/pbin_specroots_test.go @@ -0,0 +1,131 @@ +package commitment + +import ( + "encoding/hex" + "encoding/json" + "os" + "sort" + "testing" + + "github.com/stretchr/testify/require" + "lukechampine.com/blake3" +) + +// Root vectors exported from the EIP-8297 reference implementation in +// ethereum/execution-specs (branch projects/binary-trie), which hashes with +// BLAKE3. Replaying them under BLAKE3 checks this package's oracle against an +// implementation that was written independently and, more importantly, builds +// the tree by a different algorithm: the reference rebuilds canonically, the +// oracle inserts incrementally as the EIP's pseudocode does. Agreement across +// that difference is what rules out a shared misreading of the spec. +// +// The engine itself is tied to this oracle by the differential tests, so the +// chain reaches the engine even though the engine hashes with Keccak-256. + +type pbinRootVectors struct { + Meta map[string]string `json:"meta"` + EmptyRoot string `json:"empty_root"` + Trie []struct { + Name string `json:"name"` + Entries []struct { + Key string `json:"key"` + Value string `json:"value"` + } `json:"entries"` + Root string `json:"root"` + } `json:"trie_vectors"` + Sequences []struct { + Seed int `json:"seed"` + Ops []struct { + Op string `json:"op"` + Key string `json:"key"` + Value string `json:"value"` + } `json:"ops"` + RootsAfter []string `json:"roots_after"` + } `json:"sequence_vectors"` +} + +func blake3Sum(b []byte) [32]byte { return blake3.Sum256(b) } + +func loadPBinRootVectors(t *testing.T) pbinRootVectors { + t.Helper() + raw, err := os.ReadFile("testdata/eip8297_vectors.json") + require.NoError(t, err) + var v pbinRootVectors + require.NoError(t, json.Unmarshal(raw, &v)) + require.Equal(t, "blake3", v.Meta["hasher"], "vectors are only replayable under the hash they were generated with") + return v +} + +// pbinOracleRootOf builds the oracle trie from a whole key set and merkelizes it +// under BLAKE3. Building from the surviving set is also how a delete is applied: +// the EIP's insert has no removal, and the reference's removal semantics are +// still open, so nothing here depends on a delete algorithm. +func pbinOracleRootOf(t *testing.T, entries map[string][]byte) [32]byte { + t.Helper() + keys := make([]string, 0, len(entries)) + for k := range entries { + keys = append(keys, k) + } + sort.Strings(keys) + + tree := &pbinOracleTree{} + for _, k := range keys { + tree.insert([]byte(k), entries[k]) + } + return pbinOracleMerkelizeWith(tree.root, blake3Sum) +} + +func TestPBinOracleMatchesSpecTrieRoots(t *testing.T) { + t.Parallel() + v := loadPBinRootVectors(t) + require.NotEmpty(t, v.Trie) + + for _, tc := range v.Trie { + t.Run(tc.Name, func(t *testing.T) { + entries := make(map[string][]byte, len(tc.Entries)) + for _, e := range tc.Entries { + key, err := hex.DecodeString(e.Key[2:]) + require.NoError(t, err) + val, err := hex.DecodeString(e.Value[2:]) + require.NoError(t, err) + entries[string(key)] = val + } + got := pbinOracleRootOf(t, entries) + require.Equal(t, tc.Root[2:], hex.EncodeToString(got[:])) + }) + } +} + +// TestPBinOracleMatchesSpecSequenceRoots replays the reference's op sequences, +// checking the root after every operation rather than only at the end, so a +// divergence is pinned to the op that caused it. +func TestPBinOracleMatchesSpecSequenceRoots(t *testing.T) { + t.Parallel() + v := loadPBinRootVectors(t) + require.NotEmpty(t, v.Sequences) + + checked := 0 + for _, seq := range v.Sequences { + require.Len(t, seq.RootsAfter, len(seq.Ops)) + entries := make(map[string][]byte) + for i, op := range seq.Ops { + key, err := hex.DecodeString(op.Key[2:]) + require.NoError(t, err) + switch op.Op { + case "set": + val, err := hex.DecodeString(op.Value[2:]) + require.NoError(t, err) + entries[string(key)] = val + case "delete": + delete(entries, string(key)) + default: + t.Fatalf("unknown op %q", op.Op) + } + got := pbinOracleRootOf(t, entries) + require.Equal(t, seq.RootsAfter[i][2:], hex.EncodeToString(got[:]), + "seed %d diverges at op %d (%s)", seq.Seed, i, op.Op) + checked++ + } + } + t.Logf("replayed %d reference roots across %d sequences", checked, len(v.Sequences)) +} diff --git a/go.mod b/go.mod index 65426a211a4..ae30695734b 100644 --- a/go.mod +++ b/go.mod @@ -122,6 +122,7 @@ require ( google.golang.org/protobuf v1.36.11 gopkg.in/natefinch/lumberjack.v2 v2.2.1 gopkg.in/yaml.v3 v3.0.1 + lukechampine.com/blake3 v1.4.1 sigs.k8s.io/yaml v1.6.0 ) @@ -441,7 +442,6 @@ require ( google.golang.org/genproto/googleapis/rpc v0.0.0-20260414002931-afd174a4e478 // indirect google.golang.org/grpc/cmd/protoc-gen-go-grpc v1.6.2 // indirect honnef.co/go/tools v0.7.0 // indirect - lukechampine.com/blake3 v1.4.1 // indirect modernc.org/libc v1.66.7 // indirect modernc.org/mathutil v1.7.1 // indirect modernc.org/memory v1.11.0 // indirect From 7a5058fb18c8ed1b5c3eea9951433e55580f5816 Mon Sep 17 00:00:00 2001 From: awskii Date: Thu, 30 Jul 2026 09:34:51 +0700 Subject: [PATCH 22/56] execution/commitment: drive the PBin engine over reference root vectors The oracle already replays the reference's roots; this drives the engine itself, so the comparison no longer goes through our own transcription. Vectors carry raw tree keys and raw values while the engine rebuilds a leaf value from an Update, so each value is mapped onto the field the engine reads for that key position. Tree keys are digests and cannot be inverted, so the plain key is synthetic: only its length is read, to pick the cell field. Six of seven vectors reproduce. full_header_stem spans code-chunk sub-indices, which no Update field carries, and is excluded by an asserted name list so gaining code support breaks this test rather than quietly widening it. --- execution/commitment/pbin_specengine_test.go | 135 +++++++++++++++++++ 1 file changed, 135 insertions(+) create mode 100644 execution/commitment/pbin_specengine_test.go diff --git a/execution/commitment/pbin_specengine_test.go b/execution/commitment/pbin_specengine_test.go new file mode 100644 index 00000000000..e4854016eea --- /dev/null +++ b/execution/commitment/pbin_specengine_test.go @@ -0,0 +1,135 @@ +package commitment + +import ( + "encoding/binary" + "encoding/hex" + "sort" + "testing" + + "github.com/holiman/uint256" + "github.com/stretchr/testify/require" + + "github.com/erigontech/erigon/common" + "github.com/erigontech/erigon/common/length" +) + +// Drives the engine itself over the reference's root vectors, rather than the +// oracle. The vectors carry raw tree keys and raw 32-byte values, while the +// engine rebuilds a leaf's value from an Update according to where the key sits, +// so each value has to be mapped back onto the field the engine will read. +// +// Not every leaf can be expressed that way: a code-chunk sub-index has no Update +// field at all. Those vectors are excluded by name below with an asserted count, +// so gaining code support breaks this test rather than silently widening it. + +type pbinEngineLeaf struct { + treeKey []byte + plainKey []byte + update Update +} + +// pbinLeafFromVector maps a raw (key, value) pair onto the Update the engine +// reads for that key's position. ok is false when the position has no Update +// field to carry the value. +func pbinLeafFromVector(key, value []byte, seq int) (pbinEngineLeaf, bool) { + var l pbinEngineLeaf + l.treeKey = key + + // The plain key is synthetic: tree keys are digests and cannot be inverted. + // Only its length is read, to decide which cell field holds it. + account := make([]byte, length.Addr) + binary.BigEndian.PutUint32(account, uint32(seq)) + storage := make([]byte, length.Addr+length.Hash) + binary.BigEndian.PutUint32(storage, uint32(seq)) + + storageLeaf := func() { + l.plainKey = storage + l.update.Flags = StorageUpdate + l.update.StorageLen = int8(copy(l.update.Storage[:], value)) + } + + if key[0] == pbinStorageZone { + storageLeaf() + return l, true + } + switch sub := key[len(key)-1]; { + case sub == pbinBasicDataLeafKey: + // BASIC_DATA is rebuilt from nonce and balance with code_size forced to + // zero, so a value carrying a code size cannot be reproduced. + if binary.BigEndian.Uint32(value[pbinBasicDataCodeSizeOffset:]) != 0 { + return l, false + } + l.plainKey = account + l.update.Flags = BalanceUpdate | NonceUpdate + l.update.Nonce = binary.BigEndian.Uint64(value[pbinBasicDataNonceOffset:]) + l.update.Balance = *new(uint256.Int).SetBytes(value[pbinBasicDataBalanceOffset:]) + return l, true + case sub == pbinCodeHashLeafKey: + l.plainKey = account + l.update.Flags = CodeUpdate + l.update.CodeHash = common.BytesToHash(value) + return l, true + case sub >= pbinHeaderStorageOffset && sub < pbinCodeOffset: + storageLeaf() + return l, true + default: + return l, false // code chunk: no Update field carries it + } +} + +func TestPBinEngineMatchesSpecTrieRoots(t *testing.T) { + t.Parallel() + v := loadPBinRootVectors(t) + + var ran, excluded []string + for _, tc := range v.Trie { + leaves := make([]pbinEngineLeaf, 0, len(tc.Entries)) + representable := true + for i, e := range tc.Entries { + key, err := hex.DecodeString(e.Key[2:]) + require.NoError(t, err) + val, err := hex.DecodeString(e.Value[2:]) + require.NoError(t, err) + l, ok := pbinLeafFromVector(key, val, i+1) + if !ok { + representable = false + break + } + leaves = append(leaves, l) + } + if !representable { + excluded = append(excluded, tc.Name) + continue + } + ran = append(ran, tc.Name) + + t.Run(tc.Name, func(t *testing.T) { + // tree-key order is the engine's visit invariant + sort.Slice(leaves, func(i, j int) bool { + return string(leaves[i].treeKey) < string(leaves[j].treeKey) + }) + + ms := NewMockState(t) + pph := NewPBinPatriciaHashed(ms) + pph.hasher.sum = func(b []byte) common.Hash { return common.Hash(blake3Sum(b)) } + + for i := range leaves { + require.NoError(t, pph.followAndUpdate(leaves[i].treeKey, leaves[i].plainKey, &leaves[i].update), + "insert %x", leaves[i].treeKey) + } + for pph.grid.activeRows > 0 { + require.NoError(t, pph.fold()) + } + require.NoError(t, pph.storeRoot()) + + got, err := pph.RootHash() + require.NoError(t, err) + require.Equal(t, tc.Root[2:], hex.EncodeToString(got)) + }) + } + + t.Logf("engine ran %d/%d reference root vectors: %v", len(ran), len(v.Trie), ran) + t.Logf("excluded (no Update field for a code-chunk leaf): %v", excluded) + require.Equal(t, []string{"full_header_stem"}, excluded, + "exclusions must stay pinned: gaining code support should widen this list, not hide it") +} From 7b4315677aa6e2d2fe264ab1f5df2797a908f8ed Mon Sep 17 00:00:00 2001 From: awskii Date: Thu, 30 Jul 2026 13:13:51 +0700 Subject: [PATCH 23/56] =?UTF-8?q?docs:=20add=20PBin=20M1=20plan=20?= =?UTF-8?q?=E2=80=94=20binary=20trie=20as=20a=20local=20EL=20state=20trie?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Wires the M0 engine into the domain path so a dev-chain container can boot on it with --experimental.bin-commitment. Keccak stays the production hash; BLAKE3 is a test-only override for replaying the reference vectors. The header state-root check becomes an independent toggle defaulting to on, so a self-produced chain keeps it as an oracle and only a foreign-header chain turns it off. Grounded in a survey of the five integration surfaces. Carries 13 hazards as acceptance criteria and 5 open questions, two of which need an upstream answer before the paths that depend on them can be built. --- docs/plans/20260730-pbin-m1-local-el.md | 378 ++++++++++++++++++++++++ 1 file changed, 378 insertions(+) create mode 100644 docs/plans/20260730-pbin-m1-local-el.md diff --git a/docs/plans/20260730-pbin-m1-local-el.md b/docs/plans/20260730-pbin-m1-local-el.md new file mode 100644 index 00000000000..bd278bd80e8 --- /dev/null +++ b/docs/plans/20260730-pbin-m1-local-el.md @@ -0,0 +1,378 @@ +# PBin M1 — binary trie as a local EL state trie + +## Overview + +M0 landed `PBinPatriciaHashed`, an EIP-8297 binary commitment engine that computes correct roots in memory and reproduces 6 of 7 root vectors from the reference implementation. It is not wired to anything: the domain path panics, code never enters the tree, and the hash is Keccak-256 while every other client uses BLAKE3. + +M1 makes it run. **Target: a dev-chain container started with `--experimental.bin-commitment`, booting and producing blocks on the binary trie.** + +Two deliberate scope choices define what that means: + +- **Keccak-256 stays the production hash.** BLAKE3 is a **test-only** override, used to replay the reference vectors. This is not cross-client compatibility and must not be described as such — no other client would agree with our roots. +- **The header state-root check becomes independently togglable, defaulting to ON.** It is *not* gated on the variant. On a chain we produce ourselves the check is worth keeping — it cross-checks the builder's root against the executor's, a real if weak oracle — and a dev chain therefore keeps a root oracle. It must be switchable off for a chain whose headers we cannot reproduce, which is the mainnet case below. + +Defaulting to ON is the safety property: a bin run against foreign headers fails loudly at block 1 rather than silently building a wrong chain, and hex behaviour is untouched. + +This needs no overlay or migration mechanism — verified: dev pins no genesis hash (`execution/chain/spec/genesis.go:141-171`), the dev beacon takes `Eth1Data` from the runtime-computed EL genesis hash (`cmd/utils/flags.go:2243-2250`), and the header root and block-0 exec root come from the same function (`genesiswrite.ComputeGenesisCommitment` → `sd.ComputeCommitment`, `genesis_write.go:468`), so they flip together. + +But dev is not a cheap target. Its alloc (`execution/chain/spec/allocs/dev.json`) has 18 entries, 7 code-bearing, and the deposit contract `0x00000000219ab540...705Fa` is 6358 bytes = 206 chunks = 128 header + **78 CODE_ZONE overflow chunks**. Overflow keys are `key_hash(code_hash ‖ tree_index)`, which cannot be derived from a 20-byte plain key — so the one unavoidable API break lands on day one. The contract cannot be dropped: dev is PoS-from-genesis (`TerminalTotalDifficulty: 0`, `CancunTime: 0`, `DepositContract` set, `genesis.go:157-162`). + +**M1a is a mandatory intermediate gate, not acceptance.** pbin over a real MDBX datadir with no consensus, via `RebuildCommitmentFiles` (`db/state/squeeze.go:876`) or `backtester` (`execution/commitment/backtester/backtester.go:199-215`). It is the only place collation, merge, restart and branch-record round-trip get exercised without consensus noise — but **it has no header-root oracle**. A wrong root there surfaces only as non-determinism between a forward run and a rebuild. Do not mistake a green M1a for a correct engine. + +## Context (from discovery) + +- Repo `/Users/awskii/org/wrk/wt/pbin`, branch `awskii/pbin-patricia`, base `1e078ffb04`. Prior plan: `docs/plans/completed/20260729-pbin-patricia-hashed.md` (M0, complete). +- Spec: `/Users/awskii/org/wrk/EIPs/EIPS/eip-8297.md`. Reference implementation: `ethereum/execution-specs` branch `projects/binary-trie`. +- Engine: `execution/commitment/pbin_*.go` (~6.3k lines incl. tests). +- External oracles already green and to be kept green: `pbin_specroots_test.go` (7 fixed + 600 sequence roots, via the oracle), `pbin_specengine_test.go` (6/7, via the engine), `pbin_specvectors_test.go` (BASIC_DATA + key routing). +- Integration surfaces: `execution/commitment/commitmentdb/commitment_context.go`, `db/state/execctx/{domain_shared,options}.go`, `db/state/{squeeze,erigondb_settings,domain_stream}.go`, `execution/commitment/branch_cache.go`. + +## Development Approach + +- **testing approach**: TDD — the failing test comes first in every task. +- **CRITICAL naming rule** (carried from M0): `package commitment` already declares `cell`, `fold`, `unfold`, `computeCellHash` and more. **Every new package-level identifier MUST carry a `pbin` prefix.** A collision is a compile error, so this applies to every task. +- **The M0 "no external API changes" rule is relaxed, but only for three sanctioned breaks** — Task 7 (option semantics), Task 6 (new persisted toml key), Task 13 (plain-key namespace). Everything else stays additive. If a task appears to need a fourth break, stop and record it with ⚠️ rather than proceeding. +- complete each task fully before the next; small focused changes +- **every task MUST include new/updated tests**, listed as separate checklist items +- **all tests must pass before starting the next task** +- **update this plan file when scope changes during implementation** +- self-contained from a clean git state; no task depends on transient working-tree state + +## Testing Strategy + +- **unit tests**: required per task, table-driven where the input space is enumerable +- **external conformance**: the three `pbin_spec*_test.go` files are the ground truth and must stay green. Task 1 makes the test path run under BLAKE3 — `pbin_specroots_test.go:55` already hard-asserts `meta.hasher == "blake3"`, so they only become meaningful after Task 1. +- **determinism as a proxy oracle** (M1a): forward-run root vs rebuild-from-domains root over the same datadir. Note this proxy is only valid if the answer to Q2 is "pure function of state". +- **structural asserts** where a test cannot cover the failure: variant/cache combinations, monotonic visit order. +- no e2e tests in the erigon sense; the M1b gate is a node smoke run. + +## Progress Tracking + +- mark completed items `[x]` immediately +- add newly discovered tasks with ➕ +- document blockers with ⚠️ +- keep the plan in sync with the work actually done + +## Solution Overview + +Settled decisions — do not revisit during implementation: + +1. **Keccak-256 is the production hash; BLAKE3 is test-only.** Both injection seams (`pbinHasher.sum`, `pbinDigestCache.sum`) keep their Keccak nil-default. The test harness drives the engine under BLAKE3 through **both** seams so the reference vectors mean something. Never describe this as cross-client compatibility, and never as a speedup — BLAKE3 is slower than erigon's `fastkeccak` on arm64 at the 133-byte branch preimage. + + The vector conformance still transfers to the production path: the trie treats keys as opaque bytes, so an algorithm correct for BLAKE3-derived keys is correct for Keccak-derived ones. What does **not** transfer is any claim of agreement with another client. The residual risk is a hash call site that bypasses the injectable seam — caught because the vectors run under BLAKE3 and a hardcoded Keccak site would break them. + +2. **The header state-root check is independently togglable, default ON**, at all five comparison sites: `exec3.go:810`, `exec3.go:730`, `exec3_serial.go:205`, `committer.go:557`, `:659`, `:764`. Follow the existing `common/dbg/experiments.go` `EnvBool` convention (as `DiscardCommitment` does) — one definition site, no CLI plumbing, easy to set in a container. Do **not** gate it on the variant: a self-produced chain keeps the check as an oracle, and only a foreign-header chain needs it off. Note `dbg.DiscardCommitment()` is a different thing — it skips computing the root at all (`exec3.go:788`) — and must not be reused for this. +3. **The CODE_HASH leaf value stays Keccak** (eip:344-347, :578-579) — already correct at `pbin_values.go:68-73`. The empty-tree hash is 32 zero bytes and hash-independent. +4. **Zero-vs-absent is fixed in the engine, not the domain.** The engine holds the presence bit the domain lacks. Domain encoding is untouched. +5. **`code_size` on `Update` is additive, not a break** — verified below. +6. **Overflow code chunks carry their value in the branch record**, and the new plain-key shape is **tag-discriminated, never length-discriminated**. +7. **pbin is a whole-datadir property**, resolved at first start and persisted. No mid-chain activation. +8. **pbin stays `ModeDirect`, sequential.** Parallel/streaming mounting is structurally excluded. + +### Why `code_size` on `Update` is not an external break + +`Update.Encode/Decode` (`commitment.go:2253-2335`) has exactly two production call sites, both in `RecordingContext` (`recording_context.go:72,85`) feeding `BuildTrieTrace` into a debug TOML (`trie_trace.go:36-127`). `Update` is never in an MDBX table, never a domain value, never crosses gRPC. The `ModeDirect` ETL spill carries only `(hashedKey → plainKey)` (`commitment.go:1938-1942`), and the pbin branch record does not serialize `Update` at all. All 141 `Update` composite literals repo-wide are keyed, so a new field compiles everywhere unchanged. The comment at `pbin_hash.go:138-139` claiming otherwise is wrong and must be deleted. + +### The push side is dead for pbin + +The bin variant is hardwired to `ModeDirect` (`commitment.go:165-170`), whose `TouchPlainKey` ignores both `val` and `fn` (`:1668-1672`), and `HashSort` passes `update = nil` (`:1966`). So `Updates.TouchCode` (`:1834-1844`) can **never** deliver code to pbin. Everything comes from the read side, at `TrieContext.Account`. Do not patch `calc_state.go:353-360` expecting code to land. + +## Technical Details + +**Hash injection** — `pbinHasher.hash` (`pbin_hash.go:63-68`) and `pbinDigestCache.hash` (`pbin_keys.go:127-132`) both nil-fallback today. Flipping the fallback costs **0 edits** at the 43 `pbinTreeKeyAccount`/`pbinTreeKeyStorage` call sites and 12 `pbinKeyHasher()` sites; threading a parameter would cost ~45. + +**Root record key** — bit-path keys always end in a byte ≤ 7 (`pbin_bitpath.go:191-193`), so a single-byte sentinel ≥ `0x08` cannot collide. It must also avoid `0x00`, which `pbinEncodeBitPath` produces for the empty path and which the row-0 fold already writes (`pbin_patricia_hashed.go:669`). + +**State blob** — hex writes depths as one byte per row (`hex_patricia_hashed.go:2777-2779`); pbin depths are `[528]int16` (`pbin_cell.go:80`), so a naive port truncates ≥256. Preferred shape is root cell + 3 flags ≈ 160 B, resting on an unproven inference (see Thin/Unverified). The 16-byte `txNum‖blockNum` header stays byte-identical — it is read raw and variant-blind at `commitment_context.go:1140-1150`. + +**Code chunking** (eip:374-397) — pad to a multiple of 31 **before** the pushdata scan; `bytes_to_exec_data` sized `len(padded)+32`; residual pushdata carries **across** chunk boundaries; `byte0 = min(bytes_to_exec_data[pos], 31)`. `MaxCodeSize` 24576 → 793 chunks → 128 header + 665 overflow across 3 CODE_ZONE stems. A 7702 designator is 23 bytes → 1 chunk. + +## Hazard Register + +Each hazard needs a named test or a structural assert. These are the plan's real acceptance criteria. + +| ID | Hazard | Task | Guard | +|----|--------|------|-------| +| H1 | **BranchCache slot collision** — `trunkSlot` returns another node's *well-formed* record; `pbinDecodeBranch` accepts it, the subtree hashes, root is wrong, no error. Deterministic, concentrated at the top of the tree (8 slots for every ≤8-bit path) | 4 | structural assert in the ctor + test that a bin SharedDomains has no shared branch cache | +| H2 | **Root record lost to empty-key iteration truncation** — `loadRoot` treats absent as an empty tree (`pbin_patricia_hashed.go:349-351`); looks like a fresh datadir | 2 | round-trip a stored root through a real domain iteration | +| H3 | **A hash call site bypassing the injectable seam** — with Keccak in production and BLAKE3 only in tests, a site hardcoding either one drifts silently. Also a pooled engine inheriting a stale `hasher.sum` | 1 | full 32-byte key equality in `TestPBinSpecKeyRouting` under BLAKE3 — a hardcoded site breaks the vectors; `Release()` must clear `hasher.sum` | +| H4 | **Variant mismatch across processes** — genesis hex + exec pbin, flagless restart, rpcdaemon defaulting to hex, `integration commitment rebuild` overwriting pbin records | 6, 7 | persisted `trie_variant` + refusal on disagreement | +| H5 | **Backwards visit from the header-chunk fan-out** — `fold` writes with `prevData = nil` and the record replaces its predecessor outright; re-descending a folded row rewrites it with a `touchMap` that no longer names the previously-touched bit | 12 | assert monotonic visit order; test a batch touching a header slot *and* code on one account | +| H6 | **State-blob depth truncation** — `byte(depth)` truncates ≥256; paths reach 528 bits | 5 | restart round-trip with a >256-bit path | +| H7 | **Code key misread as storage** — a 52-byte length-discriminated code key read as `(addr, slot)` | 13 | tag-discriminated by construction + test that a code key never routes to the storage zone | +| H8 | **Stale high code chunks after a shortening redeploy** — header chunks overwrite in place and are never removed, so a forward run keeps residue while a rebuild emits only `ceil(code_size/31)`. Two internally-consistent, different roots. **Breaks recompute-from-domains as an oracle** | 12 | shortening-redeploy test comparing forward-run vs rebuild. See Q2 | +| H9 | **Unconditional `CodeDomain` read promotes tolerated inconsistency to root divergence** — cleared 7702 residue, `eth_simulateV1` overlays. The existing code documents the residue as benign (`commitment_context.go:1054-1057`); PBT removes that license | 11 | decide and test the residue case explicitly | +| H10 | **`ReplacePlainKeys` over pbin records** if references are ever enabled — rewrites bytes at hex cell offsets during background merge. Inert by default, one flag away, no variant check in that path | 6 | refuse the combination | +| H11 | **Overflow-chunk sibling rehash via `CodeStore` by-hash** → cache *miss* (not error) → zero-valued chunk leaf | 13 | avoided entirely by value-in-record | +| H13 | **Root verification switched off leaves nothing validating the node path** — a silently wrong chain looks healthy. Only relevant when the toggle is used, i.e. against foreign headers; a self-produced chain keeps the check | 6 | default ON so it is opt-out not opt-in; loud startup log when off; a bin run against foreign headers without the toggle must fail at block 1, not degrade | +| H12 | **`foldDelete` "enabled" to make a test pass** — collapses nodes the reference leaves in place | 10 | guarded by plan text + a test asserting it stays unreachable from `Process` | + +## Open Questions + +Blocking items needing a human or upstream answer. Do not proceed past the task that depends on one without recording the answer here. + +- **Q1 (highest value) — what does the reference produce for a removed account?** EIP-161 empty-removal and EIP-6780 destruct both hand pbin a `DeleteUpdate` on a committed leaf. Task 9 would turn that into BASIC_DATA `{version 0, code_size 0, nonce 0, balance 0}` + CODE_HASH `keccak(empty)`. Consistent with eip:345-347 but **not verified against the reference**, and `testdata/eip8297_vectors.json` does not cover it. It silently changes the root. +- **Q2 — is the tree a pure function of current state, or of history?** (H8.) eip-8297 is silent. Determines whether recompute-from-domains is a valid oracle at all, hence whether the M1a gate means anything. Possibly an upstream spec question. +- **Q3 — does the reference create a present-zero leaf on `SSTORE 0` to a virgin slot?** Erigon drops it at `execution/state/state_object.go:245-246` before any writer sees it. If yes, that guard must be variant-gated and every zero-SSTORE becomes a state write. +- **Q4 — confirm `github.com/zeebo/blake3`** as a **test-only** dependency (lower stakes now that production stays Keccak; the in-graph `lukechampine.com/blake3` may simply be enough). Its measured advantage was 13–18% arm64 / 58–78% amd64, not re-verified. +- **Q5 — forward/backward compatibility of a new `erigondb.toml` key** across erigon binaries, given the file is downloader-delivered and **wins over the CLI** (`erigondb_settings.go:74-89`). + +## Thin / Unverified + +Do not treat these as established: + +- `pbin_branch.go` record field-bit layout beyond the leaf-kind rejection at `:156-165`. **Verify before designing Task 13's value-in-record field.** +- The "grid arrays are restorable-as-zero" argument underpinning Task 5's ~160-byte blob (`SetState` only runs at `activeRows == 0`; `unfold` initializes each row before any read). Well-argued, not proven. Prove it or pay the ~3.3 KB full-grid blob. +- Whether pbin branch records are truly opaque to the pass-through merge path (believed yes with references off, not exercised). +- Task 8's deferral mis-attribution, inferred from comments at `commitment_context.go:150-157` and `:581-583`; no concrete failing sequence was constructed. + +## What Goes Where + +- **Implementation Steps** (`[ ]`): code, tests and asserts inside this repo +- **Post-Completion** (no checkboxes): the node smoke run, upstream questions, follow-on milestones + +## Implementation Steps + +### Task 1: BLAKE3 as a test-only hash, wired through both seams + +**Files:** +- Modify: `execution/commitment/pbin_patricia_hashed.go` +- Modify: `execution/commitment/pbin_specvectors_test.go` +- Modify: `execution/commitment/pbin_specengine_test.go` +- Modify: `go.mod` + +Production keeps Keccak-256. This task only makes the **test** path run the whole engine — node hashing *and* key derivation — under BLAKE3, so the reference vectors become meaningful for the key-derivation surface too. + +- [ ] write a failing test asserting `TestPBinSpecKeyRouting` compares **full 32-byte tree keys** against `embedding_vectors`, not just zone/length/sub-index — this is what proves no hash site bypasses the seam (guards H3) +- [ ] write a failing test asserting a pooled engine does not inherit a previous `hasher.sum` after `Release()` +- [ ] add `github.com/zeebo/blake3` (test use only; confirm Q4 first) +- [ ] give the engine a way to set BLAKE3 on **both** seams together — `pbinHasher.sum` and the `pbinDigestCache` behind `pbinKeyHasher` — so a half-configured test is impossible +- [ ] clear `hasher.sum` in `Release()` (`pbin_patricia_hashed.go:107-115`) +- [ ] leave the production nil-defaults on Keccak, the CODE_HASH leaf value on Keccak, and the empty-tree hash at 32 zero bytes +- [ ] run tests — the three `pbin_spec*_test.go` files must all pass under BLAKE3 before task 2 + +### Task 2: Root record key sentinel + +**Files:** +- Modify: `execution/commitment/pbin_patricia_hashed.go` +- Create: `execution/commitment/pbin_rootkey_test.go` + +- [ ] write a failing test that stores a root record and reads it back through a real `TblCommitmentVals` iteration, asserting the iteration does not truncate (guards H2) +- [ ] replace `pbinRootKey = []byte{}` with a single-byte sentinel ≥ `0x08` +- [ ] assert the sentinel cannot be produced by `pbinEncodeBitPath` for any bit length 0..528 +- [ ] write a test asserting `loadRoot` distinguishes "no record" from "empty tree" +- [ ] run tests — must pass before task 3 + +### Task 3: No nil values into the domain + +**Files:** +- Modify: `execution/commitment/pbin_patricia_hashed.go` +- Create: `execution/commitment/pbin_domainwrite_test.go` + +- [ ] write a failing test asserting neither `storeRoot` nor `foldDelete` ever hands a nil value to `PutBranch` +- [ ] route the empty-root `storeRoot` path (`:328-337`) and `foldDelete` (`:725-727`) through `DomainDel` or a non-nil zero-length slice +- [ ] pass real `prevData` at both `PutBranch` sites to avoid the extra `GetLatest` per branch write +- [ ] write a test asserting a zero-length branch value round-trips as a deletion +- [ ] run tests — must pass before task 4 + +### Task 4: Disable the shared BranchCache for the bin variant + +**Files:** +- Modify: `execution/commitment/commitmentdb/commitment_context.go` +- Create: `execution/commitment/commitmentdb/pbin_nocache_test.go` + +- [ ] write a failing test asserting a bin-variant `SharedDomains` has no shared branch cache +- [ ] write a failing test demonstrating the `trunkSlot` collision for two distinct ≤8-bit bit-path keys, so the reason is pinned in the suite (guards H1) +- [ ] construct the bin-variant `SharedDomains` with `execctx.WithoutSharedBranchCache()` +- [ ] add a structural assert in the commitment-context ctor that the bin variant never has a shared branch cache — enforce, do not document +- [ ] run tests — must pass before task 5 + +### Task 5: SetState / EncodeCurrentState for pbin, and remove the panic + +**Files:** +- Modify: `execution/commitment/pbin_patricia_hashed.go` +- Create: `execution/commitment/pbin_state.go` +- Modify: `execution/commitment/commitmentdb/commitment_context.go` +- Create: `execution/commitment/pbin_state_test.go` + +- [ ] write a failing restart round-trip test covering a path deeper than 256 bits (guards H6) +- [ ] prove or refute that the grid arrays are restorable-as-zero (`SetState` only at `activeRows == 0`); record the outcome in this plan and pick the ~160 B root-cell blob or the ~3.3 KB full-grid blob accordingly +- [ ] implement `pbin` `SetState`/`EncodeCurrentState` with 2-byte depths, never `byte(depth)` +- [ ] remove the `VariantBinPatriciaTrie` panic (`:225-229`) and fix the hardcoded `variant:` in the struct literal (`:233`) +- [ ] extend the three variant gates: `LatestCommitmentState` (`:806-808`), `encodeCommitmentState` (`:912-913`), `restorePatriciaState` (`:953-955`) +- [ ] promote `StatefulTrie` as an **optional** interface asserted at those 3 sites; do not widen `Trie` +- [ ] write a test asserting the 16-byte `txNum‖blockNum` header is byte-identical to hex's +- [ ] run tests — must pass before task 6 + +### Task 6: The --experimental.bin-commitment flag, persistence, and root-check gating + +**Files:** +- Modify: `db/state/execctx/domain_shared.go` +- Modify: `db/state/erigondb_settings.go` +- Modify: `db/state/squeeze.go` +- Modify: `cmd/utils/flags.go` +- Modify: `node/cli/default_flags.go` +- Modify: `node/ethconfig/config.go` +- Modify: `node/eth/backend.go` +- Modify: `cmd/integration/commands/flags.go` +- Create: `db/state/pbin_variant_persist_test.go` + +- [ ] write a failing test asserting a datadir created with the bin variant is **refused** when opened with a conflicting config (guards H4) +- [ ] write a failing test asserting `references_in_commitment_branches = true` is refused under the bin variant (guards H10) +- [ ] add a `statecfg` global for the variant +- [ ] add the `--experimental.bin-commitment` flag across the 7-site experimental-commitment template +- [ ] replace the duplicated inline switch at `squeeze.go:1023-1029` with `PickTrieVariant()` +- [ ] add `trie_variant` to `ErigonDBSettings`, resolved first-start exactly as `ReferencesInCommitmentBranches` is, and note in a comment that `erigondb.toml` wins over the CLI +- [ ] write a failing test asserting the header state-root comparison is enforced by default and skipped only when the new toggle is set — under **both** variants, since the toggle is variant-independent +- [ ] add the third case to `PickTrieVariant()` reachable via `--experimental.bin-commitment` +- [ ] add a root-check toggle to `common/dbg/experiments.go` following the `DiscardCommitment` `EnvBool` pattern, **defaulting to check-enabled**, and honour it at all five sites: `exec3.go:810`, `exec3.go:730`, `exec3_serial.go:205`, `committer.go:557`, `:659`, `:764` +- [ ] log loudly once at startup when the check is disabled, so a running node says so out loud (guards H13) +- [ ] write a test asserting a flagless restart of a bin datadir stays bin +- [ ] run tests — must pass before task 7 + +### Task 7: Un-pin the genesis variant + +**Files:** +- Modify: `db/state/execctx/options.go` +- Modify: `execution/state/genesiswrite/genesis_write.go` +- Modify: `rpc/rpchelper/commitment.go` +- Create: `db/state/execctx/pbin_options_test.go` + +- [ ] write a failing test asserting genesis under the bin variant computes a **binary** root, not a hex one +- [ ] add `WithoutParallelCommitment()` that demotes streaming/parallel to hex and leaves bin as bin; keep `WithSequentialCommitment()` as a deprecated alias or migrate all 11 call sites +- [ ] switch `genesis_write.go:381` to the new option +- [ ] make the 10 RPC/integrity sites return an explicit unsupported-variant error rather than silently forcing hex over pbin records +- [ ] write a test asserting each of those paths errors under bin instead of returning a hex root +- [ ] run tests — must pass before task 8 + +### Task 8: Make the silent degradations loud + +**Files:** +- Modify: `execution/commitment/commitmentdb/commitment_context.go` +- Modify: `execution/stagedsync/exec3.go` +- Create: `execution/commitment/commitmentdb/pbin_unsupported_test.go` + +- [ ] write a failing test asserting `SetLeaveDeferredForCaller` and the deferred-update take reject the bin variant instead of silently no-opping +- [ ] reject the bin variant explicitly where `exec3.go:206-210` enables deferral for fork validation and the parallel apply path +- [ ] make `SetCollapseTracer` (`:415-420`), `BranchChildCount` (`:424-431`) and trace-state capture (`:501-506`) error under bin rather than degrade +- [ ] write tests asserting each of the four paths errors under bin +- [ ] run tests — must pass before task 9 + +### Task 9: Zero-vs-absent in the engine + +**Files:** +- Modify: `execution/commitment/pbin_patricia_hashed.go` +- Create: `execution/commitment/pbin_zerovalue_test.go` + +- [ ] write a failing test asserting a `DeleteUpdate` on an existing **storage** leaf writes 32 zero bytes and keeps the leaf, matching the reference `zero_value_present` root — this part does **not** depend on Q1 and is what a dev chain actually needs +- [ ] reinterpret `DeleteUpdate` for storage at the three reject sites — `updateCell` (`:257-263`) and both `loadCellState` arms (`:786-791`, `:797-803`) +- [ ] for the **account-removal** encoding only: record the answer to Q1 in this plan first; if unanswered, mark ⚠️, leave account removal rejecting, and continue — a dev chain reaches neither EIP-161 clearing nor the EIP-6780 pre-funded-CREATE2 case +- [ ] leave the domain encoding and the three zero-write `DomainDel` sites untouched +- [ ] write a test asserting `foldDelete` remains unreachable from `Process` (guards H12) +- [ ] run tests — must pass before task 10 + +### Task 10: M1a gate — pbin over a real datadir + +**Files:** +- Create: `execution/commitment/backtester/pbin_m1a_test.go` + +- [ ] write a test driving pbin over a real MDBX datadir via `RebuildCommitmentFiles` or the backtester, with no consensus +- [ ] assert the forward-run root equals the rebuild-from-domains root over the same input +- [ ] assert a restart mid-run resumes to the same root (exercises Task 5) +- [ ] assert collation and merge preserve branch records byte-for-byte +- [ ] record in this plan that M1a has **no header-root oracle** and is not acceptance +- [ ] run tests — must pass before task 11 + +### Task 11: code_size on Update + +**Files:** +- Modify: `execution/commitment/commitment.go` +- Modify: `execution/commitment/commitmentdb/commitment_context.go` +- Modify: `execution/commitment/pbin_hash.go` +- Create: `execution/commitment/pbin_codesize_test.go` + +- [ ] write a failing test asserting BASIC_DATA for a code-bearing account carries the real `code_size`, checked against `basic_data_vectors` +- [ ] add the `code_size` field to `Update` plus handling in `Reset`/`Copy`/`Merge`/`Encode`/`Decode`/`String` +- [ ] populate it at `TrieContext.Account` (`:1026-1070`) by reading `kv.CodeDomain` unconditionally +- [ ] delete the wrong comment at `pbin_hash.go:138-139` and pass the real size instead of `0` +- [ ] decide and test the cleared-7702-residue case explicitly — the existing benign-residue license no longer holds (guards H9) +- [ ] write a test asserting the push side is inert for pbin, so nobody patches `calc_state.go` expecting code to arrive +- [ ] run tests — must pass before task 12 + +### Task 12: chunkify_code and header code chunks + +**Files:** +- Create: `execution/commitment/pbin_code.go` +- Modify: `execution/commitment/pbin_keys.go` +- Modify: `execution/commitment/pbin_hash.go` +- Modify: `execution/commitment/pbin_patricia_hashed.go` +- Create: `execution/commitment/pbin_code_test.go` + +- [ ] write failing tests for `pbinChunkifyCode` against `chunkify_vectors`, covering pushdata straddling a chunk boundary and a 7702 designator +- [ ] write a failing test for a batch touching both a header storage slot and code on one account, asserting monotonic visit order (guards H5) +- [ ] write a failing shortening-redeploy test comparing forward-run and rebuild roots (guards H8); if they differ, record Q2's answer before proceeding +- [ ] implement `pbinChunkifyCode` per eip:374-397 exactly — pad to 31 before the scan, carry residual pushdata across boundaries +- [ ] add `pbinCodeZone` and make the zone explicit at the three places a code key currently passes by accident (`pbin_keys.go:62-66`, `pbin_hash.go:132-148`, `pbin_hash.go:117-119`) +- [ ] emit header chunks 0..127 with a stem-exit flush or as their own sorted stream keys, never mid-fan-out +- [ ] run tests — must pass before task 13 + +### Task 13: CODE_ZONE overflow chunks + +**Files:** +- Modify: `execution/commitment/pbin_keys.go` +- Modify: `execution/commitment/pbin_branch.go` +- Modify: `execution/commitment/pbin_patricia_hashed.go` +- Modify: `execution/commitment/pbin_specengine_test.go` +- Create: `execution/commitment/pbin_overflow_test.go` + +- [ ] verify the `pbin_branch.go` record field-bit layout before designing the new field; record the layout in this plan +- [ ] write a failing test asserting `full_header_stem` reproduces through the **engine**, and empty the asserted exclusion list in `pbin_specengine_test.go` +- [ ] write a failing test asserting a code key never routes to the storage zone (guards H7) +- [ ] add a tag-discriminated third plain-key shape recognised by `pbinKeyHasher` and `updateCell`; never discriminate by length +- [ ] add a `pbinCellFields` bit carrying the 32-byte chunk value in the branch record, so no reverse lookup is needed (guards H11) +- [ ] extend `pbinDecodeCell` and `loadCellState` for the new shape +- [ ] run tests — all 7 engine vectors must pass before task 14 + +### Task 14: M1b gate — --chain=dev from genesis + +**Files:** +- Create: `docs/pbin-m1b-smoke.md` + +- [ ] verify genesis block 0 computes a binary root and the dev beacon accepts it +- [ ] run a local `--chain=dev` node to a few blocks, deploying and calling a contract +- [ ] verify a restart resumes at the same root +- [ ] record the observed genesis root and block roots in `docs/pbin-m1b-smoke.md` with the exact command line +- [ ] verify `integration commitment rebuild` on the resulting datadir reproduces the same roots +- [ ] run the package suite — must pass before task 15 + +### Task 15: Verify acceptance criteria + +- [ ] verify every hazard H1–H12 has a named passing test or a structural assert +- [ ] verify all five open questions are answered and recorded, or explicitly deferred with ⚠️ and a reason +- [ ] verify only the three sanctioned API breaks were taken; `git diff --stat` shows no fourth +- [ ] verify every new package-level identifier carries the `pbin` prefix +- [ ] run `go test ./execution/commitment/... ./db/state/... -count=1` +- [ ] run `go build ./...` and `make lint` until clean +- [ ] verify the three `pbin_spec*_test.go` oracles pass under BLAKE3 with 7/7 engine vectors + +### Task 16: [Final] Update documentation + +- [ ] update the package doc comment on `pbin_patricia_hashed.go` to state BLAKE3, the M1 scope, and the stated limitations (no witness, no getProof, no parallel) +- [ ] update `CLAUDE.md` if new patterns were discovered +- [ ] move this plan to `docs/plans/completed/` + +## Post-Completion + +*Manual, external, or follow-on — no checkboxes* + +**Upstream questions to raise:** +- Q1 (removed-account encoding) and Q2 (pure function of state vs history) are plausibly spec questions for EIP-8297, not just implementation ones. Q2 in particular determines whether recompute-from-domains is a legitimate oracle for any client. +- A root vector with a code-bearing account is absent from the exported reference vectors (all 8 BASIC_DATA leaves have `code_size = 0`, no zone `0x01` keys). EELS may already have one in its own suite; check before offering. + +**Deliberately out of M1:** +- Access events / witness gas (EIP-4762 recalibration). Zero repo hits, and the EIP says `WITNESS_BRANCH_COST` is not yet fixed. The state root is computable without it. +- Cross-client devnet and EEST fixtures **as acceptance**. BLAKE3 buys reference-vector comparability and a geth `--chain=dev` diff, not consensus parity, because the gas rules do not exist. Debugging tool only. +- State expiry; parallel/streaming mounting for pbin (structurally excluded — `ModeParallel`'s prefix trie is nibble-based); witness / `eth_getProof` / `eth_simulateV1` / receipt regeneration under pbin (Task 8 makes them error — a stated limitation); mid-chain fork activation (no precedent, no state-format field in `chain.Config`, and a mid-chain switch would straddle step `.kv` files with no discriminator); referenced/squeezed commitment branches; real node deletion. + +**Publishing:** +- `ethpandaops/eth-client-docker-image-builder` issue #398 tracks binary-trie branches per client; Erigon is unchecked. Its convention is a branch named literally `binary-trie`. An image is only worth building once Task 14 passes — before that it would produce a node that cannot sync. Nothing to publish from M0. From 1b352e10ea6a750d245092bf2e6128ecdb152da5 Mon Sep 17 00:00:00 2001 From: awskii Date: Thu, 30 Jul 2026 13:23:29 +0700 Subject: [PATCH 24/56] execution/commitment: replay pbin key derivation under BLAKE3 through both hash seams --- docs/plans/20260730-pbin-m1-local-el.md | 16 ++++---- execution/commitment/pbin_keys.go | 8 +++- execution/commitment/pbin_patricia_hashed.go | 9 +++++ execution/commitment/pbin_specengine_test.go | 18 ++++++++- execution/commitment/pbin_specroots_test.go | 5 +++ execution/commitment/pbin_specvectors_test.go | 39 +++++++------------ 6 files changed, 60 insertions(+), 35 deletions(-) diff --git a/docs/plans/20260730-pbin-m1-local-el.md b/docs/plans/20260730-pbin-m1-local-el.md index bd278bd80e8..74bdfcf31c7 100644 --- a/docs/plans/20260730-pbin-m1-local-el.md +++ b/docs/plans/20260730-pbin-m1-local-el.md @@ -114,7 +114,7 @@ Blocking items needing a human or upstream answer. Do not proceed past the task - **Q1 (highest value) — what does the reference produce for a removed account?** EIP-161 empty-removal and EIP-6780 destruct both hand pbin a `DeleteUpdate` on a committed leaf. Task 9 would turn that into BASIC_DATA `{version 0, code_size 0, nonce 0, balance 0}` + CODE_HASH `keccak(empty)`. Consistent with eip:345-347 but **not verified against the reference**, and `testdata/eip8297_vectors.json` does not cover it. It silently changes the root. - **Q2 — is the tree a pure function of current state, or of history?** (H8.) eip-8297 is silent. Determines whether recompute-from-domains is a valid oracle at all, hence whether the M1a gate means anything. Possibly an upstream spec question. - **Q3 — does the reference create a present-zero leaf on `SSTORE 0` to a virgin slot?** Erigon drops it at `execution/state/state_object.go:245-246` before any writer sees it. If yes, that guard must be variant-gated and every zero-SSTORE becomes a state write. -- **Q4 — confirm `github.com/zeebo/blake3`** as a **test-only** dependency (lower stakes now that production stays Keccak; the in-graph `lukechampine.com/blake3` may simply be enough). Its measured advantage was 13–18% arm64 / 58–78% amd64, not re-verified. +- **Q4 — confirm `github.com/zeebo/blake3`** as a **test-only** dependency (lower stakes now that production stays Keccak; the in-graph `lukechampine.com/blake3` may simply be enough). Its measured advantage was 13–18% arm64 / 58–78% amd64, not re-verified. **Answered (Task 1):** `zeebo` not added — production stays Keccak so BLAKE3 speed is irrelevant, and `lukechampine.com/blake3` is already a direct dependency used by `pbin_specroots_test.go`; it now backs `pbinBlake3Hash` for all three seam tests. - **Q5 — forward/backward compatibility of a new `erigondb.toml` key** across erigon binaries, given the file is downloader-delivered and **wins over the CLI** (`erigondb_settings.go:74-89`). ## Thin / Unverified @@ -143,13 +143,13 @@ Do not treat these as established: Production keeps Keccak-256. This task only makes the **test** path run the whole engine — node hashing *and* key derivation — under BLAKE3, so the reference vectors become meaningful for the key-derivation surface too. -- [ ] write a failing test asserting `TestPBinSpecKeyRouting` compares **full 32-byte tree keys** against `embedding_vectors`, not just zone/length/sub-index — this is what proves no hash site bypasses the seam (guards H3) -- [ ] write a failing test asserting a pooled engine does not inherit a previous `hasher.sum` after `Release()` -- [ ] add `github.com/zeebo/blake3` (test use only; confirm Q4 first) -- [ ] give the engine a way to set BLAKE3 on **both** seams together — `pbinHasher.sum` and the `pbinDigestCache` behind `pbinKeyHasher` — so a half-configured test is impossible -- [ ] clear `hasher.sum` in `Release()` (`pbin_patricia_hashed.go:107-115`) -- [ ] leave the production nil-defaults on Keccak, the CODE_HASH leaf value on Keccak, and the empty-tree hash at 32 zero bytes -- [ ] run tests — the three `pbin_spec*_test.go` files must all pass under BLAKE3 before task 2 +- [x] write a failing test asserting `TestPBinSpecKeyRouting` compares **full 32-byte tree keys** against `embedding_vectors`, not just zone/length/sub-index — this is what proves no hash site bypasses the seam (guards H3) — full-key equality passes: derivation matches the reference under BLAKE3 +- [x] write a failing test asserting a pooled engine does not inherit a previous `hasher.sum` after `Release()` — `TestPBinReleaseClearsHashSuite`, red before the fix +- [x] add `github.com/zeebo/blake3` (test use only; confirm Q4 first) — Q4 answered: not added, in-graph `lukechampine.com/blake3` suffices (see Open Questions) +- [x] give the engine a way to set BLAKE3 on **both** seams together — `pbinHasher.sum` and the `pbinDigestCache` behind `pbinKeyHasher` — so a half-configured test is impossible — `setHashSuite(sum)` sets the node seam and returns a matching `pbinKeyHasherWith(sum)` +- [x] clear `hasher.sum` in `Release()` (`pbin_patricia_hashed.go:107-115`) +- [x] leave the production nil-defaults on Keccak, the CODE_HASH leaf value on Keccak, and the empty-tree hash at 32 zero bytes +- [x] run tests — the three `pbin_spec*_test.go` files must all pass under BLAKE3 before task 2 ### Task 2: Root record key sentinel diff --git a/execution/commitment/pbin_keys.go b/execution/commitment/pbin_keys.go index f5e691ebbe9..27fbaacc52f 100644 --- a/execution/commitment/pbin_keys.go +++ b/execution/commitment/pbin_keys.go @@ -92,12 +92,16 @@ func pbinTreeKeyStorage(addr, slot []byte) []byte { // copies the hasher value, so a captured cache would be written by two buffers // hashing concurrently. Every hit is validated against the address it was built // from, so borrowing another goroutine's cache stays correct. -func pbinKeyHasher() keyHasher { +func pbinKeyHasher() keyHasher { return pbinKeyHasherWith(nil) } + +// pbinKeyHasherWith derives keys under sum, nil meaning Keccak-256. Tests swap +// the hash here and on node hashing together through setHashSuite. +func pbinKeyHasherWith(sum pbinHashFn) keyHasher { var pool sync.Pool return func(plainKey []byte) []byte { c, _ := pool.Get().(*pbinDigestCache) if c == nil { - c = new(pbinDigestCache) + c = &pbinDigestCache{sum: sum} } key := c.treeKey(plainKey) pool.Put(c) diff --git a/execution/commitment/pbin_patricia_hashed.go b/execution/commitment/pbin_patricia_hashed.go index bd6de284289..d9cf68d6fba 100644 --- a/execution/commitment/pbin_patricia_hashed.go +++ b/execution/commitment/pbin_patricia_hashed.go @@ -103,11 +103,20 @@ func (pph *PBinPatriciaHashed) Reset() { pph.rootChecked, pph.rootTouched, pph.rootPresent = false, false, false } +// setHashSuite swaps H on both seams at once — node hashing on this engine and +// the returned key-derivation hasher — so neither can be configured without the +// other. Production never calls it: the nil default is Keccak-256 on both. +func (pph *PBinPatriciaHashed) setHashSuite(sum pbinHashFn) keyHasher { + pph.hasher.sum = sum + return pbinKeyHasherWith(sum) +} + // Release returns the engine to the pool. The caller must not use it afterwards. func (pph *PBinPatriciaHashed) Release() { pph.Reset() pph.ctx = nil pph.traceW = nil + pph.hasher.sum = nil pph.counters = pbinCounters{} pph.branchEncoder.buf = pph.branchEncoder.buf[:0] pbinPool.Put(pph) diff --git a/execution/commitment/pbin_specengine_test.go b/execution/commitment/pbin_specengine_test.go index e4854016eea..3e579c0cebe 100644 --- a/execution/commitment/pbin_specengine_test.go +++ b/execution/commitment/pbin_specengine_test.go @@ -111,7 +111,7 @@ func TestPBinEngineMatchesSpecTrieRoots(t *testing.T) { ms := NewMockState(t) pph := NewPBinPatriciaHashed(ms) - pph.hasher.sum = func(b []byte) common.Hash { return common.Hash(blake3Sum(b)) } + pph.setHashSuite(pbinBlake3Hash) for i := range leaves { require.NoError(t, pph.followAndUpdate(leaves[i].treeKey, leaves[i].plainKey, &leaves[i].update), @@ -133,3 +133,19 @@ func TestPBinEngineMatchesSpecTrieRoots(t *testing.T) { require.Equal(t, []string{"full_header_stem"}, excluded, "exclusions must stay pinned: gaining code support should widen this list, not hide it") } + +// TestPBinReleaseClearsHashSuite pins pooling hygiene: a released engine must +// come back on the Keccak default, not carrying a previous user's BLAKE3. +// Not parallel — it inspects a pooled object. +func TestPBinReleaseClearsHashSuite(t *testing.T) { + pph := NewPBinPatriciaHashed(NewMockState(t)) + pph.setHashSuite(pbinBlake3Hash) + require.NotNil(t, pph.hasher.sum) + + pph.Release() + require.Nil(t, pph.hasher.sum, "Release must drop the hash override before pooling") + + reused := NewPBinPatriciaHashed(NewMockState(t)) + require.Nil(t, reused.hasher.sum, "a pooled engine must start on the Keccak default") + reused.Release() +} diff --git a/execution/commitment/pbin_specroots_test.go b/execution/commitment/pbin_specroots_test.go index b8e0b06bc48..b71cf10c7c9 100644 --- a/execution/commitment/pbin_specroots_test.go +++ b/execution/commitment/pbin_specroots_test.go @@ -9,6 +9,8 @@ import ( "github.com/stretchr/testify/require" "lukechampine.com/blake3" + + "github.com/erigontech/erigon/common" ) // Root vectors exported from the EIP-8297 reference implementation in @@ -46,6 +48,9 @@ type pbinRootVectors struct { func blake3Sum(b []byte) [32]byte { return blake3.Sum256(b) } +// pbinBlake3Hash adapts blake3Sum to the engine's injectable hash seam. +var pbinBlake3Hash pbinHashFn = func(b []byte) common.Hash { return common.Hash(blake3.Sum256(b)) } + func loadPBinRootVectors(t *testing.T) pbinRootVectors { t.Helper() raw, err := os.ReadFile("testdata/eip8297_vectors.json") diff --git a/execution/commitment/pbin_specvectors_test.go b/execution/commitment/pbin_specvectors_test.go index 1706b123e2b..0c944966c73 100644 --- a/execution/commitment/pbin_specvectors_test.go +++ b/execution/commitment/pbin_specvectors_test.go @@ -11,12 +11,10 @@ import ( ) // Vectors exported from the EIP-8297 reference implementation in -// ethereum/execution-specs (branch projects/binary-trie). The reference hashes -// with BLAKE3 and this engine with Keccak-256, so every digest-bearing vector — -// the trie roots and the tree-key bodies — cannot be compared directly. What -// survives the hash difference is checked here: the BASIC_DATA packing, which -// involves no hash at all, and the zone/length/sub-index routing, which is -// positional. +// ethereum/execution-specs (branch projects/binary-trie), which hashes with +// BLAKE3. The BASIC_DATA packing involves no hash and is compared as-is; key +// derivation is replayed under BLAKE3 through the injectable seam, so the +// tree-key bodies compare in full. type pbinSpecVectors struct { Meta map[string]string `json:"meta"` BasicData []struct { @@ -71,36 +69,29 @@ func TestPBinSpecBasicDataVectors(t *testing.T) { } } -// TestPBinSpecKeyRouting checks the positional half of key derivation against -// the reference: which zone a key lands in, how long it is, and which sub-index -// it carries. The 32-byte digest bodies differ by hash and are not compared. +// TestPBinSpecKeyRouting compares full tree keys against the reference under +// the BLAKE3 the vectors were generated with — zone, digest bodies and +// sub-index alike. Full equality through the production keyHasher seam is what +// proves no derivation step hashes outside it: a hardcoded Keccak site would +// diverge here (guards H3). func TestPBinSpecKeyRouting(t *testing.T) { t.Parallel() v := loadPBinSpecVectors(t) addr := mustHex(t, v.Embedding.Address) require.Len(t, addr, 20) - var c pbinDigestCache + hasher := pbinKeyHasherWith(pbinBlake3Hash) + require.Equal(t, mustHex(t, v.Embedding.BasicDataKey), hasher(addr), "BASIC_DATA key") - header := mustHex(t, v.Embedding.BasicDataKey) - got := c.accountKey(addr, pbinBasicDataLeafKey) - require.Len(t, got, len(header)) - require.Equal(t, header[0], got[0], "account zone byte") - require.Equal(t, header[len(header)-1], got[len(got)-1], "BASIC_DATA sub-index") - - codeHash := mustHex(t, v.Embedding.CodeHashKey) - got = c.accountKey(addr, pbinCodeHashLeafKey) - require.Equal(t, codeHash[len(codeHash)-1], got[len(got)-1], "CODE_HASH sub-index") + c := pbinDigestCache{sum: pbinBlake3Hash} + require.Equal(t, mustHex(t, v.Embedding.CodeHashKey), c.accountKey(addr, pbinCodeHashLeafKey), "CODE_HASH key") for _, s := range v.Embedding.Slots { slot, err := uint256.FromDecimal(s.Slot.String()) require.NoError(t, err, "slot %s", s.Slot) - want := mustHex(t, s.Key) slotBytes := slot.Bytes32() - got := c.storageKey(addr, slotBytes[:]) - require.Len(t, got, len(want), "slot %s key length", s.Slot) - require.Equal(t, want[0], got[0], "slot %s zone byte", s.Slot) - require.Equal(t, want[len(want)-1], got[len(got)-1], "slot %s sub-index", s.Slot) + plainKey := append(append(make([]byte, 0, len(addr)+len(slotBytes)), addr...), slotBytes[:]...) + require.Equal(t, mustHex(t, s.Key), hasher(plainKey), "slot %s key", s.Slot) } } From 92e219dca0abd44555fc28f5d87bbb24d5b17177 Mon Sep 17 00:00:00 2001 From: awskii Date: Thu, 30 Jul 2026 13:32:10 +0700 Subject: [PATCH 25/56] execution/commitment: move the pbin root record off the empty key MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Domain iteration treats a zero-length key as end-of-stream, and the empty key sorts first in TblCommitmentVals — so a root record stored under it truncated the whole iteration and the datadir read back as fresh (H2). Replace the empty root key with a single-byte 0x08 sentinel, which no pbinAppendBitPath encoding can produce: every bit-path key ends in a trailing bit-count byte <= 7. --- docs/plans/20260730-pbin-m1-local-el.md | 10 +- execution/commitment/pbin_patricia_hashed.go | 13 +- execution/commitment/pbin_rootkey_test.go | 126 +++++++++++++++++++ 3 files changed, 138 insertions(+), 11 deletions(-) create mode 100644 execution/commitment/pbin_rootkey_test.go diff --git a/docs/plans/20260730-pbin-m1-local-el.md b/docs/plans/20260730-pbin-m1-local-el.md index 74bdfcf31c7..bfe100084db 100644 --- a/docs/plans/20260730-pbin-m1-local-el.md +++ b/docs/plans/20260730-pbin-m1-local-el.md @@ -157,11 +157,11 @@ Production keeps Keccak-256. This task only makes the **test** path run the whol - Modify: `execution/commitment/pbin_patricia_hashed.go` - Create: `execution/commitment/pbin_rootkey_test.go` -- [ ] write a failing test that stores a root record and reads it back through a real `TblCommitmentVals` iteration, asserting the iteration does not truncate (guards H2) -- [ ] replace `pbinRootKey = []byte{}` with a single-byte sentinel ≥ `0x08` -- [ ] assert the sentinel cannot be produced by `pbinEncodeBitPath` for any bit length 0..528 -- [ ] write a test asserting `loadRoot` distinguishes "no record" from "empty tree" -- [ ] run tests — must pass before task 3 +- [x] write a failing test that stores a root record and reads it back through a real `TblCommitmentVals` iteration, asserting the iteration does not truncate (guards H2) — `TestPBinRootRecordRealTableIteration`, red before the fix: MDBX accepts the empty-key Put but hands the key back zero-length mid-iteration, which `domain_stream.go:343,577` reads as end-of-stream +- [x] replace `pbinRootKey = []byte{}` with a single-byte sentinel ≥ `0x08` — `0x08` +- [x] assert the sentinel cannot be produced by `pbinEncodeBitPath` for any bit length 0..528 — `TestPBinRootKeySentinelNotABitPath`, plus `pbinDecodeBitPath` rejecting the sentinel outright +- [x] write a test asserting `loadRoot` distinguishes "no record" from "empty tree" — `TestPBinLoadRootNoRecordVersusStoredTree`: no record reads as the empty tree with `rootPresent == false`; a stored record reproduces the stored root +- [x] run tests — must pass before task 3 ### Task 3: No nil values into the domain diff --git a/execution/commitment/pbin_patricia_hashed.go b/execution/commitment/pbin_patricia_hashed.go index d9cf68d6fba..c429ff3d55a 100644 --- a/execution/commitment/pbin_patricia_hashed.go +++ b/execution/commitment/pbin_patricia_hashed.go @@ -127,12 +127,13 @@ var ( errPBinDeleteUnsupported = errors.New("pbin: EIP-8297 defines no deletion") ) -// pbinRootKey names the record holding the root cell. It is the empty key, which -// pbinAppendBitPath never produces — every encoded path carries at least the -// trailing bit-count byte — so it cannot collide with a node record. The root is -// the one node no descent can name: every other node is found by the path that -// reaches it, while the root's own prefix is stored nowhere else. -var pbinRootKey = []byte{} +// pbinRootKey names the record holding the root cell — the one node no descent +// can name: every other node is found by the path that reaches it, while the +// root's own prefix is stored nowhere else. The sentinel cannot collide with a +// node record: every pbinAppendBitPath key ends in a trailing bit-count byte +// ≤ 7. The empty key would not do — domain iteration reads a zero-length key as +// end-of-stream, and the empty key sorts first, truncating the whole table. +var pbinRootKey = []byte{0x08} // Process folds the update stream into the tree and returns the new root. // HashSort hands keys over in tree-key order, which is descent order, so the diff --git a/execution/commitment/pbin_rootkey_test.go b/execution/commitment/pbin_rootkey_test.go new file mode 100644 index 00000000000..dd36f85bd8e --- /dev/null +++ b/execution/commitment/pbin_rootkey_test.go @@ -0,0 +1,126 @@ +// Copyright 2026 The Erigon Authors +// This file is part of Erigon. +// +// Erigon is free software: you can redistribute it and/or modify +// it under the terms of the GNU Lesser General Public License as published by +// the Free Software Foundation, either version 3 of the License, or +// (at your option) any later version. +// +// Erigon is distributed in the hope that it will be useful, +// but WITHOUT ANY WARRANTY; without even the implied warranty of +// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +// GNU Lesser General Public License for more details. +// +// You should have received a copy of the GNU Lesser General Public License +// along with Erigon. If not, see . + +package commitment + +import ( + "bytes" + "testing" + + "github.com/stretchr/testify/require" + + "github.com/erigontech/erigon/common/length" + "github.com/erigontech/erigon/db/kv" + "github.com/erigontech/erigon/db/kv/dbcfg" + "github.com/erigontech/erigon/db/kv/memdb" +) + +// pbinTestStoredTree runs a small corpus through the engine and returns the +// backing state with every record the run persisted, plus the root it computed. +func pbinTestStoredTree(t *testing.T) (*MockState, []byte) { + t.Helper() + corpus := new(pbinTestCorpus). + storage(pbinOracleAddr(1), pbinOracleSlot(64), 0x01). + storage(pbinOracleAddr(1), pbinOracleSlot(1000), 0x02, 0x03) + pph, ms := pbinTestEngine(t) + require.NoError(t, ms.applyPlainUpdates(corpus.plainKeys, corpus.updates)) + return ms, pbinTestProcess(t, pph, corpus.plainKeys, corpus.updates) +} + +// TestPBinRootRecordRealTableIteration guards H2: every record a Process run +// writes, the root record included, must survive a round-trip through the real +// TblCommitmentVals table. Domain iteration treats a zero-length key as +// end-of-stream, and the empty key sorts first — a root record stored under it +// truncates the whole iteration and the datadir reads back as fresh. +func TestPBinRootRecordRealTableIteration(t *testing.T) { + t.Parallel() + + ms, _ := pbinTestStoredTree(t) + rootRecord := bytes.Clone(ms.cm[string(pbinRootKey)]) + require.NotEmpty(t, rootRecord) + + db := memdb.NewTestDB(t, dbcfg.ChainDB) + tx := memdb.BeginRw(t, db) + for key, record := range ms.cm { + require.NoError(t, tx.Put(kv.TblCommitmentVals, []byte(key), record)) + } + + cursor, err := tx.Cursor(kv.TblCommitmentVals) + require.NoError(t, err) + defer cursor.Close() + + var gotRoot []byte + seen := 0 + for k, v, err := cursor.First(); k != nil; k, v, err = cursor.Next() { + require.NoError(t, err) + require.NotEmpty(t, k, "a zero-length key reads as end-of-stream in domain iteration") + if bytes.Equal(k, pbinRootKey) { + gotRoot = bytes.Clone(v) + } + seen++ + } + require.Equal(t, len(ms.cm), seen, "iteration truncated: not every stored record came back") + require.Equal(t, rootRecord, gotRoot, "root record lost or damaged by the table round-trip") +} + +// TestPBinRootKeySentinelNotABitPath pins the root key to a shape no bit-path +// key can take. Every pbinAppendBitPath encoding ends in a trailing bit-count +// byte ≤ 7, so a single byte ≥ 0x08 cannot collide with any encoded path, and +// pbinDecodeBitPath must reject it outright. +func TestPBinRootKeySentinelNotABitPath(t *testing.T) { + t.Parallel() + + require.Len(t, pbinRootKey, 1) + require.GreaterOrEqual(t, pbinRootKey[0], byte(0x08)) + + _, err := pbinDecodeBitPath(pbinRootKey) + require.Error(t, err, "a canonical bit-path key must never spell the root key") + + for bitLen := int16(0); bitLen <= pbinMaxPathBits; bitLen++ { + for _, fill := range []byte{0x00, 0xFF} { + path := pbinPathFromBits(bytes.Repeat([]byte{fill}, (int(bitLen)+7)/8), bitLen) + encoded := pbinEncodeBitPath(&path) + require.NotEqual(t, pbinRootKey, encoded, "bit length %d fill %#x collides with the root key", bitLen, fill) + require.LessOrEqual(t, encoded[len(encoded)-1], byte(0x07)) + } + } +} + +// TestPBinLoadRootNoRecordVersusStoredTree asserts loadRoot tells a fresh +// datadir from a persisted tree: no record reads back as the empty tree, while +// a stored record must reproduce the stored root, never a fresh one. +func TestPBinLoadRootNoRecordVersusStoredTree(t *testing.T) { + t.Parallel() + + pph, _ := pbinTestEngine(t) + require.NoError(t, pph.loadRoot()) + require.True(t, pph.rootChecked) + require.False(t, pph.rootPresent) + require.Equal(t, pbinNodeEmpty, pph.grid.root.kind) + root, err := pph.RootHash() + require.NoError(t, err) + require.Equal(t, make([]byte, length.Hash), root) + + ms, storedRoot := pbinTestStoredTree(t) + fresh := NewPBinPatriciaHashed(ms) + require.NoError(t, fresh.loadRoot()) + require.True(t, fresh.rootPresent) + require.NotEqual(t, pbinNodeEmpty, fresh.grid.root.kind) + reloaded, err := fresh.RootHash() + require.NoError(t, err) + require.Equal(t, storedRoot, reloaded) + require.NotEqual(t, make([]byte, length.Hash), reloaded) +} From b279f407a98e9c5747562e1af16559d27b31beda Mon Sep 17 00:00:00 2001 From: awskii Date: Thu, 30 Jul 2026 13:45:58 +0700 Subject: [PATCH 26/56] execution/commitment: write pbin branch deletions as zero-length values with real prevData The domain layer refuses a nil value in DomainPut, so the empty-root storeRoot path and foldDelete would fail the first time the engine runs over a real datadir. Zero-length is the deletion encoding at that boundary (TemporalMemBatch routes any len==0 write to DeleteWithPrev), so both sites now hand over a non-nil empty slice. Every PutBranch also carries the record it replaces: the grid retains each row's record bytes at unfold and the engine retains the root record across load/store, sparing the domain a GetLatest per branch write and keeping history rows accurate. --- docs/plans/20260730-pbin-m1-local-el.md | 10 +- execution/commitment/pbin_cell.go | 12 ++ execution/commitment/pbin_domainwrite_test.go | 193 ++++++++++++++++++ execution/commitment/pbin_patricia_hashed.go | 17 +- 4 files changed, 223 insertions(+), 9 deletions(-) create mode 100644 execution/commitment/pbin_domainwrite_test.go diff --git a/docs/plans/20260730-pbin-m1-local-el.md b/docs/plans/20260730-pbin-m1-local-el.md index bfe100084db..3ea1853e31b 100644 --- a/docs/plans/20260730-pbin-m1-local-el.md +++ b/docs/plans/20260730-pbin-m1-local-el.md @@ -169,11 +169,11 @@ Production keeps Keccak-256. This task only makes the **test** path run the whol - Modify: `execution/commitment/pbin_patricia_hashed.go` - Create: `execution/commitment/pbin_domainwrite_test.go` -- [ ] write a failing test asserting neither `storeRoot` nor `foldDelete` ever hands a nil value to `PutBranch` -- [ ] route the empty-root `storeRoot` path (`:328-337`) and `foldDelete` (`:725-727`) through `DomainDel` or a non-nil zero-length slice -- [ ] pass real `prevData` at both `PutBranch` sites to avoid the extra `GetLatest` per branch write -- [ ] write a test asserting a zero-length branch value round-trips as a deletion -- [ ] run tests — must pass before task 4 +- [x] write a failing test asserting neither `storeRoot` nor `foldDelete` ever hands a nil value to `PutBranch` — `TestPBinStoreRootEmptiedTreeWritesNonNil` + `TestPBinFoldDeleteWritesNonNilWithRealPrev` over `pbinStrictWriteContext`, which refuses nil the way `SharedDomains.DomainPut` does; both red before the fix +- [x] route the empty-root `storeRoot` path (`:328-337`) and `foldDelete` (`:725-727`) through `DomainDel` or a non-nil zero-length slice — non-nil zero-length: `PatriciaContext` has no `DomainDel`, and `TemporalMemBatch.putHistory` routes any `len(v) == 0` write to `DeleteWithPrev`, so `[]byte{}` IS the deletion encoding at the domain boundary +- [x] pass real `prevData` at both `PutBranch` sites to avoid the extra `GetLatest` per branch write — the grid retains each row's record bytes at unfold (`pbinGrid.prevRecord`), the engine retains the root record across load/store (`rootPrev`); all three write sites (`foldBranch`, `foldDelete`, `storeRoot`) now pass it. `TestPBinProcessPutBranchCarriesRealPrev` checks every write's prev equals the record it replaces, red before +- [x] write a test asserting a zero-length branch value round-trips as a deletion — `TestPBinZeroLengthBranchRoundTripsAsDeletion`: the engine empties a stored tree, the zero-length records stay in the store, a fresh engine reads them back as no tree +- [x] run tests — `go test ./execution/commitment/... -count=1` green, `make lint` clean ### Task 4: Disable the shared BranchCache for the bin variant diff --git a/execution/commitment/pbin_cell.go b/execution/commitment/pbin_cell.go index a549e084ddb..8e1e6d25f53 100644 --- a/execution/commitment/pbin_cell.go +++ b/execution/commitment/pbin_cell.go @@ -80,6 +80,7 @@ type pbinGrid struct { rows [pbinGridRows][2]pbinCell depths [pbinGridRows]int16 branchBefore [pbinGridRows]bool + prevRecord [pbinGridRows][]byte touchMap [pbinGridRows]uint16 afterMap [pbinGridRows]uint16 activeRows int @@ -95,8 +96,19 @@ func (g *pbinGrid) resetForReuse() { g.rows[row][1].reset() g.depths[row] = 0 g.branchBefore[row] = false + g.prevRecord[row] = nil g.touchMap[row] = 0 g.afterMap[row] = 0 } g.activeRows = 0 } + +// prevRecordFor is what the store holds at a row's record key: the bytes the +// row unfolded from, or zero-length when it had no record. Never nil, so the +// write layer takes it as the known previous value instead of reading its own. +func (g *pbinGrid) prevRecordFor(row int) []byte { + if g.prevRecord[row] == nil { + return []byte{} + } + return g.prevRecord[row] +} diff --git a/execution/commitment/pbin_domainwrite_test.go b/execution/commitment/pbin_domainwrite_test.go new file mode 100644 index 00000000000..15f21638875 --- /dev/null +++ b/execution/commitment/pbin_domainwrite_test.go @@ -0,0 +1,193 @@ +// Copyright 2026 The Erigon Authors +// This file is part of Erigon. +// +// Erigon is free software: you can redistribute it and/or modify +// it under the terms of the GNU Lesser General Public License as published by +// the Free Software Foundation, either version 3 of the License, or +// (at your option) any later version. +// +// Erigon is distributed in the hope that it will be useful, +// but WITHOUT ANY WARRANTY; without even the implied warranty of +// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +// GNU Lesser General Public License for more details. +// +// You should have received a copy of the GNU Lesser General Public License +// along with Erigon. If not, see . + +package commitment + +import ( + "bytes" + "fmt" + "testing" + + "github.com/stretchr/testify/require" + + "github.com/erigontech/erigon/common" + "github.com/erigontech/erigon/common/length" +) + +// pbinStrictWriteContext mirrors the domain's write contract: SharedDomains +// refuses a nil value outright, so a PutBranch handing one over fails here the +// way it would over a real datadir. Accepted writes are recorded in order with +// the prevData the engine claimed. +type pbinStrictWriteContext struct { + *MockState + puts []pbinRecordedPut +} + +type pbinRecordedPut struct { + prefix, data, prev []byte +} + +func (c *pbinStrictWriteContext) PutBranch(prefix, data, prevData []byte) error { + if data == nil { + return fmt.Errorf("pbin test: nil value for %x refused, as the domain would", prefix) + } + c.puts = append(c.puts, pbinRecordedPut{bytes.Clone(prefix), bytes.Clone(data), bytes.Clone(prevData)}) + return c.MockState.PutBranch(prefix, data, prevData) +} + +func pbinTestStrictEngine(t *testing.T) (*PBinPatriciaHashed, *pbinStrictWriteContext, *MockState) { + t.Helper() + ms := NewMockState(t) + ctx := &pbinStrictWriteContext{MockState: ms} + return NewPBinPatriciaHashed(ctx), ctx, ms +} + +// TestPBinStoreRootEmptiedTreeWritesNonNil pins the empty-root storeRoot path: +// an emptied tree deletes its record by writing a zero-length value, never nil. +func TestPBinStoreRootEmptiedTreeWritesNonNil(t *testing.T) { + t.Parallel() + + pph, ctx, _ := pbinTestStrictEngine(t) + require.NoError(t, pph.loadRoot()) + pph.rootTouched = true + + require.NoError(t, pph.storeRoot()) + require.Len(t, ctx.puts, 1) + put := ctx.puts[0] + require.Equal(t, pbinRootKey, put.prefix) + require.NotNil(t, put.data) + require.Empty(t, put.data) + require.NotNil(t, put.prev) +} + +// TestPBinFoldDeleteWritesNonNilWithRealPrev drives a stored record through the +// touched-but-gone unfold into foldDelete: the deletion write must carry a +// zero-length value, and prevData must be the record bytes the row unfolded +// from — likewise for the root record storeRoot then empties. +func TestPBinFoldDeleteWritesNonNilWithRealPrev(t *testing.T) { + t.Parallel() + + pph, ctx, ms := pbinTestStrictEngine(t) + pbinTestPutTopRecord(t, ms, [2]pbinCell{ + pbinTestSpecCell(t, pbinNodeLeaf, "010"), + pbinTestSpecCell(t, pbinNodeLeaf, "110"), + }) + emptyPath := pbinBitpath{} + recordKey := pbinEncodeBitPath(&emptyPath) + storedRecord := bytes.Clone(ms.cm[string(recordKey)]) + storedRoot := bytes.Clone(ms.cm[string(pbinRootKey)]) + + probe := pbinTestPathFromBits(t, pbinTestBitSpec(t, "0101")) + pbinTestUnfoldStep(t, pph, &probe) + pph.grid.touchMap[0], pph.grid.afterMap[0] = 0b11, 0 + require.NoError(t, pph.fold()) + require.NoError(t, pph.storeRoot()) + + require.Len(t, ctx.puts, 2) + del, root := ctx.puts[0], ctx.puts[1] + require.Equal(t, recordKey, del.prefix) + require.NotNil(t, del.data) + require.Empty(t, del.data) + require.Equal(t, storedRecord, del.prev) + + require.Equal(t, pbinRootKey, root.prefix) + require.NotNil(t, root.data) + require.Empty(t, root.data) + require.Equal(t, storedRoot, root.prev) +} + +// TestPBinZeroLengthBranchRoundTripsAsDeletion checks the deletion writes all +// the way back around: after the engine empties a stored tree, the zero-length +// records still sitting in the store must read back as no tree at all. +func TestPBinZeroLengthBranchRoundTripsAsDeletion(t *testing.T) { + t.Parallel() + + pph, _, ms := pbinTestStrictEngine(t) + pbinTestPutTopRecord(t, ms, [2]pbinCell{ + pbinTestSpecCell(t, pbinNodeLeaf, "010"), + pbinTestSpecCell(t, pbinNodeLeaf, "110"), + }) + + probe := pbinTestPathFromBits(t, pbinTestBitSpec(t, "0101")) + pbinTestUnfoldStep(t, pph, &probe) + pph.grid.touchMap[0], pph.grid.afterMap[0] = 0b11, 0 + require.NoError(t, pph.fold()) + require.NoError(t, pph.storeRoot()) + + emptyPath := pbinBitpath{} + require.Contains(t, ms.cm, string(pbinEncodeBitPath(&emptyPath))) + require.Contains(t, ms.cm, string(pbinRootKey)) + + fresh := NewPBinPatriciaHashed(ms) + require.NoError(t, fresh.loadRoot()) + require.False(t, fresh.rootPresent, "a zero-length root record must read back as no tree") + root, err := fresh.RootHash() + require.NoError(t, err) + require.Equal(t, make([]byte, length.Hash), root) +} + +// pbinRequirePutsMatchStore walks recorded writes in order against what the +// store held before the run, requiring each prevData to be exactly the value +// the write replaces — and non-nil, so the domain never falls back to its own +// read. Returns how many writes replaced an existing record. +func pbinRequirePutsMatchStore(t *testing.T, puts []pbinRecordedPut, store map[string][]byte) (overwrites int) { + t.Helper() + for _, put := range puts { + require.NotNil(t, put.prev, "nil prevData at %x forces an extra domain read", put.prefix) + require.True(t, bytes.Equal(store[string(put.prefix)], put.prev), + "prevData at %x does not match the record it replaces", put.prefix) + if len(put.prev) > 0 { + overwrites++ + } + store[string(put.prefix)] = put.data + } + return overwrites +} + +// TestPBinProcessPutBranchCarriesRealPrev runs a second batch over a stored +// tree and requires every branch write to carry the previous record it +// replaces: empty on a fresh store, the stored bytes on a rewrite. +func TestPBinProcessPutBranchCarriesRealPrev(t *testing.T) { + t.Parallel() + + addr := pbinOracleAddr(31) + stored := new(pbinTestCorpus). + storage(addr, pbinOracleSlot(256), 0x01). + storage(addr, pbinOracleSlot(257), 0x02). + account(pbinOracleAddr(32), 3, 4, common.Hash{0x32}) + touch := new(pbinTestCorpus). + storage(addr, pbinOracleSlot(256), 0x0A). + storage(addr, pbinOracleSlot(258), 0x03) + + pph, ctx, ms := pbinTestStrictEngine(t) + require.NoError(t, ms.applyPlainUpdates(stored.plainKeys, stored.updates)) + pbinTestProcess(t, pph, stored.plainKeys, stored.updates) + require.NotEmpty(t, ctx.puts) + require.Zero(t, pbinRequirePutsMatchStore(t, ctx.puts, map[string][]byte{}), + "the first run has nothing to overwrite") + + snapshot := make(map[string][]byte, len(ms.cm)) + for k, v := range ms.cm { + snapshot[k] = bytes.Clone(v) + } + ctx.puts = nil + + require.NoError(t, ms.applyPlainUpdates(touch.plainKeys, touch.updates)) + pph.Reset() + pbinTestProcess(t, pph, touch.plainKeys, touch.updates) + require.NotZero(t, pbinRequirePutsMatchStore(t, ctx.puts, snapshot), + "the second run must rewrite at least one stored record") +} diff --git a/execution/commitment/pbin_patricia_hashed.go b/execution/commitment/pbin_patricia_hashed.go index c429ff3d55a..d3bf1067c41 100644 --- a/execution/commitment/pbin_patricia_hashed.go +++ b/execution/commitment/pbin_patricia_hashed.go @@ -59,6 +59,7 @@ type PBinPatriciaHashed struct { rootChecked bool // whether the root record is known to be absent rootTouched bool rootPresent bool + rootPrev []byte // root record as last read or written; nil = never read } // pbinCounters measures what keeping a single hash per branch cell costs. A @@ -101,6 +102,7 @@ func (pph *PBinPatriciaHashed) Reset() { pph.grid.resetForReuse() pph.currentKey = pbinBitpath{} pph.rootChecked, pph.rootTouched, pph.rootPresent = false, false, false + pph.rootPrev = nil } // setHashSuite swaps H on both seams at once — node hashing on this engine and @@ -335,16 +337,19 @@ func (pph *PBinPatriciaHashed) storeRoot() error { if !pph.rootTouched { return nil } - var record []byte + // An emptied tree deletes the record: zero-length is the deletion encoding, + // and the domain refuses a nil value outright. + record := []byte{} if pph.grid.root.kind != pbinNodeEmpty { var err error if record, err = pbinAppendCell(nil, &pph.grid.root); err != nil { return err } } - if err := pph.ctx.PutBranch(pbinRootKey, record, nil); err != nil { + if err := pph.ctx.PutBranch(pbinRootKey, record, pph.rootPrev); err != nil { return fmt.Errorf("pbin: write root cell: %w", err) } + pph.rootPrev = record return nil } @@ -357,8 +362,10 @@ func (pph *PBinPatriciaHashed) loadRoot() error { return fmt.Errorf("pbin: read root cell: %w", err) } if len(data) == 0 { + pph.rootPrev = []byte{} return nil } + pph.rootPrev = data pph.grid.root.reset() pos, err := pbinDecodeCell(data, 0, &pph.grid.root) if err != nil { @@ -477,6 +484,7 @@ func (pph *PBinPatriciaHashed) unfold(probe *pbinBitpath, u pbinUnfolding) error g.rows[row][0].reset() g.rows[row][1].reset() g.touchMap[row], g.afterMap[row], g.branchBefore[row] = 0, 0, false + g.prevRecord[row] = nil if u.action == pbinUnfoldRecord { return pph.unfoldBranchNode(row, upDepth+1, touched && !present) @@ -544,6 +552,7 @@ func (pph *PBinPatriciaHashed) unfoldBranchNode(row int, depth int16, deleted bo if err != nil { return fmt.Errorf("pbin: decode branch at %x: %w", key, err) } + g.prevRecord[row] = data // The record's own touch map is write-time bookkeeping; nothing in this run // has touched the row yet. A parent cell that is touched but gone takes the // whole subtree with it. @@ -681,7 +690,7 @@ func (pph *PBinPatriciaHashed) foldBranch(row int, bit uint64, upDepth, depth in if err != nil { return err } - if err = pph.ctx.PutBranch(key, bytes.Clone(record), nil); err != nil { + if err = pph.ctx.PutBranch(key, bytes.Clone(record), g.prevRecordFor(row)); err != nil { return fmt.Errorf("pbin: write branch at %x: %w", key, err) } @@ -733,7 +742,7 @@ func (pph *PBinPatriciaHashed) foldDelete(row int, bit uint64, upCell *pbinCell) return nil } key := pbinEncodeBitPath(&pph.currentKey) - if err := pph.ctx.PutBranch(key, nil, nil); err != nil { + if err := pph.ctx.PutBranch(key, []byte{}, g.prevRecordFor(row)); err != nil { return fmt.Errorf("pbin: delete branch at %x: %w", key, err) } return nil From 9a44be45b2c7c3a448c1e6f376f37f9c1647b185 Mon Sep 17 00:00:00 2001 From: awskii Date: Thu, 30 Jul 2026 13:59:38 +0700 Subject: [PATCH 27/56] execution/commitment, db/state: refuse a shared branch cache under the pbin variant --- db/state/execctx/domain_shared.go | 9 ++ docs/plans/20260730-pbin-m1-local-el.md | 10 +- .../commitmentdb/commitment_context.go | 10 ++ .../commitmentdb/pbin_nocache_test.go | 140 ++++++++++++++++++ 4 files changed, 164 insertions(+), 5 deletions(-) create mode 100644 execution/commitment/commitmentdb/pbin_nocache_test.go diff --git a/db/state/execctx/domain_shared.go b/db/state/execctx/domain_shared.go index ea77d118adb..398c2068ba2 100644 --- a/db/state/execctx/domain_shared.go +++ b/db/state/execctx/domain_shared.go @@ -195,6 +195,11 @@ func NewSharedDomains(ctx context.Context, tx kv.TemporalTx, logger log.Logger, for _, opt := range opts { opt(&o) } + // Bit-path branch keys collide in the cache's hex-shaped trunk slots; the + // commitment-context ctor refuses a bin SD that shares the cache. + if o.trieCfg.Variant == commitment.VariantBinPatriciaTrie { + WithoutSharedBranchCache()(&o) + } trieCfg := o.trieCfg sd := &SharedDomains{ @@ -778,6 +783,10 @@ func (sd *SharedDomains) IndexAdd(table kv.InvertedIdx, key []byte, txNum uint64 func (sd *SharedDomains) StepSize() uint64 { return sd.stepSize } +// HasSharedBranchCache reports whether commitment-branch reads go through the +// aggregator-scope BranchCache shared across SharedDomains instances. +func (sd *SharedDomains) HasSharedBranchCache() bool { return sd.branchCache != nil } + // IsUnfrozenStepEdge reports whether txNum is the last tx of a step whose // commitment is not yet frozen into files — where a step-boundary checkpoint // must be written. diff --git a/docs/plans/20260730-pbin-m1-local-el.md b/docs/plans/20260730-pbin-m1-local-el.md index 3ea1853e31b..797225c9375 100644 --- a/docs/plans/20260730-pbin-m1-local-el.md +++ b/docs/plans/20260730-pbin-m1-local-el.md @@ -181,11 +181,11 @@ Production keeps Keccak-256. This task only makes the **test** path run the whol - Modify: `execution/commitment/commitmentdb/commitment_context.go` - Create: `execution/commitment/commitmentdb/pbin_nocache_test.go` -- [ ] write a failing test asserting a bin-variant `SharedDomains` has no shared branch cache -- [ ] write a failing test demonstrating the `trunkSlot` collision for two distinct ≤8-bit bit-path keys, so the reason is pinned in the suite (guards H1) -- [ ] construct the bin-variant `SharedDomains` with `execctx.WithoutSharedBranchCache()` -- [ ] add a structural assert in the commitment-context ctor that the bin variant never has a shared branch cache — enforce, do not document -- [ ] run tests — must pass before task 5 +- [x] write a failing test asserting a bin-variant `SharedDomains` has no shared branch cache — `TestPBinSharedDomainsHasNoSharedBranchCache`, red while the ctor assert saw the shared cache; written to survive Task 5 (tolerates the save/restore panic, asserts directly on the SD once construction succeeds) +- [x] write a failing test demonstrating the `trunkSlot` collision for two distinct ≤8-bit bit-path keys, so the reason is pinned in the suite (guards H1) — `TestPBinBranchCacheTrunkSlotCollision`: 3-bit paths 000 (`00 03`) and 001 (`20 03`) both index `d2[0x03]`; `Get` serves the other path's record as a well-formed hit. Pinning test — it passes against current `trunkSlot` by design and fails if the collision ever disappears +- [x] construct the bin-variant `SharedDomains` with `execctx.WithoutSharedBranchCache()` — `NewSharedDomains` applies it whenever `trieCfg.Variant` is bin; the co-located `AdaptivePinController` is gated on the same option and stays off too +- [x] add a structural assert in the commitment-context ctor that the bin variant never has a shared branch cache — enforce, do not document — the commitmentdb `sd` interface gained `HasSharedBranchCache()` (implemented by `execctx.SharedDomains`); `NewSharedDomainsCommitmentContext` panics on bin+shared-cache ahead of the save/restore panic Task 5 removes +- [x] run tests — `go test ./execution/commitment/... -count=1` and `./db/state/... -short` green, `make lint` clean twice ### Task 5: SetState / EncodeCurrentState for pbin, and remove the panic diff --git a/execution/commitment/commitmentdb/commitment_context.go b/execution/commitment/commitmentdb/commitment_context.go index fb23ead2d4a..3c37fbdad73 100644 --- a/execution/commitment/commitmentdb/commitment_context.go +++ b/execution/commitment/commitmentdb/commitment_context.go @@ -51,6 +51,10 @@ type sd interface { // per domain (Storage value loads vs Commitment branch reads // vs Account loads). Metrics() *kvmetrics.DomainMetrics + + // HasSharedBranchCache reports whether commitment-branch reads go through + // the aggregator-scope BranchCache shared across SharedDomains instances. + HasSharedBranchCache() bool } type SharedDomainsCommitmentContext struct { @@ -223,6 +227,12 @@ func NewSharedDomainsCommitmentContext(sd sd, mode commitment.Mode, tmpDir strin variant = commitment.VariantHexPatriciaTrie } if variant == commitment.VariantBinPatriciaTrie { + // The shared BranchCache indexes trunk slots by hex compact prefixes; + // distinct bin bit-path keys collapse onto one slot, so a shared cache + // would serve another node's record as a well-formed hit. + if sd != nil && sd.HasSharedBranchCache() { + panic("commitment variant " + string(variant) + " cannot use the shared branch cache: bit-path keys collide in its trunk slots") + } // encodeCommitmentState/restorePatriciaState are hex-only, so this variant // would fail after a full Process instead of at configuration time. panic("commitment variant " + string(variant) + " has no state save/restore and cannot back a domain commitment context") diff --git a/execution/commitment/commitmentdb/pbin_nocache_test.go b/execution/commitment/commitmentdb/pbin_nocache_test.go new file mode 100644 index 00000000000..cb4f2982786 --- /dev/null +++ b/execution/commitment/commitmentdb/pbin_nocache_test.go @@ -0,0 +1,140 @@ +// Copyright 2026 The Erigon Authors +// This file is part of Erigon. +// +// Erigon is free software: you can redistribute it and/or modify +// it under the terms of the GNU Lesser General Public License as published by +// the Free Software Foundation, either version 3 of the License, or +// (at your option) any later version. +// +// Erigon is distributed in the hope that it will be useful, +// but WITHOUT ANY WARRANTY; without even the implied warranty of +// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +// GNU Lesser General Public License for more details. +// +// You should have received a copy of the GNU Lesser General Public License +// along with Erigon. If not, see . + +package commitmentdb_test + +import ( + "fmt" + "testing" + + "github.com/c2h5oh/datasize" + "github.com/stretchr/testify/require" + + "github.com/erigontech/erigon/common/log/v3" + "github.com/erigontech/erigon/db/datadir" + "github.com/erigontech/erigon/db/kv" + "github.com/erigontech/erigon/db/kv/dbcfg" + "github.com/erigontech/erigon/db/kv/mdbx" + "github.com/erigontech/erigon/db/kv/temporal" + "github.com/erigontech/erigon/db/state" + "github.com/erigontech/erigon/db/state/execctx" + "github.com/erigontech/erigon/db/state/kvmetrics" + "github.com/erigontech/erigon/execution/commitment" + "github.com/erigontech/erigon/execution/commitment/commitmentdb" +) + +type pbinStubSharedDomains struct{ sharedCache bool } + +func (s *pbinStubSharedDomains) SetTxNum(uint64) {} +func (s *pbinStubSharedDomains) AsGetter(kv.TemporalTx) kv.TemporalGetter { return nil } +func (s *pbinStubSharedDomains) AsPutDel(kv.TemporalTx) kv.TemporalPutDel { return nil } +func (s *pbinStubSharedDomains) MergeMetrics(kvmetrics.Source, *kvmetrics.DomainMetrics) {} +func (s *pbinStubSharedDomains) StepSize() uint64 { return 1 } +func (s *pbinStubSharedDomains) Metrics() *kvmetrics.DomainMetrics { return nil } +func (s *pbinStubSharedDomains) HasSharedBranchCache() bool { return s.sharedCache } + +func pbinRecoverMessage(t *testing.T, fn func()) (msg string) { + t.Helper() + defer func() { + if r := recover(); r != nil { + msg = fmt.Sprint(r) + } + }() + fn() + return "" +} + +// TestPBinCtorRefusesSharedBranchCache pins the structural assert for H1: a +// bin-variant commitment context over a SharedDomains that shares the branch +// cache must be refused at construction, by name, before anything else runs. +func TestPBinCtorRefusesSharedBranchCache(t *testing.T) { + t.Parallel() + + cfg := commitment.DefaultTrieConfig() + cfg.Variant = commitment.VariantBinPatriciaTrie + msg := pbinRecoverMessage(t, func() { + commitmentdb.NewSharedDomainsCommitmentContext(&pbinStubSharedDomains{sharedCache: true}, commitment.ModeDirect, t.TempDir(), cfg) + }) + require.Contains(t, msg, "branch cache") +} + +// TestPBinBranchCacheTrunkSlotCollision demonstrates H1, the reason the bin +// variant must not share the BranchCache. The trunk-slot index reads a prefix +// as a hex compact path, which is injective for hex keys; a pbin bit-path key +// is packed MSB-first bits plus a trailing bitLen%8 byte, so distinct short +// paths land on one slot and the cache serves another node's record as a +// well-formed hit. +func TestPBinBranchCacheTrunkSlotCollision(t *testing.T) { + t.Parallel() + + cache := commitment.NewBranchCache(commitment.DefaultBranchCacheTailCapacity) + defer cache.Close() + + // 3-bit path 000 and 3-bit path 001: both index depth-2 slot d2[0x03]. + keyA := []byte{0x00, 0x03} + keyB := []byte{0x20, 0x03} + dataA := []byte{0xde, 0xad, 0xbe, 0xef} + + cache.Put(keyA, dataA, 1, 1) + got, _, ok := cache.Get(keyB) + require.True(t, ok, "distinct bit-path key no longer collides — revisit whether the bin variant may share the BranchCache") + require.Equal(t, dataA, got) +} + +func pbinNewTestDb(tb testing.TB) kv.TemporalRwDB { + tb.Helper() + logger := log.New() + dirs := datadir.New(tb.TempDir()) + db := mdbx.New(dbcfg.ChainDB, logger).InMem(tb, dirs.Chaindata).GrowthStep(32 * datasize.MB).MapSize(2 * datasize.GB).MustOpen() + tb.Cleanup(db.Close) + + agg := state.NewTest(dirs).StepSize(16).Logger(logger).MustOpen(tb.Context(), db) + tb.Cleanup(agg.Close) + require.NoError(tb, agg.OpenFolder()) + tdb, err := temporal.New(db, agg, nil) + require.NoError(tb, err) + return tdb +} + +// TestPBinSharedDomainsHasNoSharedBranchCache checks the execctx wiring: a +// bin-variant SharedDomains over an aggregator whose AggTx provides the shared +// BranchCache must reach the commitment-context ctor without it. While the bin +// variant still lacks state save/restore the ctor refuses it outright — but it +// must refuse for that reason, never because a shared cache got through. +func TestPBinSharedDomainsHasNoSharedBranchCache(t *testing.T) { + t.Parallel() + + db := pbinNewTestDb(t) + tx, err := db.BeginTemporalRw(t.Context()) + require.NoError(t, err) + defer tx.Rollback() + + cfg := commitment.DefaultTrieConfig() + cfg.Variant = commitment.VariantBinPatriciaTrie + + var sd *execctx.SharedDomains + msg := pbinRecoverMessage(t, func() { + var sdErr error + sd, sdErr = execctx.NewSharedDomains(t.Context(), tx, log.New(), execctx.WithTrieConfig(cfg)) + require.NoError(t, sdErr) + }) + if msg != "" { + require.NotContains(t, msg, "branch cache") + return + } + defer sd.Close() + require.False(t, sd.HasSharedBranchCache()) +} From a8c8dff90248abad981195b87978c35e071a4a05 Mon Sep 17 00:00:00 2001 From: awskii Date: Thu, 30 Jul 2026 14:23:05 +0700 Subject: [PATCH 28/56] execution/commitment: pbin state save/restore behind a StatefulTrie seam The pbin state blob is the root cell plus the three root flags. Proven restorable-as-zero: every row-indexed grid array is initialized at unfold before activeRows exposes the row, so nothing per row is serialized and no depth ever meets a one-byte encoding (H6). The commitmentdb save/restore gates (LatestCommitmentState, encodeCommitmentState, restorePatriciaState) now assert the optional StatefulTrie interface instead of enumerating hex variants, the bin-variant ctor panic is gone, and the context carries the real variant tag. The 16-byte txNum-blockNum header stays byte-identical across variants. --- docs/plans/20260730-pbin-m1-local-el.md | 18 +-- execution/commitment/commitment.go | 9 ++ .../commitmentdb/commitment_context.go | 67 ++------ .../commitmentdb/commitment_context_test.go | 15 +- .../commitmentdb/pbin_state_header_test.go | 76 +++++++++ .../commitment/parallel_patricia_hashed.go | 10 ++ execution/commitment/pbin_state.go | 109 +++++++++++++ execution/commitment/pbin_state_test.go | 147 ++++++++++++++++++ 8 files changed, 385 insertions(+), 66 deletions(-) create mode 100644 execution/commitment/commitmentdb/pbin_state_header_test.go create mode 100644 execution/commitment/pbin_state.go create mode 100644 execution/commitment/pbin_state_test.go diff --git a/docs/plans/20260730-pbin-m1-local-el.md b/docs/plans/20260730-pbin-m1-local-el.md index 797225c9375..acff1d48d0a 100644 --- a/docs/plans/20260730-pbin-m1-local-el.md +++ b/docs/plans/20260730-pbin-m1-local-el.md @@ -122,7 +122,7 @@ Blocking items needing a human or upstream answer. Do not proceed past the task Do not treat these as established: - `pbin_branch.go` record field-bit layout beyond the leaf-kind rejection at `:156-165`. **Verify before designing Task 13's value-in-record field.** -- The "grid arrays are restorable-as-zero" argument underpinning Task 5's ~160-byte blob (`SetState` only runs at `activeRows == 0`; `unfold` initializes each row before any read). Well-argued, not proven. Prove it or pay the ~3.3 KB full-grid blob. +- ~~The "grid arrays are restorable-as-zero" argument underpinning Task 5's ~160-byte blob~~ — **proven in Task 5** (see the Task 5 checklist for the read/write-site audit); the root-cell blob landed. - Whether pbin branch records are truly opaque to the pass-through merge path (believed yes with references off, not exercised). - Task 8's deferral mis-attribution, inferred from comments at `commitment_context.go:150-157` and `:581-583`; no concrete failing sequence was constructed. @@ -195,14 +195,14 @@ Production keeps Keccak-256. This task only makes the **test** path run the whol - Modify: `execution/commitment/commitmentdb/commitment_context.go` - Create: `execution/commitment/pbin_state_test.go` -- [ ] write a failing restart round-trip test covering a path deeper than 256 bits (guards H6) -- [ ] prove or refute that the grid arrays are restorable-as-zero (`SetState` only at `activeRows == 0`); record the outcome in this plan and pick the ~160 B root-cell blob or the ~3.3 KB full-grid blob accordingly -- [ ] implement `pbin` `SetState`/`EncodeCurrentState` with 2-byte depths, never `byte(depth)` -- [ ] remove the `VariantBinPatriciaTrie` panic (`:225-229`) and fix the hardcoded `variant:` in the struct literal (`:233`) -- [ ] extend the three variant gates: `LatestCommitmentState` (`:806-808`), `encodeCommitmentState` (`:912-913`), `restorePatriciaState` (`:953-955`) -- [ ] promote `StatefulTrie` as an **optional** interface asserted at those 3 sites; do not widen `Trie` -- [ ] write a test asserting the 16-byte `txNum‖blockNum` header is byte-identical to hex's -- [ ] run tests — must pass before task 6 +- [x] write a failing restart round-trip test covering a path deeper than 256 bits (guards H6) — `TestPBinRestartRoundTripDeepPath`: same-group slots 256/257 share the first 520 tree-key bits, so the root branch prefix is 527 bits; encode → restore → continue reproduces the oracle root +- [x] prove or refute that the grid arrays are restorable-as-zero — **proven**: every row-indexed array (`rows`, `depths`, `touchMap`, `afterMap`, `branchBefore`, `prevRecord`) is written only in `unfold`/`unfoldBranchNode` before `activeRows++` exposes the row, and read only at indexes < `activeRows` (`updateCell`, `needUnfolding`, `fold` and its three arms). At `activeRows == 0` — which both state calls enforce — the live state is exactly the root cell plus the three root flags. Chose the root-cell blob: `0xB1 marker ‖ flags ‖ uint16 len ‖ pbinAppendCell(root)`. `rootPrev` is deliberately not serialized: a post-restore `storeRoot` passes nil prev and `DomainPut` fetches the stored value itself +- [x] implement `pbin` `SetState`/`EncodeCurrentState` — the chosen blob serializes no depths at all, so no depth ever meets a one-byte encoding; the marker byte also rejects a hex blob outright (hex starts with a flags byte ≤ 0x07) +- [x] remove the `VariantBinPatriciaTrie` panic and fix the hardcoded `variant:` in the struct literal — the stale `Test_NewSharedDomainsCommitmentContext_RejectsBinVariant` that pinned the panic became `..._AcceptsBinVariant` +- [x] extend the three variant gates: `LatestCommitmentState`, `encodeCommitmentState`, `restorePatriciaState` — all three assert `commitment.StatefulTrie`; the trie-trace state capture in `ComputeCommitment` now uses the same seam instead of a hex/parallel type switch +- [x] promote `StatefulTrie` as an **optional** interface asserted at those 3 sites; do not widen `Trie` — declared beside `Trie`; hex satisfies it as-is, `ParallelPatriciaHashed` delegates to its template trie, pbin implements it in `pbin_state.go` +- [x] write a test asserting the 16-byte `txNum‖blockNum` header is byte-identical to hex's — `TestPBinCommitmentStateHeaderMatchesHex` (➕ white-box `commitmentdb/pbin_state_header_test.go`, not in the planned file list) also round-trips block/tx through `restorePatriciaState` under bin +- [x] run tests — `go test ./execution/commitment/... ./db/state/... -count=1` green, `make lint` clean ### Task 6: The --experimental.bin-commitment flag, persistence, and root-check gating diff --git a/execution/commitment/commitment.go b/execution/commitment/commitment.go index 28e1531c9bf..c49b83f9354 100644 --- a/execution/commitment/commitment.go +++ b/execution/commitment/commitment.go @@ -116,6 +116,15 @@ type Trie interface { Release() } +// StatefulTrie is the optional capability of a Trie to save its in-memory state +// into the commitment-state record and restore it after a restart. Both calls +// require a fully folded trie; a state blob is engine-specific and must only be +// restored by the variant that produced it. +type StatefulTrie interface { + EncodeCurrentState(buf []byte) ([]byte, error) + SetState(buf []byte) error +} + type CommitProgress struct { KeyIndex uint64 UpdateCount uint64 diff --git a/execution/commitment/commitmentdb/commitment_context.go b/execution/commitment/commitmentdb/commitment_context.go index 3c37fbdad73..4b5a603b2a3 100644 --- a/execution/commitment/commitmentdb/commitment_context.go +++ b/execution/commitment/commitmentdb/commitment_context.go @@ -233,14 +233,11 @@ func NewSharedDomainsCommitmentContext(sd sd, mode commitment.Mode, tmpDir strin if sd != nil && sd.HasSharedBranchCache() { panic("commitment variant " + string(variant) + " cannot use the shared branch cache: bit-path keys collide in its trunk slots") } - // encodeCommitmentState/restorePatriciaState are hex-only, so this variant - // would fail after a full Process instead of at configuration time. - panic("commitment variant " + string(variant) + " has no state save/restore and cannot back a domain commitment context") } ctx := &SharedDomainsCommitmentContext{ sharedDomains: sd, tmpDir: tmpDir, - variant: commitment.VariantHexPatriciaTrie, + variant: variant, warmupBase: commitment.WarmupConfig{ Enabled: cfg.EnableTrieWarmup, NumWorkers: cfg.WarmupNumWorkersOrDefault(), @@ -252,6 +249,7 @@ func NewSharedDomainsCommitmentContext(sd sd, mode commitment.Mode, tmpDir strin // never wire one (RPC, integrity, tests) keep working under a global variant // selection. if variant == commitment.VariantParallelHexPatricia || variant == commitment.VariantStreamingHexPatricia { + ctx.variant = commitment.VariantHexPatriciaTrie ctx.pendingVariant = variant cfg.Variant = commitment.VariantHexPatriciaTrie ctx.pendingCfg = cfg @@ -508,11 +506,8 @@ func (sdc *SharedDomainsCommitmentContext) ComputeCommitment(ctx context.Context // In production the trie has been restored via seekCommitment/SetState; // without this snapshot, replay starts from empty state and diverges. var trieState []byte - switch trie := sdc.patriciaTrie.(type) { - case *commitment.HexPatriciaHashed: - trieState, err = trie.EncodeCurrentState(nil) - case *commitment.ParallelPatriciaHashed: - trieState, err = trie.RootTrie().EncodeCurrentState(nil) + if st, ok := sdc.patriciaTrie.(commitment.StatefulTrie); ok { + trieState, err = st.EncodeCurrentState(nil) } if err != nil { log.Warn("[commitment] failed to encode trie state for trace", "err", err) @@ -812,9 +807,8 @@ func DecodeTxBlockNums(v []byte) (txNum, blockNum uint64) { // LatestCommitmentState searches for last encoded state for CommitmentContext. // Found value does not become current state. func (sdc *SharedDomainsCommitmentContext) LatestCommitmentState(trieContext *TrieContext) (blockNum, txNum uint64, state []byte, err error) { - tv := sdc.patriciaTrie.Variant() - if tv != commitment.VariantHexPatriciaTrie && tv != commitment.VariantParallelHexPatricia && tv != commitment.VariantStreamingHexPatricia { - return 0, 0, nil, errors.New("state storing is only supported hex patricia trie") + if _, ok := sdc.patriciaTrie.(commitment.StatefulTrie); !ok { + return 0, 0, nil, fmt.Errorf("commitment state is not supported by trie %T", sdc.patriciaTrie) } var step kv.Step @@ -905,23 +899,14 @@ func (sdc *SharedDomainsCommitmentContext) encodeAndStoreCommitmentState(trieCon // Encodes current trie state and returns it func (sdc *SharedDomainsCommitmentContext) encodeCommitmentState(blockNum, txNum uint64) ([]byte, error) { - var state []byte - var err error - - switch trie := (sdc.patriciaTrie).(type) { - case *commitment.HexPatriciaHashed: - state, err = trie.EncodeCurrentState(nil) - if err != nil { - return nil, err - } - case *commitment.ParallelPatriciaHashed: - state, err = trie.RootTrie().EncodeCurrentState(nil) - if err != nil { - return nil, err - } - default: + st, ok := sdc.patriciaTrie.(commitment.StatefulTrie) + if !ok { return nil, fmt.Errorf("unsupported state storing for patricia trie type: %T", sdc.patriciaTrie) } + state, err := st.EncodeCurrentState(nil) + if err != nil { + return nil, err + } cs := &commitmentState{trieState: state, blockNum: blockNum, txNum: txNum} encoded, err := cs.Encode() @@ -941,35 +926,17 @@ func (sdc *SharedDomainsCommitmentContext) restorePatriciaState(value []byte) (u } // nil value is acceptable for SetState and will reset trie } - tv := sdc.patriciaTrie.Variant() - - var hext *commitment.HexPatriciaHashed - var ppht *commitment.ParallelPatriciaHashed - if tv == commitment.VariantHexPatriciaTrie { - var ok bool - hext, ok = sdc.patriciaTrie.(*commitment.HexPatriciaHashed) - if !ok { - return 0, 0, errors.New("cannot typecast hex patricia trie") - } - } - if tv == commitment.VariantParallelHexPatricia || tv == commitment.VariantStreamingHexPatricia { - var ok bool - ppht, ok = sdc.patriciaTrie.(*commitment.ParallelPatriciaHashed) - if !ok { - return 0, 0, errors.New("cannot typecast parallel hex patricia trie") - } - hext = ppht.RootTrie() - } - if hext == nil { - return 0, 0, errors.New("unsupported trie variant: state restore requires a hex patricia trie") + st, ok := sdc.patriciaTrie.(commitment.StatefulTrie) + if !ok { + return 0, 0, fmt.Errorf("state restore is not supported by trie %T", sdc.patriciaTrie) } - if err := hext.SetState(cs.trieState); err != nil { + if err := st.SetState(cs.trieState); err != nil { return 0, 0, fmt.Errorf("failed restore state : %w", err) } sdc.justRestored.Store(true) // to prevent double reset if sdc.traceW != nil { - rootHash, err := hext.RootHash() + rootHash, err := sdc.patriciaTrie.RootHash() if err != nil { return 0, 0, fmt.Errorf("failed to get root hash after state restore: %w", err) } diff --git a/execution/commitment/commitmentdb/commitment_context_test.go b/execution/commitment/commitmentdb/commitment_context_test.go index be8216501cf..a6a669c0081 100644 --- a/execution/commitment/commitmentdb/commitment_context_test.go +++ b/execution/commitment/commitmentdb/commitment_context_test.go @@ -85,15 +85,16 @@ func Test_TrieContext_BranchCopiesData(t *testing.T) { require.Equal(t, []byte{9, 2, 3}, reader.branchData) } -// Test_NewSharedDomainsCommitmentContext_RejectsBinVariant pins that a variant -// without commitment state save/restore is refused at construction rather than -// mid-block, where encodeCommitmentState would fail after a full Process. -func Test_NewSharedDomainsCommitmentContext_RejectsBinVariant(t *testing.T) { +// Test_NewSharedDomainsCommitmentContext_AcceptsBinVariant pins that the bin +// variant constructs like any other stateful trie and carries its own variant +// tag instead of the hex default. +func Test_NewSharedDomainsCommitmentContext_AcceptsBinVariant(t *testing.T) { t.Parallel() cfg := commitment.DefaultTrieConfig() cfg.Variant = commitment.VariantBinPatriciaTrie - require.Panics(t, func() { - NewSharedDomainsCommitmentContext(nil, commitment.ModeDirect, t.TempDir(), cfg) - }) + sdc := NewSharedDomainsCommitmentContext(nil, commitment.ModeDirect, t.TempDir(), cfg) + defer sdc.Close() + require.Equal(t, commitment.VariantBinPatriciaTrie, sdc.Trie().Variant()) + require.Equal(t, commitment.VariantBinPatriciaTrie, sdc.variant) } diff --git a/execution/commitment/commitmentdb/pbin_state_header_test.go b/execution/commitment/commitmentdb/pbin_state_header_test.go new file mode 100644 index 00000000000..225cc9c6033 --- /dev/null +++ b/execution/commitment/commitmentdb/pbin_state_header_test.go @@ -0,0 +1,76 @@ +// Copyright 2026 The Erigon Authors +// This file is part of Erigon. +// +// Erigon is free software: you can redistribute it and/or modify +// it under the terms of the GNU Lesser General Public License as published by +// the Free Software Foundation, either version 3 of the License, or +// (at your option) any later version. +// +// Erigon is distributed in the hope that it will be useful, +// but WITHOUT ANY WARRANTY; without even the implied warranty of +// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +// GNU Lesser General Public License for more details. +// +// You should have received a copy of the GNU Lesser General Public License +// along with Erigon. If not, see . + +package commitmentdb + +import ( + "testing" + + "github.com/stretchr/testify/require" + + "github.com/erigontech/erigon/db/kv" + "github.com/erigontech/erigon/db/state/kvmetrics" + "github.com/erigontech/erigon/execution/commitment" +) + +type pbinStateStubSD struct{} + +func (s *pbinStateStubSD) SetTxNum(uint64) {} +func (s *pbinStateStubSD) AsGetter(kv.TemporalTx) kv.TemporalGetter { return nil } +func (s *pbinStateStubSD) AsPutDel(kv.TemporalTx) kv.TemporalPutDel { return nil } +func (s *pbinStateStubSD) MergeMetrics(kvmetrics.Source, *kvmetrics.DomainMetrics) {} +func (s *pbinStateStubSD) StepSize() uint64 { return 1 } +func (s *pbinStateStubSD) Metrics() *kvmetrics.DomainMetrics { return nil } +func (s *pbinStateStubSD) HasSharedBranchCache() bool { return false } + +func pbinStateTestCtx(t *testing.T, variant commitment.TrieVariant) *SharedDomainsCommitmentContext { + t.Helper() + cfg := commitment.DefaultTrieConfig() + cfg.Variant = variant + sdc := NewSharedDomainsCommitmentContext(&pbinStateStubSD{}, commitment.ModeDirect, t.TempDir(), cfg) + t.Cleanup(sdc.Close) + return sdc +} + +// TestPBinCommitmentStateHeaderMatchesHex pins the commitment-state record +// layout across variants: the 16-byte txNum‖blockNum header is read raw and +// variant-blind (DecodeTxBlockNums, LatestBlockNumWithCommitment), so the bin +// variant must produce it byte-identically to hex. +func TestPBinCommitmentStateHeaderMatchesHex(t *testing.T) { + t.Parallel() + + const blockNum, txNum = uint64(41), uint64(4321) + + hexCtx := pbinStateTestCtx(t, commitment.VariantHexPatriciaTrie) + hexState, err := hexCtx.encodeCommitmentState(blockNum, txNum) + require.NoError(t, err) + + binCtx := pbinStateTestCtx(t, commitment.VariantBinPatriciaTrie) + require.Equal(t, commitment.VariantBinPatriciaTrie, binCtx.variant) + binState, err := binCtx.encodeCommitmentState(blockNum, txNum) + require.NoError(t, err) + + require.Equal(t, hexState[:16], binState[:16], "the txNum‖blockNum header must stay byte-identical across variants") + + gotTx, gotBlock := DecodeTxBlockNums(binState) + require.Equal(t, txNum, gotTx) + require.Equal(t, blockNum, gotBlock) + + restoredBlock, restoredTx, err := binCtx.restorePatriciaState(binState) + require.NoError(t, err) + require.Equal(t, blockNum, restoredBlock) + require.Equal(t, txNum, restoredTx) +} diff --git a/execution/commitment/parallel_patricia_hashed.go b/execution/commitment/parallel_patricia_hashed.go index 94956c82326..7a6aea7ac3e 100644 --- a/execution/commitment/parallel_patricia_hashed.go +++ b/execution/commitment/parallel_patricia_hashed.go @@ -110,6 +110,16 @@ func (p *ParallelPatriciaHashed) RootTrie() *HexPatriciaHashed { return p.template } +// EncodeCurrentState and SetState delegate to the template trie, which is where +// the live root state lives; they make the parallel trie a StatefulTrie. +func (p *ParallelPatriciaHashed) EncodeCurrentState(buf []byte) ([]byte, error) { + return p.template.EncodeCurrentState(buf) +} + +func (p *ParallelPatriciaHashed) SetState(buf []byte) error { + return p.template.SetState(buf) +} + // Reset clears the published root hash, drops pooled workers, and resets the template so the instance can be reused. func (p *ParallelPatriciaHashed) Reset() { if p.template != nil { diff --git a/execution/commitment/pbin_state.go b/execution/commitment/pbin_state.go new file mode 100644 index 00000000000..9d1d5a47d8b --- /dev/null +++ b/execution/commitment/pbin_state.go @@ -0,0 +1,109 @@ +// Copyright 2026 The Erigon Authors +// This file is part of Erigon. +// +// Erigon is free software: you can redistribute it and/or modify +// it under the terms of the GNU Lesser General Public License as published by +// the Free Software Foundation, either version 3 of the License, or +// (at your option) any later version. +// +// Erigon is distributed in the hope that it will be useful, +// but WITHOUT ANY WARRANTY; without even the implied warranty of +// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +// GNU Lesser General Public License for more details. +// +// You should have received a copy of the GNU Lesser General Public License +// along with Erigon. If not, see . + +package commitment + +import ( + "encoding/binary" + "errors" + "fmt" +) + +// The pbin state blob is the root cell plus the three root flags — nothing per +// row. State is only encoded with every row folded, and unfold fully +// initializes a row before anything reads it, so the grid arrays restore as +// zero. Depths in particular are never serialized, and no depth ever meets a +// one-byte encoding. +const ( + // pbinStateMarker opens every pbin blob. A hex blob opens with a root-flags + // byte ≤ 0x07, so the marker also refuses a cross-variant restore outright. + pbinStateMarker = 0xB1 + + pbinStateRootPresent = 1 + pbinStateRootChecked = 2 + pbinStateRootTouched = 4 + + pbinStateFlagsAll = pbinStateRootPresent | pbinStateRootChecked | pbinStateRootTouched +) + +var ( + errPBinStateBlob = errors.New("pbin: malformed state blob") + errPBinStateOpen = errors.New("pbin: trie state unavailable with rows open") + + _ StatefulTrie = (*PBinPatriciaHashed)(nil) +) + +func (pph *PBinPatriciaHashed) EncodeCurrentState(buf []byte) ([]byte, error) { + if pph.grid.activeRows != 0 || pph.currentKey.bitLen != 0 { + return nil, fmt.Errorf("%w: %d rows, %d-bit key", errPBinStateOpen, pph.grid.activeRows, pph.currentKey.bitLen) + } + var flags byte + if pph.rootPresent { + flags |= pbinStateRootPresent + } + if pph.rootChecked { + flags |= pbinStateRootChecked + } + if pph.rootTouched { + flags |= pbinStateRootTouched + } + buf = append(buf, pbinStateMarker, flags, 0, 0) + lenAt := len(buf) - 2 + if pph.grid.root.kind != pbinNodeEmpty { + var err error + if buf, err = pbinAppendCell(buf, &pph.grid.root); err != nil { + return nil, err + } + } + binary.BigEndian.PutUint16(buf[lenAt:], uint16(len(buf)-lenAt-2)) + return buf, nil +} + +// SetState is the inverse of EncodeCurrentState; nil or empty resets the engine, +// and the tree is then found again through the stored root record. +func (pph *PBinPatriciaHashed) SetState(buf []byte) error { + if pph.grid.activeRows != 0 { + return fmt.Errorf("%w: cannot restore over %d rows", errPBinStateOpen, pph.grid.activeRows) + } + pph.Reset() + if len(buf) == 0 { + return nil + } + if len(buf) < 4 || buf[0] != pbinStateMarker { + return fmt.Errorf("%w: not a pbin blob", errPBinStateBlob) + } + flags := buf[1] + if flags&^byte(pbinStateFlagsAll) != 0 { + return fmt.Errorf("%w: unknown flags %08b", errPBinStateBlob, flags) + } + if rootLen := int(binary.BigEndian.Uint16(buf[2:4])); len(buf) != 4+rootLen { + return fmt.Errorf("%w: root cell of %d bytes in a %d-byte blob", errPBinStateBlob, rootLen, len(buf)) + } + if len(buf) > 4 { + pos, err := pbinDecodeCell(buf, 4, &pph.grid.root) + if err == nil && pos != len(buf) { + err = fmt.Errorf("%w: %d trailing bytes after the root cell", errPBinStateBlob, len(buf)-pos) + } + if err != nil { + pph.grid.root.reset() + return err + } + } + pph.rootPresent = flags&pbinStateRootPresent != 0 + pph.rootChecked = flags&pbinStateRootChecked != 0 + pph.rootTouched = flags&pbinStateRootTouched != 0 + return nil +} diff --git a/execution/commitment/pbin_state_test.go b/execution/commitment/pbin_state_test.go new file mode 100644 index 00000000000..cb0567f142f --- /dev/null +++ b/execution/commitment/pbin_state_test.go @@ -0,0 +1,147 @@ +// Copyright 2026 The Erigon Authors +// This file is part of Erigon. +// +// Erigon is free software: you can redistribute it and/or modify +// it under the terms of the GNU Lesser General Public License as published by +// the Free Software Foundation, either version 3 of the License, or +// (at your option) any later version. +// +// Erigon is distributed in the hope that it will be useful, +// but WITHOUT ANY WARRANTY; without even the implied warranty of +// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +// GNU Lesser General Public License for more details. +// +// You should have received a copy of the GNU Lesser General Public License +// along with Erigon. If not, see . + +package commitment + +import ( + "testing" + + "github.com/stretchr/testify/require" + + "github.com/erigontech/erigon/common/length" +) + +// TestPBinRestartRoundTripDeepPath guards H6: two same-group storage slots share +// the first 520 bits of their tree keys, so the tree's one branch sits deeper +// than any depth a single byte can hold. The engine must encode its state after +// a full fold, restore it in a fresh engine, and keep folding correctly past +// the restart. +func TestPBinRestartRoundTripDeepPath(t *testing.T) { + t.Parallel() + + addr := pbinOracleAddr(51) + stored := new(pbinTestCorpus). + storage(addr, pbinOracleSlot(256), 0x01). + storage(addr, pbinOracleSlot(257), 0x02) + + pph, ms := pbinTestEngine(t) + require.NoError(t, ms.applyPlainUpdates(stored.plainKeys, stored.updates)) + rootBefore := pbinTestProcess(t, pph, stored.plainKeys, stored.updates) + + require.Equal(t, pbinNodeBranch, pph.grid.root.kind) + require.Greater(t, int(pph.grid.root.prefix.bitLen), 256, "the corpus must put the branch past byte-depth range") + + blob, err := pph.EncodeCurrentState(nil) + require.NoError(t, err) + + restored := NewPBinPatriciaHashed(ms) + require.NoError(t, restored.SetState(blob)) + rootAfter, err := restored.RootHash() + require.NoError(t, err) + require.Equal(t, rootBefore, rootAfter, "restored engine must reproduce the pre-restart root") + + more := new(pbinTestCorpus).storage(addr, pbinOracleSlot(258), 0x03) + require.NoError(t, ms.applyPlainUpdates(more.plainKeys, more.updates)) + rootContinued := pbinTestProcess(t, restored, more.plainKeys, more.updates) + + full := new(pbinTestCorpus). + storage(addr, pbinOracleSlot(256), 0x01). + storage(addr, pbinOracleSlot(257), 0x02). + storage(addr, pbinOracleSlot(258), 0x03) + require.Equal(t, full.oracleRoot(t), rootContinued, "the restored engine must keep folding correctly") +} + +// TestPBinStateBlobRoundTripsFlags checks the three root flags survive the blob: +// they are the only engine state beside the root cell, so losing one changes how +// the next run treats the stored tree. +func TestPBinStateBlobRoundTripsFlags(t *testing.T) { + t.Parallel() + + ms, storedRoot := pbinTestStoredTree(t) + pph := NewPBinPatriciaHashed(ms) + require.NoError(t, pph.loadRoot()) + + blob, err := pph.EncodeCurrentState(nil) + require.NoError(t, err) + + restored := NewPBinPatriciaHashed(ms) + require.NoError(t, restored.SetState(blob)) + require.Equal(t, pph.rootChecked, restored.rootChecked) + require.Equal(t, pph.rootTouched, restored.rootTouched) + require.Equal(t, pph.rootPresent, restored.rootPresent) + + root, err := restored.RootHash() + require.NoError(t, err) + require.Equal(t, storedRoot, root) +} + +// TestPBinSetStateEmptyResetsToStored pins the hex convention: no state blob +// resets the engine, and the tree is then found again through the stored root +// record rather than being lost. +func TestPBinSetStateEmptyResetsToStored(t *testing.T) { + t.Parallel() + + ms, storedRoot := pbinTestStoredTree(t) + pph := NewPBinPatriciaHashed(ms) + require.NoError(t, pph.SetState(nil)) + require.False(t, pph.rootChecked) + + root, err := pph.RootHash() + require.NoError(t, err) + require.Equal(t, storedRoot, root) +} + +// TestPBinSetStateRejectsForeignBlob: the blob is read back by whatever engine +// the datadir opens with, so a pbin engine handed a hex blob (or a damaged pbin +// one) must refuse it instead of decoding garbage into the root cell. +func TestPBinSetStateRejectsForeignBlob(t *testing.T) { + t.Parallel() + + hexBlob, err := NewHexPatriciaHashed(length.Addr, nil, DefaultTrieConfig()).EncodeCurrentState(nil) + require.NoError(t, err) + + pph, ms := pbinTestEngine(t) + validBlob, err := pph.EncodeCurrentState(nil) + require.NoError(t, err) + + for name, blob := range map[string][]byte{ + "hex state blob": hexBlob, + "truncated": validBlob[:len(validBlob)-1], + "trailing bytes": append(append([]byte{}, validBlob...), 0x00), + "marker only": {validBlob[0]}, + "unknown flags": {validBlob[0], 0xF8, 0, 0}, + } { + t.Run(name, func(t *testing.T) { + t.Parallel() + fresh := NewPBinPatriciaHashed(ms) + require.Error(t, fresh.SetState(blob), "blob %x must be refused", blob) + }) + } +} + +// TestPBinStateRefusesOpenRows pins the precondition the root-cell blob rests +// on: with a row still open, part of the tree lives in the grid arrays and a +// root-cell snapshot would silently drop it. +func TestPBinStateRefusesOpenRows(t *testing.T) { + t.Parallel() + + pph, _ := pbinTestEngine(t) + pph.grid.activeRows = 1 + + _, err := pph.EncodeCurrentState(nil) + require.Error(t, err) + require.Error(t, pph.SetState(nil)) +} From e39c5d53f15ef24b555ecd05218b10a6ef395b01 Mon Sep 17 00:00:00 2001 From: awskii Date: Thu, 30 Jul 2026 18:24:08 +0700 Subject: [PATCH 29/56] execution/stagedsync, db/state, cmd, node: --experimental.bin-commitment flag, trie_variant persistence, togglable header root check MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The bin trie variant becomes a whole-datadir property: a new trie_variant key in erigondb.toml (written only when bin, absent = hex) is reconciled at every settings resolve — a persisted bin adopts bin process-wide, while a hex datadir under the bin flag, a legacy datadir, references-in-branches, and the streaming/parallel experiments are refused rather than degraded (guards H4, H10). PickTrieVariant gains the bin case and replaces the duplicated inline switch in squeeze.go. The header state-root check is now independently togglable via dbg.CheckHeaderStateRoot (CHECK_HEADER_STATE_ROOT, default ON), honoured through headerRootMismatch at all five comparison sites in exec3.go, exec3_serial.go and committer.go, with a loud startup warning when disabled (guards H13). --- cmd/integration/commands/flags.go | 1 + cmd/utils/flags.go | 12 ++ common/dbg/experiments.go | 6 + db/state/erigondb_settings.go | 68 ++++++- db/state/execctx/commitment_flag_test.go | 13 ++ db/state/execctx/domain_shared.go | 6 +- db/state/pbin_variant_persist_test.go | 186 ++++++++++++++++++ db/state/squeeze.go | 13 +- db/state/statecfg/state_schema.go | 6 + docs/plans/20260730-pbin-m1-local-el.md | 26 +-- execution/stagedsync/committer.go | 7 +- execution/stagedsync/exec3.go | 10 +- execution/stagedsync/exec3_serial.go | 3 +- .../stagedsync/header_root_check_test.go | 54 +++++ node/cli/default_flags.go | 1 + node/eth/backend.go | 7 + node/ethconfig/config.go | 1 + 17 files changed, 386 insertions(+), 34 deletions(-) create mode 100644 db/state/pbin_variant_persist_test.go create mode 100644 execution/stagedsync/header_root_check_test.go diff --git a/cmd/integration/commands/flags.go b/cmd/integration/commands/flags.go index d5551a57689..954ba1b28c0 100644 --- a/cmd/integration/commands/flags.go +++ b/cmd/integration/commands/flags.go @@ -176,6 +176,7 @@ func withDataDir(cmd *cobra.Command) { func withExperimentalCommitment(cmd *cobra.Command) { cmd.Flags().BoolVar(&statecfg.ExperimentalParallelCommitment, utils.ExperimentalParallelCommitmentFlag.Name, statecfg.ExperimentalParallelCommitment, utils.ExperimentalParallelCommitmentFlag.Usage) cmd.Flags().BoolVar(&statecfg.ExperimentalStreamingCommitment, utils.ExperimentalStreamingCommitmentFlag.Name, statecfg.ExperimentalStreamingCommitment, utils.ExperimentalStreamingCommitmentFlag.Usage) + cmd.Flags().BoolVar(&statecfg.ExperimentalBinCommitment, utils.ExperimentalBinCommitmentFlag.Name, statecfg.ExperimentalBinCommitment, utils.ExperimentalBinCommitmentFlag.Usage) } func withBatchSize(cmd *cobra.Command) { diff --git a/cmd/utils/flags.go b/cmd/utils/flags.go index a1bc44cbdf5..4fa4d9d8943 100644 --- a/cmd/utils/flags.go +++ b/cmd/utils/flags.go @@ -1114,6 +1114,14 @@ var ( Usage: "EXPERIMENTAL: enables streaming trie for commitment (StreamingCommitter, overlaps folding with execution). Takes precedence over --experimental.parallel-commitment if set.", Value: false, } + // ExperimentalBinCommitmentFlag selects the EIP-8297 binary commitment trie. + // A whole-datadir property: honoured on a fresh datadir, persisted to + // erigondb.toml there, and adopted from it on later starts. + ExperimentalBinCommitmentFlag = cli.BoolFlag{ + Name: "experimental.bin-commitment", + Usage: "EXPERIMENTAL: enables the EIP-8297 binary commitment trie. Takes effect on a fresh datadir only and is persisted there.", + Value: false, + } GDBMeFlag = cli.BoolFlag{ Name: "gdbme", Usage: "restart erigon under gdb for debug purposes", @@ -2007,6 +2015,10 @@ func SetEthConfig(nodeCtx context.Context, ctx *cli.Command, nodeConfig *nodecfg cfg.ExperimentalStreamingCommitment = true } + if ctx.Bool(ExperimentalBinCommitmentFlag.Name) { + cfg.ExperimentalBinCommitment = true + } + cfg.FcuTimeout = ctx.Duration(FcuTimeoutFlag.Name) cfg.FcuBackgroundPrune = ctx.Bool(FcuBackgroundPruneFlag.Name) cfg.FcuBackgroundCommit = ctx.Bool(FcuBackgroundCommitFlag.Name) diff --git a/common/dbg/experiments.go b/common/dbg/experiments.go index e5d399d58a0..e42fa40c06c 100644 --- a/common/dbg/experiments.go +++ b/common/dbg/experiments.go @@ -67,6 +67,12 @@ var ( discardCommitment = EnvBool("DISCARD_COMMITMENT", false) pruneTotalDifficulty = EnvBool("PRUNE_TOTAL_DIFFICULTY", true) + // CheckHeaderStateRoot gates the post-execution comparison of the computed + // state root against the block header's. On by default; switch off only for + // a chain whose headers this node cannot reproduce — with it off nothing + // cross-checks execution results. + CheckHeaderStateRoot = EnvBool("CHECK_HEADER_STATE_ROOT", true) + // force skipping of any non-Erigon2 .torrent files DownloaderOnlyBlocks = EnvBool("DOWNLOADER_ONLY_BLOCKS", false) diff --git a/db/state/erigondb_settings.go b/db/state/erigondb_settings.go index 3eeb28df813..823fe583a5b 100644 --- a/db/state/erigondb_settings.go +++ b/db/state/erigondb_settings.go @@ -1,6 +1,8 @@ package state import ( + "errors" + "fmt" "os" "path/filepath" @@ -10,14 +12,23 @@ import ( "github.com/erigontech/erigon/common/log/v3" "github.com/erigontech/erigon/db/config3" "github.com/erigontech/erigon/db/datadir" + "github.com/erigontech/erigon/db/state/statecfg" ) const ERIGONDB_SETTINGS_FILE = "erigondb.toml" +const ( + TrieVariantHex = "hex" + TrieVariantBin = "bin" +) + type ErigonDBSettings struct { StepSize uint64 `toml:"step_size"` StepsInFrozenFile uint64 `toml:"steps_in_frozen_file"` ReferencesInCommitmentBranches *bool `toml:"references_in_commitment_branches"` + // TrieVariant is the commitment trie the datadir was created with ("hex" or + // "bin"); absent means hex. Like every erigondb.toml key it wins over the CLI. + TrieVariant *string `toml:"trie_variant,omitempty"` } // RefsInCommitmentBranches resolves the commitment "references in branches" regime, @@ -29,6 +40,41 @@ func (s *ErigonDBSettings) RefsInCommitmentBranches() bool { return *s.ReferencesInCommitmentBranches } +// TrieVariantName resolves the persisted commitment trie variant, treating an +// absent field as the hex trie. +func (s *ErigonDBSettings) TrieVariantName() string { + if s.TrieVariant == nil || *s.TrieVariant == "" { + return TrieVariantHex + } + return *s.TrieVariant +} + +// reconcileTrieVariant applies the datadir's trie variant to the process: a bin +// datadir turns the bin flag on process-wide, and a combination the bin engine +// cannot honour is refused rather than degraded to a wrong-root run. +func reconcileTrieVariant(s *ErigonDBSettings, logger log.Logger) error { + switch s.TrieVariantName() { + case TrieVariantBin: + if s.RefsInCommitmentBranches() { + return errors.New("trie_variant \"bin\" conflicts with references_in_commitment_branches = true") + } + if statecfg.ExperimentalStreamingCommitment || statecfg.ExperimentalParallelCommitment { + return errors.New("the bin commitment trie is sequential-only; drop --experimental.streaming-commitment / --experimental.parallel-commitment") + } + if !statecfg.ExperimentalBinCommitment { + logger.Info("datadir uses the bin commitment trie; enabling it for this process") + statecfg.ExperimentalBinCommitment = true + } + case TrieVariantHex: + if statecfg.ExperimentalBinCommitment { + return errors.New("--experimental.bin-commitment: datadir was created with the hex commitment trie; the bin trie needs a fresh datadir") + } + default: + return fmt.Errorf("erigondb.toml: unknown trie_variant %q", s.TrieVariantName()) + } + return nil +} + func readErigonDBSettings(path string) (*ErigonDBSettings, error) { data, err := os.ReadFile(path) if err != nil { @@ -77,6 +123,9 @@ func ResolveErigonDBSettingsWithRefsDefault(dirs datadir.Dirs, logger log.Logger if err != nil { return nil, err } + if err := reconcileTrieVariant(settings, logger); err != nil { + return nil, err + } if refsFirstStart != nil { logger.Info("--commitment.plainValues ignored: erigondb.toml already exists", "references_in_commitment_branches", settings.RefsInCommitmentBranches()) @@ -84,7 +133,8 @@ func ResolveErigonDBSettingsWithRefsDefault(dirs datadir.Dirs, logger log.Logger // An absent field is resolved through RefsInCommitmentBranches(); the file is synced // snapshot metadata and must not be rewritten. logger.Info("erigondb settings", "step_size", settings.StepSize, "steps_in_frozen_file", settings.StepsInFrozenFile, - "references_in_commitment_branches", settings.RefsInCommitmentBranches()) + "references_in_commitment_branches", settings.RefsInCommitmentBranches(), + "trie_variant", settings.TrieVariantName()) return settings, nil } @@ -93,6 +143,12 @@ func ResolveErigonDBSettingsWithRefsDefault(dirs datadir.Dirs, logger log.Logger refs = *refsFirstStart } + var trieVariant *string + if statecfg.ExperimentalBinCommitment { + v := TrieVariantBin + trieVariant = &v + } + preverifiedExists, err := dir.FileExist(filepath.Join(dirs.Snap, datadir.PreverifiedFileName)) if err != nil { return nil, err @@ -100,6 +156,9 @@ func ResolveErigonDBSettingsWithRefsDefault(dirs datadir.Dirs, logger log.Logger // Legacy datadir (Erigon <= 3.3): write legacy settings so erigondb.toml exists on disk. if preverifiedExists { + if statecfg.ExperimentalBinCommitment { + return nil, errors.New("--experimental.bin-commitment: this datadir already has hex commitment state; the bin trie needs a fresh datadir") + } settings := &ErigonDBSettings{ StepSize: config3.LegacyStepSize, StepsInFrozenFile: config3.LegacyStepsInFrozenFile, @@ -119,12 +178,17 @@ func ResolveErigonDBSettingsWithRefsDefault(dirs datadir.Dirs, logger log.Logger StepSize: config3.DefaultStepSize, StepsInFrozenFile: config3.DefaultStepsInFrozenFile, ReferencesInCommitmentBranches: &refs, + TrieVariant: trieVariant, + } + if err := reconcileTrieVariant(settings, logger); err != nil { + return nil, err } if noDownloader { // No downloader to provide the real file — write defaults to disk now. logger.Info("Initializing erigondb.toml with DEFAULT settings (nodownloader)", "step_size", settings.StepSize, "steps_in_frozen_file", settings.StepsInFrozenFile, - "references_in_commitment_branches", settings.RefsInCommitmentBranches()) + "references_in_commitment_branches", settings.RefsInCommitmentBranches(), + "trie_variant", settings.TrieVariantName()) if err := writeErigonDBSettings(settingsPath, settings); err != nil { return nil, err } diff --git a/db/state/execctx/commitment_flag_test.go b/db/state/execctx/commitment_flag_test.go index ef11b126c63..4fbf60d5e78 100644 --- a/db/state/execctx/commitment_flag_test.go +++ b/db/state/execctx/commitment_flag_test.go @@ -122,6 +122,19 @@ func TestPickTrieVariant_StreamingFlag(t *testing.T) { require.Equal(t, commitment.VariantParallelHexPatricia, execctx.PickTrieVariant()) } +func TestPickTrieVariant_BinFlag(t *testing.T) { + // No t.Parallel: mutates process-global statecfg flags. + origBin := statecfg.ExperimentalBinCommitment + t.Cleanup(func() { statecfg.ExperimentalBinCommitment = origBin }) + + statecfg.ExperimentalBinCommitment = true + require.Equal(t, commitment.VariantBinPatriciaTrie, execctx.PickTrieVariant()) + + // Bin is a persisted datadir property, so it wins over the runtime experiments. + withCommitmentFlag(t, commitment.VariantStreamingHexPatricia) + require.Equal(t, commitment.VariantBinPatriciaTrie, execctx.PickTrieVariant()) +} + func TestSharedDomains_StreamingFlag_RootEquivalence(t *testing.T) { if testing.Short() { t.Skip() diff --git a/db/state/execctx/domain_shared.go b/db/state/execctx/domain_shared.go index 398c2068ba2..4552f38a704 100644 --- a/db/state/execctx/domain_shared.go +++ b/db/state/execctx/domain_shared.go @@ -176,8 +176,12 @@ type SharedDomains struct { func PickTrieVariant() commitment.TrieVariant { switch { // Selecting more than one experimental-commitment flag is a misconfiguration; - // they are alternative paths. Streaming overlaps folding with execution, so it + // they are alternative paths. Bin is a persisted whole-datadir property, so + // it wins over the runtime experiments (the settings resolver refuses the + // combination outright); streaming overlaps folding with execution, so it // wins over parallel. + case statecfg.ExperimentalBinCommitment: + return commitment.VariantBinPatriciaTrie case statecfg.ExperimentalStreamingCommitment: return commitment.VariantStreamingHexPatricia case statecfg.ExperimentalParallelCommitment: diff --git a/db/state/pbin_variant_persist_test.go b/db/state/pbin_variant_persist_test.go new file mode 100644 index 00000000000..e2680b151c5 --- /dev/null +++ b/db/state/pbin_variant_persist_test.go @@ -0,0 +1,186 @@ +// Copyright 2026 The Erigon Authors +// This file is part of Erigon. +// +// Erigon is free software: you can redistribute it and/or modify +// it under the terms of the GNU Lesser General Public License as published by +// the Free Software Foundation, either version 3 of the License, or +// (at your option) any later version. +// +// Erigon is distributed in the hope that it will be useful, +// but WITHOUT ANY WARRANTY; without even the implied warranty of +// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +// GNU Lesser General Public License for more details. +// +// You should have received a copy of the GNU Lesser General Public License +// along with Erigon. If not, see . + +package state + +import ( + "os" + "path/filepath" + "testing" + + "github.com/stretchr/testify/require" + + "github.com/erigontech/erigon/common/log/v3" + "github.com/erigontech/erigon/db/datadir" + "github.com/erigontech/erigon/db/state/execctx" + "github.com/erigontech/erigon/db/state/statecfg" + "github.com/erigontech/erigon/execution/commitment" +) + +// The tests below mutate process-wide statecfg flags, so none of them may run +// in parallel; save/restore keeps the rest of the package unaffected. +func withVariantFlags(t *testing.T, bin, streaming, parallel bool) { + t.Helper() + origBin := statecfg.ExperimentalBinCommitment + origStream := statecfg.ExperimentalStreamingCommitment + origPar := statecfg.ExperimentalParallelCommitment + t.Cleanup(func() { + statecfg.ExperimentalBinCommitment = origBin + statecfg.ExperimentalStreamingCommitment = origStream + statecfg.ExperimentalParallelCommitment = origPar + }) + statecfg.ExperimentalBinCommitment = bin + statecfg.ExperimentalStreamingCommitment = streaming + statecfg.ExperimentalParallelCommitment = parallel +} + +func writeToml(t *testing.T, dirs datadir.Dirs, content string) string { + t.Helper() + path := filepath.Join(dirs.Snap, ERIGONDB_SETTINGS_FILE) + require.NoError(t, os.WriteFile(path, []byte(content), 0644)) + return path +} + +func TestPBinVariantFirstStartPersistsBin(t *testing.T) { + withVariantFlags(t, true, false, false) + dirs := datadir.New(t.TempDir()) + + settings, err := ResolveErigonDBSettings(dirs, log.New(), true) + require.NoError(t, err) + require.Equal(t, TrieVariantBin, settings.TrieVariantName()) + + written, err := readErigonDBSettings(filepath.Join(dirs.Snap, ERIGONDB_SETTINGS_FILE)) + require.NoError(t, err) + require.NotNil(t, written.TrieVariant) + require.Equal(t, TrieVariantBin, *written.TrieVariant) +} + +func TestPBinVariantHexFirstStartWritesNoVariantKey(t *testing.T) { + withVariantFlags(t, false, false, false) + dirs := datadir.New(t.TempDir()) + + settings, err := ResolveErigonDBSettings(dirs, log.New(), true) + require.NoError(t, err) + require.Equal(t, TrieVariantHex, settings.TrieVariantName()) + + raw, err := os.ReadFile(filepath.Join(dirs.Snap, ERIGONDB_SETTINGS_FILE)) + require.NoError(t, err) + require.NotContains(t, string(raw), "trie_variant") +} + +func TestPBinVariantFlaglessRestartStaysBin(t *testing.T) { + withVariantFlags(t, true, false, false) + dirs := datadir.New(t.TempDir()) + _, err := ResolveErigonDBSettings(dirs, log.New(), true) + require.NoError(t, err) + + // Flagless restart: the persisted trie_variant wins over the CLI default + // and is adopted process-wide. + statecfg.ExperimentalBinCommitment = false + settings, err := ResolveErigonDBSettings(dirs, log.New(), true) + require.NoError(t, err) + require.Equal(t, TrieVariantBin, settings.TrieVariantName()) + require.True(t, statecfg.ExperimentalBinCommitment) + require.Equal(t, commitment.VariantBinPatriciaTrie, execctx.PickTrieVariant()) +} + +func TestPBinVariantHexDatadirRefusesBinFlag(t *testing.T) { + withVariantFlags(t, true, false, false) + + for name, content := range map[string]string{ + "absent_field": "step_size = 100\nsteps_in_frozen_file = 8\n", + "explicit_hex": "step_size = 100\nsteps_in_frozen_file = 8\ntrie_variant = \"hex\"\n", + } { + t.Run(name, func(t *testing.T) { + dirs := datadir.New(t.TempDir()) + writeToml(t, dirs, content) + _, err := ResolveErigonDBSettings(dirs, log.New(), false) + require.Error(t, err) + }) + } +} + +func TestPBinVariantBinDatadirRefusesStreamingAndParallel(t *testing.T) { + const binToml = "step_size = 100\nsteps_in_frozen_file = 8\ntrie_variant = \"bin\"\n" + + t.Run("streaming", func(t *testing.T) { + withVariantFlags(t, false, true, false) + dirs := datadir.New(t.TempDir()) + writeToml(t, dirs, binToml) + _, err := ResolveErigonDBSettings(dirs, log.New(), false) + require.Error(t, err) + }) + t.Run("parallel", func(t *testing.T) { + withVariantFlags(t, false, false, true) + dirs := datadir.New(t.TempDir()) + writeToml(t, dirs, binToml) + _, err := ResolveErigonDBSettings(dirs, log.New(), false) + require.Error(t, err) + }) +} + +func TestPBinVariantRefusesReferences(t *testing.T) { + t.Run("persisted", func(t *testing.T) { + withVariantFlags(t, false, false, false) + dirs := datadir.New(t.TempDir()) + writeToml(t, dirs, "step_size = 100\nsteps_in_frozen_file = 8\nreferences_in_commitment_branches = true\ntrie_variant = \"bin\"\n") + _, err := ResolveErigonDBSettings(dirs, log.New(), false) + require.Error(t, err) + }) + t.Run("first_start", func(t *testing.T) { + withVariantFlags(t, true, false, false) + dirs := datadir.New(t.TempDir()) + refs := true + _, err := ResolveErigonDBSettingsWithRefsDefault(dirs, log.New(), true, &refs) + require.Error(t, err) + }) +} + +func TestPBinVariantLegacyDatadirRefusesBin(t *testing.T) { + withVariantFlags(t, true, false, false) + dirs := datadir.New(t.TempDir()) + require.NoError(t, os.WriteFile(filepath.Join(dirs.Snap, datadir.PreverifiedFileName), []byte(""), 0644)) + + _, err := ResolveErigonDBSettings(dirs, log.New(), false) + require.Error(t, err) +} + +func TestPBinVariantUnknownVariantRefused(t *testing.T) { + withVariantFlags(t, false, false, false) + dirs := datadir.New(t.TempDir()) + writeToml(t, dirs, "step_size = 100\nsteps_in_frozen_file = 8\ntrie_variant = \"verkle\"\n") + + _, err := ResolveErigonDBSettings(dirs, log.New(), false) + require.Error(t, err) +} + +func TestPBinVariantFreshWithDownloaderCarriesBinWithoutWrite(t *testing.T) { + withVariantFlags(t, true, false, false) + dirs := datadir.New(t.TempDir()) + + settings, err := ResolveErigonDBSettings(dirs, log.New(), false) + require.NoError(t, err) + require.Equal(t, TrieVariantBin, settings.TrieVariantName()) + + _, err = os.Stat(filepath.Join(dirs.Snap, ERIGONDB_SETTINGS_FILE)) + require.True(t, os.IsNotExist(err), "fresh+downloader must leave erigondb.toml for the downloader") + + // A later downloader-delivered hex toml must be refused under the bin + // process, not silently adopted. + writeToml(t, dirs, "step_size = 100\nsteps_in_frozen_file = 8\n") + _, err = ResolveErigonDBSettings(dirs, log.New(), false) + require.Error(t, err) +} diff --git a/db/state/squeeze.go b/db/state/squeeze.go index 4a58b274685..566dbec47d7 100644 --- a/db/state/squeeze.go +++ b/db/state/squeeze.go @@ -33,7 +33,6 @@ import ( "github.com/erigontech/erigon/db/seg" downloadertype "github.com/erigontech/erigon/db/snaptype" "github.com/erigontech/erigon/db/state/execctx" - "github.com/erigontech/erigon/db/state/statecfg" "github.com/erigontech/erigon/execution/commitment" "github.com/erigontech/erigon/execution/commitment/commitmentdb" "github.com/erigontech/erigon/execution/stagedsync/stages" @@ -1018,15 +1017,7 @@ func RebuildCommitmentFiles(ctx context.Context, rwDb kv.TemporalRwDB, txNumsRea } roTx.Rollback() - streaming := statecfg.ExperimentalStreamingCommitment - parallel := statecfg.ExperimentalParallelCommitment - trieVariant := commitment.VariantHexPatriciaTrie - switch { - case streaming: - trieVariant = commitment.VariantStreamingHexPatricia - case parallel: - trieVariant = commitment.VariantParallelHexPatricia - } + trieVariant := execctx.PickTrieVariant() for shardFrom < lastShard { // recreate this file range 1+ steps nextKey := func() (ok bool, k []byte) { @@ -1061,7 +1052,7 @@ func RebuildCommitmentFiles(ctx context.Context, rwDb kv.TemporalRwDB, txNumsRea domains.SetTxNum(lastTxnumInShard - 1) currentTxNum := lastTxnumInShard - 1 domains.GetCommitmentCtx().SetStateReader(commitmentdb.NewFilesOnlyStateReader(rwTx, lastTxnumInShard-1)) - if parallel || streaming { + if trieVariant == commitment.VariantParallelHexPatricia || trieVariant == commitment.VariantStreamingHexPatricia { domains.EnableParaTrieDB(rwDb) } diff --git a/db/state/statecfg/state_schema.go b/db/state/statecfg/state_schema.go index 2d4ec5894af..814a01d149a 100644 --- a/db/state/statecfg/state_schema.go +++ b/db/state/statecfg/state_schema.go @@ -207,6 +207,12 @@ var ExperimentalParallelCommitment = dbg.EnvBool("COMMITMENT_PARALLEL", false) // ExperimentalParallelCommitment. var ExperimentalStreamingCommitment = false +// ExperimentalBinCommitment selects the EIP-8297 binary commitment trie +// (commitment.ModeDirect + VariantBinPatriciaTrie). A whole-datadir property: +// persisted to erigondb.toml on first start and adopted from it on later +// starts, so a flagless restart of a bin datadir stays bin. +var ExperimentalBinCommitment = dbg.EnvBool("COMMITMENT_BIN", false) + var Schema = SchemaGen{ AccountsDomain: DomainCfg{ Name: kv.AccountsDomain, ValuesTable: kv.TblAccountVals, diff --git a/docs/plans/20260730-pbin-m1-local-el.md b/docs/plans/20260730-pbin-m1-local-el.md index acff1d48d0a..4501e0927ae 100644 --- a/docs/plans/20260730-pbin-m1-local-el.md +++ b/docs/plans/20260730-pbin-m1-local-el.md @@ -115,7 +115,7 @@ Blocking items needing a human or upstream answer. Do not proceed past the task - **Q2 — is the tree a pure function of current state, or of history?** (H8.) eip-8297 is silent. Determines whether recompute-from-domains is a valid oracle at all, hence whether the M1a gate means anything. Possibly an upstream spec question. - **Q3 — does the reference create a present-zero leaf on `SSTORE 0` to a virgin slot?** Erigon drops it at `execution/state/state_object.go:245-246` before any writer sees it. If yes, that guard must be variant-gated and every zero-SSTORE becomes a state write. - **Q4 — confirm `github.com/zeebo/blake3`** as a **test-only** dependency (lower stakes now that production stays Keccak; the in-graph `lukechampine.com/blake3` may simply be enough). Its measured advantage was 13–18% arm64 / 58–78% amd64, not re-verified. **Answered (Task 1):** `zeebo` not added — production stays Keccak so BLAKE3 speed is irrelevant, and `lukechampine.com/blake3` is already a direct dependency used by `pbin_specroots_test.go`; it now backs `pbinBlake3Hash` for all three seam tests. -- **Q5 — forward/backward compatibility of a new `erigondb.toml` key** across erigon binaries, given the file is downloader-delivered and **wins over the CLI** (`erigondb_settings.go:74-89`). +- **Q5 — forward/backward compatibility of a new `erigondb.toml` key** across erigon binaries, given the file is downloader-delivered and **wins over the CLI** (`erigondb_settings.go:74-89`). **Answered (Task 6):** `readErigonDBSettings` uses `go-toml/v2` `Unmarshal`, which ignores unknown keys — older binaries parse a `trie_variant` toml fine. The key is written only when bin, so published/downloader tomls stay byte-identical, and a downloader-delivered hex toml under a bin process is refused at resolve. Residual risk: a binary **predating the key** opens a bin datadir as hex with no guard — inherent to any new key; acceptable while bin is experimental and fresh-datadir-only. ## Thin / Unverified @@ -217,18 +217,18 @@ Production keeps Keccak-256. This task only makes the **test** path run the whol - Modify: `cmd/integration/commands/flags.go` - Create: `db/state/pbin_variant_persist_test.go` -- [ ] write a failing test asserting a datadir created with the bin variant is **refused** when opened with a conflicting config (guards H4) -- [ ] write a failing test asserting `references_in_commitment_branches = true` is refused under the bin variant (guards H10) -- [ ] add a `statecfg` global for the variant -- [ ] add the `--experimental.bin-commitment` flag across the 7-site experimental-commitment template -- [ ] replace the duplicated inline switch at `squeeze.go:1023-1029` with `PickTrieVariant()` -- [ ] add `trie_variant` to `ErigonDBSettings`, resolved first-start exactly as `ReferencesInCommitmentBranches` is, and note in a comment that `erigondb.toml` wins over the CLI -- [ ] write a failing test asserting the header state-root comparison is enforced by default and skipped only when the new toggle is set — under **both** variants, since the toggle is variant-independent -- [ ] add the third case to `PickTrieVariant()` reachable via `--experimental.bin-commitment` -- [ ] add a root-check toggle to `common/dbg/experiments.go` following the `DiscardCommitment` `EnvBool` pattern, **defaulting to check-enabled**, and honour it at all five sites: `exec3.go:810`, `exec3.go:730`, `exec3_serial.go:205`, `committer.go:557`, `:659`, `:764` -- [ ] log loudly once at startup when the check is disabled, so a running node says so out loud (guards H13) -- [ ] write a test asserting a flagless restart of a bin datadir stays bin -- [ ] run tests — must pass before task 7 +- [x] write a failing test asserting a datadir created with the bin variant is **refused** when opened with a conflicting config (guards H4) — `db/state/pbin_variant_persist_test.go`: bin datadir + streaming/parallel flags refused; hex datadir (absent or explicit `trie_variant`) + bin flag refused; a downloader-delivered hex toml under an in-memory-bin process refused; legacy datadir (preverified.toml) + bin flag refused +- [x] write a failing test asserting `references_in_commitment_branches = true` is refused under the bin variant (guards H10) — `TestPBinVariantRefusesReferences`: both the persisted refs=true+bin toml and the first-start refs-override+bin combination error +- [x] add a `statecfg` global for the variant — `statecfg.ExperimentalBinCommitment` (`COMMITMENT_BIN` env, mirroring `COMMITMENT_PARALLEL`) +- [x] add the `--experimental.bin-commitment` flag across the 7-site experimental-commitment template — flag def + ctx→cfg (`cmd/utils/flags.go`), `node/cli/default_flags.go`, `ethconfig.Config` field, cfg→statecfg (`node/eth/backend.go`), `cmd/integration/commands/flags.go`, statecfg global +- [x] replace the duplicated inline switch at `squeeze.go:1023-1029` with `PickTrieVariant()` — the `EnableParaTrieDB` gate below it now derives from the returned variant instead of the raw flags +- [x] add `trie_variant` to `ErigonDBSettings`, resolved first-start exactly as `ReferencesInCommitmentBranches` is, and note in a comment that `erigondb.toml` wins over the CLI — `*string` ("hex"/"bin", absent = hex), written only when bin so published tomls stay unchanged; `reconcileTrieVariant` runs at every resolve: a persisted bin adopts bin process-wide (sets the statecfg global), all conflicts refuse rather than degrade +- [x] write a failing test asserting the header state-root comparison is enforced by default and skipped only when the new toggle is set — under **both** variants, since the toggle is variant-independent — `TestHeaderRootCheckDefaultOnAndTogglable` drives `headerRootMismatch` under both settings of the bin global +- [x] add the third case to `PickTrieVariant()` reachable via `--experimental.bin-commitment` — bin wins over streaming/parallel (the resolver refuses the combination anyway); `TestPickTrieVariant_BinFlag` +- [x] add a root-check toggle to `common/dbg/experiments.go` following the `DiscardCommitment` `EnvBool` pattern, **defaulting to check-enabled**, and honour it at all five sites: `exec3.go:810`, `exec3.go:730`, `exec3_serial.go:205`, `committer.go:557`, `:659`, `:764` — `dbg.CheckHeaderStateRoot` (`CHECK_HEADER_STATE_ROOT`, default true), applied via a shared `headerRootMismatch` helper at all five comparisons; `handleIncorrectRootHashError` (the `:730` arm) is only reachable from gated comparisons +- [x] log loudly once at startup when the check is disabled, so a running node says so out loud (guards H13) — `backend.go` Warn at node construction +- [x] write a test asserting a flagless restart of a bin datadir stays bin — `TestPBinVariantFlaglessRestartStaysBin`: persisted bin re-adopts with the global off, `PickTrieVariant()` returns bin +- [x] run tests — `go test ./execution/commitment/... ./db/state/... -count=1` and `./execution/stagedsync/... -short` green, `make lint` clean twice ### Task 7: Un-pin the genesis variant diff --git a/execution/stagedsync/committer.go b/execution/stagedsync/committer.go index 1cddb43b005..b51dcb6ffd4 100644 --- a/execution/stagedsync/committer.go +++ b/execution/stagedsync/committer.go @@ -1,7 +1,6 @@ package stagedsync import ( - "bytes" "context" "errors" "fmt" @@ -552,7 +551,7 @@ func (cc *commitmentCalculator) computeBlockFromBAL(ctx context.Context, pb *pen cc.fail(ctx, br, fmt.Errorf("BAL-driven compute-ahead block %d: %w", req.blockNum, err)) return } - if !bytes.Equal(rh, req.stateRoot[:]) { + if headerRootMismatch(rh, req.stateRoot[:]) { cc.fail(ctx, br, fmt.Errorf("%w: BAL-driven block %d root %x expected %x", ErrWrongTrieRoot, req.blockNum, rh, req.stateRoot)) return @@ -654,7 +653,7 @@ func (cc *commitmentCalculator) shadowCrossCheck(ctx context.Context, r *blockRe cc.fail(ctx, r, fmt.Errorf("shadow incremental compute: %w", err)) return } - if !bytes.Equal(rh, balRoot) { + if headerRootMismatch(rh, balRoot) { cc.fail(ctx, r, fmt.Errorf("%w: shadow mismatch block %d incremental %x BAL-driven %x", ErrWrongTrieRoot, r.BlockNum, rh, balRoot)) return @@ -761,7 +760,7 @@ func (cc *commitmentCalculator) compute(ctx context.Context, t commitTarget, m c if !m.checkRoot { return } - mismatch := !bytes.Equal(rh, t.stateRoot[:]) + mismatch := headerRootMismatch(rh, t.stateRoot[:]) if !m.publishRoot && !mismatch { return } diff --git a/execution/stagedsync/exec3.go b/execution/stagedsync/exec3.go index c8250d4ef43..91c12459fa3 100644 --- a/execution/stagedsync/exec3.go +++ b/execution/stagedsync/exec3.go @@ -725,6 +725,14 @@ func (te *txExecutor) executeBlocks(ctx context.Context, startBlockNum uint64, m return nil } +// headerRootMismatch reports whether a computed state root fails the header +// state-root check. Variant-independent and on by default; +// dbg.CheckHeaderStateRoot switches the check off for a chain whose headers +// this node cannot reproduce. +func headerRootMismatch(computed, expected []byte) bool { + return dbg.CheckHeaderStateRoot && !bytes.Equal(computed, expected) +} + func handleIncorrectRootHashError(blockNumber uint64, blockHash common.Hash, applyTx kv.TemporalRwTx, cfg ExecuteBlockCfg, s *StageState, logger log.Logger, u Unwinder) error { if cfg.badBlockHalt { return fmt.Errorf("%w, block=%d", ErrWrongTrieRoot, blockNumber) @@ -807,7 +815,7 @@ func computeAndCheckCommitmentV3(ctx context.Context, header *types.Header, appl return false, times, fmt.Errorf("compute commitment: %w", err) } - if !bytes.Equal(computedRootHash, header.Root[:]) { + if headerRootMismatch(computedRootHash, header.Root[:]) { logger.Warn(fmt.Sprintf("[%s] Wrong trie root of block %d: %x, expected (from header): %x. Block hash: %x", e.LogPrefix(), header.Number.Uint64(), computedRootHash, header.Root[:], header.Hash())) err = handleIncorrectRootHashError(header.Number.Uint64(), header.Hash(), applyTx, cfg, e, logger, u) return false, times, err diff --git a/execution/stagedsync/exec3_serial.go b/execution/stagedsync/exec3_serial.go index 848e97d6bf4..c9f5e1caa9d 100644 --- a/execution/stagedsync/exec3_serial.go +++ b/execution/stagedsync/exec3_serial.go @@ -1,7 +1,6 @@ package stagedsync import ( - "bytes" "context" "errors" "fmt" @@ -200,7 +199,7 @@ func (se *serialExecutor) exec(ctx context.Context, execStage *StageState, u Unw } se.doms.SetChangesetAccumulator(nil) - if !bytes.Equal(rh, header.Root[:]) { + if headerRootMismatch(rh, header.Root[:]) { se.logger.Error(fmt.Sprintf("[%s] Wrong trie root of block %d: %x, expected (from header): %x. Block hash: %x", se.logPrefix, header.Number.Uint64(), rh, header.Root[:], header.Hash())) return b.HeaderNoCopy(), rwTx, fmt.Errorf("%w, block=%d", ErrWrongTrieRoot, blockNum) } diff --git a/execution/stagedsync/header_root_check_test.go b/execution/stagedsync/header_root_check_test.go new file mode 100644 index 00000000000..7b84f4d202b --- /dev/null +++ b/execution/stagedsync/header_root_check_test.go @@ -0,0 +1,54 @@ +// Copyright 2026 The Erigon Authors +// This file is part of Erigon. +// +// Erigon is free software: you can redistribute it and/or modify +// it under the terms of the GNU Lesser General Public License as published by +// the Free Software Foundation, either version 3 of the License, or +// (at your option) any later version. +// +// Erigon is distributed in the hope that it will be useful, +// but WITHOUT ANY WARRANTY; without even the implied warranty of +// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +// GNU Lesser General Public License for more details. +// +// You should have received a copy of the GNU Lesser General Public License +// along with Erigon. If not, see . + +package stagedsync + +import ( + "testing" + + "github.com/stretchr/testify/require" + + "github.com/erigontech/erigon/common/dbg" + "github.com/erigontech/erigon/db/state/statecfg" +) + +// The header state-root check must be on by default and skippable only through +// dbg.CheckHeaderStateRoot, independently of the commitment trie variant. +func TestHeaderRootCheckDefaultOnAndTogglable(t *testing.T) { + computed := make([]byte, 32) + expected := make([]byte, 32) + expected[0] = 0x01 + + require.True(t, dbg.CheckHeaderStateRoot, "header root check must default to enabled") + + origBin := statecfg.ExperimentalBinCommitment + origCheck := dbg.CheckHeaderStateRoot + t.Cleanup(func() { + statecfg.ExperimentalBinCommitment = origBin + dbg.CheckHeaderStateRoot = origCheck + }) + + for _, bin := range []bool{false, true} { + statecfg.ExperimentalBinCommitment = bin + + dbg.CheckHeaderStateRoot = true + require.True(t, headerRootMismatch(computed, expected)) + require.False(t, headerRootMismatch(computed, computed)) + + dbg.CheckHeaderStateRoot = false + require.False(t, headerRootMismatch(computed, expected)) + } +} diff --git a/node/cli/default_flags.go b/node/cli/default_flags.go index 1f8c0e18926..0a8371efcbf 100644 --- a/node/cli/default_flags.go +++ b/node/cli/default_flags.go @@ -268,6 +268,7 @@ var DefaultFlags = []cli.Flag{ &utils.ExperimentalParallelCommitmentFlag, &utils.ExperimentalStreamingCommitmentFlag, + &utils.ExperimentalBinCommitmentFlag, &utils.MCPDisableFlag, &utils.MCPAddrFlag, diff --git a/node/eth/backend.go b/node/eth/backend.go index 4c690a24ca2..f8060499c91 100644 --- a/node/eth/backend.go +++ b/node/eth/backend.go @@ -313,6 +313,9 @@ func New(ctx context.Context, stack *node.Node, config *ethconfig.Config, logger if config.ExperimentalStreamingCommitment { statecfg.ExperimentalStreamingCommitment = true } + if config.ExperimentalBinCommitment { + statecfg.ExperimentalBinCommitment = true + } if err = stages.UpdateMetrics(tx); err != nil { return err @@ -328,6 +331,10 @@ func New(ctx context.Context, stack *node.Node, config *ethconfig.Config, logger return nil, err } + if !dbg.CheckHeaderStateRoot { + logger.Warn("HEADER STATE-ROOT CHECK IS DISABLED (CHECK_HEADER_STATE_ROOT=false): nothing cross-checks execution results against headers; a wrong chain will look healthy") + } + ctx, ctxCancel := context.WithCancel(context.Background()) // kv_remote architecture does blocks on stream.Send - means current architecture require unlimited amount of txs to provide good throughput diff --git a/node/ethconfig/config.go b/node/ethconfig/config.go index 6c795df23a5..d68e60c2de1 100644 --- a/node/ethconfig/config.go +++ b/node/ethconfig/config.go @@ -325,6 +325,7 @@ type Sync struct { KeepExecutionProofs bool ExperimentalParallelCommitment bool ExperimentalStreamingCommitment bool + ExperimentalBinCommitment bool PersistReceiptsCacheV2 bool SnapshotDownloadToBlock uint64 // exclusive [0,toBlock) } From 57f4ba5eb4865917375beaded458c445a664f9f6 Mon Sep 17 00:00:00 2001 From: awskii Date: Thu, 30 Jul 2026 18:39:55 +0700 Subject: [PATCH 30/56] db/state/execctx, execution/state/genesiswrite, rpc, db/integrity: keep the bin variant at genesis, refuse it on hex-only paths --- db/integrity/commitment_integrity.go | 8 +- db/integrity/pbin_hex_only_test.go | 52 +++++++++ db/state/execctx/domain_shared.go | 7 +- db/state/execctx/options.go | 38 +++++-- db/state/execctx/pbin_options_test.go | 101 ++++++++++++++++++ docs/plans/20260730-pbin-m1-local-el.md | 12 +-- execution/state/genesiswrite/genesis_write.go | 5 +- .../state/genesiswrite/pbin_genesis_test.go | 92 ++++++++++++++++ rpc/jsonrpc/debug_execution_witness.go | 2 +- rpc/jsonrpc/eth_call.go | 4 +- rpc/jsonrpc/eth_simulation.go | 2 +- rpc/jsonrpc/pbin_hex_only_test.go | 64 +++++++++++ rpc/jsonrpc/receipts/receipts_generator.go | 4 +- rpc/rpchelper/commitment.go | 2 +- rpc/rpchelper/pbin_commitment_test.go | 50 +++++++++ 15 files changed, 416 insertions(+), 27 deletions(-) create mode 100644 db/integrity/pbin_hex_only_test.go create mode 100644 db/state/execctx/pbin_options_test.go create mode 100644 execution/state/genesiswrite/pbin_genesis_test.go create mode 100644 rpc/jsonrpc/pbin_hex_only_test.go create mode 100644 rpc/rpchelper/pbin_commitment_test.go diff --git a/db/integrity/commitment_integrity.go b/db/integrity/commitment_integrity.go index d5496ec79c2..8212bad90f1 100644 --- a/db/integrity/commitment_integrity.go +++ b/db/integrity/commitment_integrity.go @@ -212,7 +212,7 @@ func checkCommitmentRootViaFileData(ctx context.Context, tx kv.TemporalTx, br db func checkCommitmentRootViaSd(ctx context.Context, tx kv.TemporalTx, f state.VisibleFile, info commitmentRootInfo, logger log.Logger) (*execctx.SharedDomains, error) { maxTxNum := f.EndRootNum() - 1 - sd, err := execctx.NewSharedDomains(ctx, tx, logger, execctx.WithSequentialCommitment()) + sd, err := execctx.NewSharedDomains(ctx, tx, logger, execctx.WithHexCommitmentOnly()) if err != nil { return nil, err } @@ -1109,7 +1109,7 @@ func CheckCommitmentHistAtBlk(ctx context.Context, db kv.TemporalRoDB, br dbserv return err } defer tx.Rollback() - sd, err := execctx.NewSharedDomains(ctx, tx, logger, execctx.WithoutDeferredBranchUpdates(), execctx.WithSequentialCommitment()) + sd, err := execctx.NewSharedDomains(ctx, tx, logger, execctx.WithoutDeferredBranchUpdates(), execctx.WithHexCommitmentOnly()) if err != nil { return err } @@ -1177,7 +1177,7 @@ func CheckCommitmentHistAtBlkRange(ctx context.Context, sc SamplerCfg, db kv.Tem return err } defer tx.Rollback() - sd, err := execctx.NewSharedDomains(wCtx, tx, logger, execctx.WithoutDeferredBranchUpdates(), execctx.WithSequentialCommitment()) + sd, err := execctx.NewSharedDomains(wCtx, tx, logger, execctx.WithoutDeferredBranchUpdates(), execctx.WithHexCommitmentOnly()) if err != nil { return err } @@ -1191,7 +1191,7 @@ func CheckCommitmentHistAtBlkRange(ctx context.Context, sc SamplerCfg, db kv.Tem for blockNum := range sampler.BlockNums(windowStart, windowEnd) { // Fresh SharedDomains per block: an SD is committed-or-closed, // never reset in place. - sd, err := execctx.NewSharedDomains(wCtx, tx, logger, execctx.WithoutDeferredBranchUpdates()) + sd, err := execctx.NewSharedDomains(wCtx, tx, logger, execctx.WithoutDeferredBranchUpdates(), execctx.WithHexCommitmentOnly()) if err != nil { return err } diff --git a/db/integrity/pbin_hex_only_test.go b/db/integrity/pbin_hex_only_test.go new file mode 100644 index 00000000000..8855b943c19 --- /dev/null +++ b/db/integrity/pbin_hex_only_test.go @@ -0,0 +1,52 @@ +// Copyright 2026 The Erigon Authors +// This file is part of Erigon. +// +// Erigon is free software: you can redistribute it and/or modify +// it under the terms of the GNU Lesser General Public License as published by +// the Free Software Foundation, either version 3 of the License, or +// (at your option) any later version. +// +// Erigon is distributed in the hope that it will be useful, +// but WITHOUT ANY WARRANTY; without even the implied warranty of +// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +// GNU Lesser General Public License for more details. +// +// You should have received a copy of the GNU Lesser General Public License +// along with Erigon. If not, see . + +package integrity + +import ( + "testing" + + "github.com/stretchr/testify/require" + + "github.com/erigontech/erigon/common/log/v3" + "github.com/erigontech/erigon/db/datadir" + "github.com/erigontech/erigon/db/kv/temporal/temporaltest" + "github.com/erigontech/erigon/db/state/execctx" + "github.com/erigontech/erigon/db/state/statecfg" +) + +func withBinCommitment(t *testing.T, on bool) { + t.Helper() + orig := statecfg.ExperimentalBinCommitment + t.Cleanup(func() { statecfg.ExperimentalBinCommitment = orig }) + statecfg.ExperimentalBinCommitment = on +} + +// The history checks recompute roots with the hex trie, so on a bin datadir they +// must refuse rather than report a mismatch against correct bin records. +func TestPBinCommitmentHistChecksRefuseBin(t *testing.T) { + // No t.Parallel: mutates process-global statecfg flags. + db := temporaltest.NewTestDB(t, datadir.New(t.TempDir())) + withBinCommitment(t, true) + + err := CheckCommitmentHistAtBlk(t.Context(), db, nil, 1, log.LvlInfo, log.New()) + require.ErrorIs(t, err, execctx.ErrBinCommitmentUnsupported) + + sc, err := NewSamplerCfg(1, 1.0) + require.NoError(t, err) + err = CheckCommitmentHistAtBlkRange(t.Context(), sc, db, nil, 0, 1, log.New()) + require.ErrorIs(t, err, execctx.ErrBinCommitmentUnsupported) +} diff --git a/db/state/execctx/domain_shared.go b/db/state/execctx/domain_shared.go index 4552f38a704..dd911b9aa3c 100644 --- a/db/state/execctx/domain_shared.go +++ b/db/state/execctx/domain_shared.go @@ -199,9 +199,12 @@ func NewSharedDomains(ctx context.Context, tx kv.TemporalTx, logger log.Logger, for _, opt := range opts { opt(&o) } - // Bit-path branch keys collide in the cache's hex-shaped trunk slots; the - // commitment-context ctor refuses a bin SD that shares the cache. if o.trieCfg.Variant == commitment.VariantBinPatriciaTrie { + if o.hexCommitmentOnly { + return nil, ErrBinCommitmentUnsupported + } + // Bit-path branch keys collide in the cache's hex-shaped trunk slots; the + // commitment-context ctor refuses a bin SD that shares the cache. WithoutSharedBranchCache()(&o) } trieCfg := o.trieCfg diff --git a/db/state/execctx/options.go b/db/state/execctx/options.go index 853ad487c54..fe7cc7a9280 100644 --- a/db/state/execctx/options.go +++ b/db/state/execctx/options.go @@ -16,11 +16,20 @@ package execctx -import "github.com/erigontech/erigon/execution/commitment" +import ( + "errors" + + "github.com/erigontech/erigon/execution/commitment" +) + +// ErrBinCommitmentUnsupported is returned by NewSharedDomains for a caller that +// declared itself hex-only (WithHexCommitmentOnly) over a bin-variant datadir. +var ErrBinCommitmentUnsupported = errors.New("this code path supports the hex commitment trie only, and the datadir uses the bin trie") type sharedDomainOptions struct { trieCfg commitment.TrieConfig useSharedBranchCache bool + hexCommitmentOnly bool } // SharedDomainOption configures NewSharedDomains. @@ -41,9 +50,26 @@ func WithoutSharedBranchCache() SharedDomainOption { return func(o *sharedDomainOptions) { o.useSharedBranchCache = false } } -// WithSequentialCommitment forces the sequential HexPatriciaHashed trie regardless -// of the experimental parallel/concurrent flags — for one-shot / empty-DB paths -// (e.g. genesis) that wire no trie-context factory for the parallel trie. -func WithSequentialCommitment() SharedDomainOption { - return func(o *sharedDomainOptions) { o.trieCfg.Variant = commitment.VariantHexPatriciaTrie } +// WithoutParallelCommitment demotes the experimental parallel/streaming tries to the +// sequential HexPatriciaHashed — for one-shot / empty-DB paths (e.g. genesis) that +// wire no trie-context factory for the parallel trie. The bin variant is a persisted +// whole-datadir property and stays bin: demoting it would compute a hex root over a +// datadir the executor reads as bin. +func WithoutParallelCommitment() SharedDomainOption { + return func(o *sharedDomainOptions) { + if o.trieCfg.Variant != commitment.VariantBinPatriciaTrie { + o.trieCfg.Variant = commitment.VariantHexPatriciaTrie + } + } +} + +// WithHexCommitmentOnly is WithoutParallelCommitment for callers that can only read +// hex branch records — witness, eth_getProof, eth_simulateV1, receipt regeneration, +// commitment integrity. Under the bin variant NewSharedDomains returns +// ErrBinCommitmentUnsupported instead of reading bit-path records as hex ones. +func WithHexCommitmentOnly() SharedDomainOption { + return func(o *sharedDomainOptions) { + o.hexCommitmentOnly = true + WithoutParallelCommitment()(o) + } } diff --git a/db/state/execctx/pbin_options_test.go b/db/state/execctx/pbin_options_test.go new file mode 100644 index 00000000000..0ed5d9690cb --- /dev/null +++ b/db/state/execctx/pbin_options_test.go @@ -0,0 +1,101 @@ +// Copyright 2026 The Erigon Authors +// This file is part of Erigon. +// +// Erigon is free software: you can redistribute it and/or modify +// it under the terms of the GNU Lesser General Public License as published by +// the Free Software Foundation, either version 3 of the License, or +// (at your option) any later version. +// +// Erigon is distributed in the hope that it will be useful, +// but WITHOUT ANY WARRANTY; without even the implied warranty of +// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +// GNU Lesser General Public License for more details. +// +// You should have received a copy of the GNU Lesser General Public License +// along with Erigon. If not, see . + +package execctx_test + +import ( + "testing" + + "github.com/stretchr/testify/require" + + "github.com/erigontech/erigon/common/log/v3" + "github.com/erigontech/erigon/db/state/execctx" + "github.com/erigontech/erigon/db/state/statecfg" + "github.com/erigontech/erigon/execution/commitment" +) + +func withBinCommitmentFlag(t *testing.T, on bool) { + t.Helper() + orig := statecfg.ExperimentalBinCommitment + t.Cleanup(func() { statecfg.ExperimentalBinCommitment = orig }) + statecfg.ExperimentalBinCommitment = on +} + +// The genesis-style option demotes only the experimental parallel/streaming tries; +// bin is a persisted datadir property, so demoting it would compute a hex block-0 +// root over a datadir the executor then reads as bin. +func TestPBinWithoutParallelCommitmentKeepsBin(t *testing.T) { + // No t.Parallel: mutates process-global statecfg flags. + for _, tc := range []struct { + name string + flag commitment.TrieVariant + want commitment.TrieVariant + }{ + {"hex", commitment.VariantHexPatriciaTrie, commitment.VariantHexPatriciaTrie}, + {"streaming", commitment.VariantStreamingHexPatricia, commitment.VariantHexPatriciaTrie}, + {"parallel", commitment.VariantParallelHexPatricia, commitment.VariantHexPatriciaTrie}, + {"bin", commitment.VariantBinPatriciaTrie, commitment.VariantBinPatriciaTrie}, + } { + t.Run(tc.name, func(t *testing.T) { + withBinCommitmentFlag(t, tc.flag == commitment.VariantBinPatriciaTrie) + withCommitmentFlag(t, tc.flag) + + db := newTestDb(t, 16) + tx, err := db.BeginTemporalRw(t.Context()) + require.NoError(t, err) + defer tx.Rollback() + + sd, err := execctx.NewSharedDomains(t.Context(), tx, log.New(), execctx.WithoutParallelCommitment()) + require.NoError(t, err) + defer sd.Close() + + require.Equal(t, tc.want, sd.GetCommitmentCtx().Trie().Variant()) + }) + } +} + +// Paths that can only read hex branch records must fail loudly on a bin datadir +// instead of reinterpreting bit-path records as hex ones. +func TestPBinHexOnlyCommitmentRefusesBin(t *testing.T) { + // No t.Parallel: mutates process-global statecfg flags. + withBinCommitmentFlag(t, true) + + db := newTestDb(t, 16) + tx, err := db.BeginTemporalRw(t.Context()) + require.NoError(t, err) + defer tx.Rollback() + + sd, err := execctx.NewSharedDomains(t.Context(), tx, log.New(), execctx.WithHexCommitmentOnly()) + require.ErrorIs(t, err, execctx.ErrBinCommitmentUnsupported) + require.Nil(t, sd) +} + +func TestPBinHexOnlyCommitmentDemotesParallel(t *testing.T) { + // No t.Parallel: mutates process-global statecfg flags. + withBinCommitmentFlag(t, false) + withCommitmentFlag(t, commitment.VariantParallelHexPatricia) + + db := newTestDb(t, 16) + tx, err := db.BeginTemporalRw(t.Context()) + require.NoError(t, err) + defer tx.Rollback() + + sd, err := execctx.NewSharedDomains(t.Context(), tx, log.New(), execctx.WithHexCommitmentOnly()) + require.NoError(t, err) + defer sd.Close() + + require.Equal(t, commitment.VariantHexPatriciaTrie, sd.GetCommitmentCtx().Trie().Variant()) +} diff --git a/docs/plans/20260730-pbin-m1-local-el.md b/docs/plans/20260730-pbin-m1-local-el.md index 4501e0927ae..4240eb2c5b6 100644 --- a/docs/plans/20260730-pbin-m1-local-el.md +++ b/docs/plans/20260730-pbin-m1-local-el.md @@ -238,12 +238,12 @@ Production keeps Keccak-256. This task only makes the **test** path run the whol - Modify: `rpc/rpchelper/commitment.go` - Create: `db/state/execctx/pbin_options_test.go` -- [ ] write a failing test asserting genesis under the bin variant computes a **binary** root, not a hex one -- [ ] add `WithoutParallelCommitment()` that demotes streaming/parallel to hex and leaves bin as bin; keep `WithSequentialCommitment()` as a deprecated alias or migrate all 11 call sites -- [ ] switch `genesis_write.go:381` to the new option -- [ ] make the 10 RPC/integrity sites return an explicit unsupported-variant error rather than silently forcing hex over pbin records -- [ ] write a test asserting each of those paths errors under bin instead of returning a hex root -- [ ] run tests — must pass before task 8 +- [x] write a failing test asserting genesis under the bin variant computes a **binary** root, not a hex one — `TestPBinGenesisComputesBinaryRoot` (➕ `execution/state/genesiswrite/pbin_genesis_test.go`, not in the planned file list), red before: `GenesisToBlock` returned the hex root under bin. Asserts both `bin != hex` and `bin == ` the root of a SharedDomains explicitly running the bin trie over the same alloc. Code-free alloc — code chunking is Task 12/13 +- [x] add `WithoutParallelCommitment()` that demotes streaming/parallel to hex and leaves bin as bin; keep `WithSequentialCommitment()` as a deprecated alias or migrate all 11 call sites — migrated all 11 and removed `WithSequentialCommitment`; no alias, so a new call site has to pick a variant policy deliberately +- [x] switch `genesis_write.go:381` to the new option +- [x] make the 10 RPC/integrity sites return an explicit unsupported-variant error rather than silently forcing hex over pbin records — `WithHexCommitmentOnly()` + `ErrBinCommitmentUnsupported`, refused in `NewSharedDomains` before any domain work. ➕ an 11th site (`commitment_integrity.go:1194`, the per-block SD inside `CheckCommitmentHistAtBlkRange`) carried no variant option at all and got the same gate +- [x] write a test asserting each of those paths errors under bin instead of returning a hex root — functional per-caller tests: `CheckCommitmentHistAtBlk` + `CheckCommitmentHistAtBlkRange` (`db/integrity/pbin_hex_only_test.go`), `ComputeCustomCommitmentFromStateHistory` (`rpc/rpchelper/pbin_commitment_test.go`), `eth_getProof` + `eth_simulateV1` (`rpc/jsonrpc/pbin_hex_only_test.go`). The remaining sites (`getWitness`, `buildWitnessResult`, both receipt-regeneration sites, `checkCommitmentRootViaSd`) reach their SharedDomains only after full block re-execution or over snapshot files, so they are covered by the shared refusal itself, tested directly in `TestPBinHexOnlyCommitmentRefusesBin` +- [x] run tests — `./db/state/... ./execution/commitment/... ./execution/state/genesiswrite ./db/integrity ./rpc/rpchelper ./rpc/jsonrpc/...` green, `make lint` clean twice ### Task 8: Make the silent degradations loud diff --git a/execution/state/genesiswrite/genesis_write.go b/execution/state/genesiswrite/genesis_write.go index fb2ffd14cdf..4b088627bc5 100644 --- a/execution/state/genesiswrite/genesis_write.go +++ b/execution/state/genesiswrite/genesis_write.go @@ -377,8 +377,9 @@ func GenesisToBlock(tb testing.TB, g *types.Genesis, dirs datadir.Dirs, logger l defer tx.Rollback() // Genesis is a one-shot commitment over an empty DB; the parallel trie has no - // context factory wired here, so use the sequential trie (identical root). - sd, err := execctx.NewSharedDomains(ctx, tx, logger, execctx.WithSequentialCommitment()) + // context factory wired here, so demote it to the sequential trie (identical + // root). The bin variant is kept — block 0 must be the root the executor computes. + sd, err := execctx.NewSharedDomains(ctx, tx, logger, execctx.WithoutParallelCommitment()) if err != nil { return nil, nil, err } diff --git a/execution/state/genesiswrite/pbin_genesis_test.go b/execution/state/genesiswrite/pbin_genesis_test.go new file mode 100644 index 00000000000..11e366db48a --- /dev/null +++ b/execution/state/genesiswrite/pbin_genesis_test.go @@ -0,0 +1,92 @@ +// Copyright 2026 The Erigon Authors +// This file is part of Erigon. +// +// Erigon is free software: you can redistribute it and/or modify +// it under the terms of the GNU Lesser General Public License as published by +// the Free Software Foundation, either version 3 of the License, or +// (at your option) any later version. +// +// Erigon is distributed in the hope that it will be useful, +// but WITHOUT ANY WARRANTY; without even the implied warranty of +// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +// GNU Lesser General Public License for more details. +// +// You should have received a copy of the GNU Lesser General Public License +// along with Erigon. If not, see . + +package genesiswrite_test + +import ( + "math/big" + "testing" + + "github.com/stretchr/testify/require" + + "github.com/erigontech/erigon/common" + "github.com/erigontech/erigon/common/log/v3" + "github.com/erigontech/erigon/db/datadir" + "github.com/erigontech/erigon/db/kv/temporal/temporaltest" + "github.com/erigontech/erigon/db/state/execctx" + "github.com/erigontech/erigon/db/state/statecfg" + "github.com/erigontech/erigon/execution/chain" + "github.com/erigontech/erigon/execution/commitment" + "github.com/erigontech/erigon/execution/state/genesiswrite" + "github.com/erigontech/erigon/execution/types" +) + +func withBinCommitment(t *testing.T, on bool) { + t.Helper() + orig := statecfg.ExperimentalBinCommitment + t.Cleanup(func() { statecfg.ExperimentalBinCommitment = orig }) + statecfg.ExperimentalBinCommitment = on +} + +// Code-free alloc: code chunking into the tree is not part of this task. +func pbinTestGenesis() *types.Genesis { + return &types.Genesis{ + Config: chain.AllProtocolChanges, + Alloc: types.GenesisAlloc{ + common.HexToAddress("0x0000000000000000000000000000000000000042"): {Balance: big.NewInt(1)}, + common.HexToAddress("0x00000000000000000000000000000000000000ff"): {Balance: big.NewInt(0xdeadbeef), Nonce: 3}, + }, + } +} + +// Genesis is the block-0 state root the executor is later checked against, so it +// must be computed on the variant the datadir uses, not always on the hex trie. +func TestPBinGenesisComputesBinaryRoot(t *testing.T) { + // No t.Parallel: mutates process-global statecfg flags. + logger := log.New() + g := pbinTestGenesis() + + withBinCommitment(t, false) + hexBlock, _, err := genesiswrite.GenesisToBlock(t, g, datadir.New(t.TempDir()), logger) + require.NoError(t, err) + + withBinCommitment(t, true) + binBlock, _, err := genesiswrite.GenesisToBlock(t, g, datadir.New(t.TempDir()), logger) + require.NoError(t, err) + + require.NotEqual(t, hexBlock.Root(), binBlock.Root(), "genesis under the bin variant returned the hex root") + require.Equal(t, common.BytesToHash(pbinGenesisRoot(t, g)), binBlock.Root()) +} + +// pbinGenesisRoot computes the genesis root over a SharedDomains explicitly +// running the bin trie, as an oracle for what GenesisToBlock must return. +func pbinGenesisRoot(t *testing.T, g *types.Genesis) []byte { + t.Helper() + db := temporaltest.NewTestDB(t, datadir.New(t.TempDir())) + tx, err := db.BeginTemporalRw(t.Context()) + require.NoError(t, err) + defer tx.Rollback() + + sd, err := execctx.NewSharedDomains(t.Context(), tx, log.New()) + require.NoError(t, err) + defer sd.Close() + require.Equal(t, commitment.VariantBinPatriciaTrie, sd.GetCommitmentCtx().Trie().Variant()) + + head, _ := genesiswrite.GenesisWithoutStateToBlock(g) + root, _, err := genesiswrite.ComputeGenesisCommitment(t.Context(), g, tx, sd, head) + require.NoError(t, err) + return root +} diff --git a/rpc/jsonrpc/debug_execution_witness.go b/rpc/jsonrpc/debug_execution_witness.go index a1cbaf9e240..715bb90bb36 100644 --- a/rpc/jsonrpc/debug_execution_witness.go +++ b/rpc/jsonrpc/debug_execution_witness.go @@ -805,7 +805,7 @@ func (api *DebugAPIImpl) buildWitnessResult(ctx context.Context, tx kv.TemporalT // Use the proof infrastructure from the commitment context. // Witness generation requires the sequential HexPatriciaHashed (Witness() // type-asserts it); the parallel trie cannot serve it. - domains, err := execctx.NewSharedDomains(ctx, tx, log.New(), execctx.WithoutDeferredBranchUpdates(), execctx.WithSequentialCommitment()) + domains, err := execctx.NewSharedDomains(ctx, tx, log.New(), execctx.WithoutDeferredBranchUpdates(), execctx.WithHexCommitmentOnly()) if err != nil { return nil, err } diff --git a/rpc/jsonrpc/eth_call.go b/rpc/jsonrpc/eth_call.go index ecbc89a5110..380c45a3352 100644 --- a/rpc/jsonrpc/eth_call.go +++ b/rpc/jsonrpc/eth_call.go @@ -473,7 +473,7 @@ func (api *APIImpl) getProof(ctx context.Context, roTx kv.TemporalTx, address co return nil, err } - domains, err := execctx.NewSharedDomains(ctx, tx, log.New(), execctx.WithoutDeferredBranchUpdates(), execctx.WithSequentialCommitment()) + domains, err := execctx.NewSharedDomains(ctx, tx, log.New(), execctx.WithoutDeferredBranchUpdates(), execctx.WithHexCommitmentOnly()) if err != nil { return nil, err } @@ -765,7 +765,7 @@ func (api *BaseAPI) getWitness(ctx context.Context, db kv.TemporalRoDB, blockNrO it.Close() } - domains, err := execctx.NewSharedDomains(ctx, tx, log.New(), execctx.WithoutDeferredBranchUpdates(), execctx.WithSequentialCommitment()) + domains, err := execctx.NewSharedDomains(ctx, tx, log.New(), execctx.WithoutDeferredBranchUpdates(), execctx.WithHexCommitmentOnly()) if err != nil { return nil, err } diff --git a/rpc/jsonrpc/eth_simulation.go b/rpc/jsonrpc/eth_simulation.go index 94ad7d7796e..3adfbb3229f 100644 --- a/rpc/jsonrpc/eth_simulation.go +++ b/rpc/jsonrpc/eth_simulation.go @@ -164,7 +164,7 @@ func (api *APIImpl) SimulateV1(ctx context.Context, req SimulationRequest, block return nil, err } - sharedDomains, err := execctx.NewSharedDomains(ctx, tx, api.logger, execctx.WithoutDeferredBranchUpdates(), execctx.WithSequentialCommitment()) + sharedDomains, err := execctx.NewSharedDomains(ctx, tx, api.logger, execctx.WithoutDeferredBranchUpdates(), execctx.WithHexCommitmentOnly()) if err != nil { return nil, err } diff --git a/rpc/jsonrpc/pbin_hex_only_test.go b/rpc/jsonrpc/pbin_hex_only_test.go new file mode 100644 index 00000000000..39d729bfce2 --- /dev/null +++ b/rpc/jsonrpc/pbin_hex_only_test.go @@ -0,0 +1,64 @@ +// Copyright 2026 The Erigon Authors +// This file is part of Erigon. +// +// Erigon is free software: you can redistribute it and/or modify +// it under the terms of the GNU Lesser General Public License as published by +// the Free Software Foundation, either version 3 of the License, or +// (at your option) any later version. +// +// Erigon is distributed in the hope that it will be useful, +// but WITHOUT ANY WARRANTY; without even the implied warranty of +// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +// GNU Lesser General Public License for more details. +// +// You should have received a copy of the GNU Lesser General Public License +// along with Erigon. If not, see . + +package jsonrpc + +import ( + "testing" + "time" + + "github.com/stretchr/testify/require" + + "github.com/erigontech/erigon/cmd/rpcdaemon/rpcdaemontest" + "github.com/erigontech/erigon/common" + "github.com/erigontech/erigon/common/log/v3" + "github.com/erigontech/erigon/db/state/execctx" + "github.com/erigontech/erigon/db/state/statecfg" + "github.com/erigontech/erigon/node/ethconfig" + "github.com/erigontech/erigon/rpc" + "github.com/erigontech/erigon/rpc/ethapi" + "github.com/erigontech/erigon/rpc/rpccfg" +) + +// eth_getProof rebuilds proofs with the hex trie, so it must refuse a bin datadir +// instead of reading bit-path branch records as hex ones. +func TestPBinGetProofRefusesBin(t *testing.T) { + // No t.Parallel: mutates process-global statecfg flags. + m, _, _ := rpcdaemontest.CreateTestExecModule(t) + cfg := &rpccfg.EthApiConfig{ + GasCap: 5000000, + FeeCap: ethconfig.Defaults.RPCTxFeeCap, + ReturnDataLimit: 100_000, + MaxGetProofRewindBlockCount: 1, + SubscribeLogsChannelSize: 128, + RpcTxSyncDefaultTimeout: 20 * time.Second, + RpcTxSyncMaxTimeout: 1 * time.Minute, + } + api := NewEthAPI(newBaseApiForTest(m), m.DB, nil, nil, nil, cfg, log.New()) + + // The chain above is built on the hex trie; only the proof call runs under bin. + orig := statecfg.ExperimentalBinCommitment + t.Cleanup(func() { statecfg.ExperimentalBinCommitment = orig }) + statecfg.ExperimentalBinCommitment = true + + latest := rpc.BlockNumberOrHashWithNumber(rpc.LatestBlockNumber) + _, err := api.GetProof(t.Context(), common.HexToAddress("0x71562b71999873db5b286df957af199ec94617f7"), nil, &latest) + require.ErrorIs(t, err, execctx.ErrBinCommitmentUnsupported) + + req := SimulationRequest{BlockStateCalls: []SimulatedBlock{{Calls: []ethapi.CallArgs{{}}}}} + _, err = api.SimulateV1(t.Context(), req, latest) + require.ErrorIs(t, err, execctx.ErrBinCommitmentUnsupported) +} diff --git a/rpc/jsonrpc/receipts/receipts_generator.go b/rpc/jsonrpc/receipts/receipts_generator.go index 35115a2dfa7..bef71bffd0e 100644 --- a/rpc/jsonrpc/receipts/receipts_generator.go +++ b/rpc/jsonrpc/receipts/receipts_generator.go @@ -325,7 +325,7 @@ func (g *Generator) GetReceipt(ctx context.Context, cfg *chain.Config, tx kv.Tem var stateWriter state.StateWriter if calculatePostState && postState.CommitmentHistory { - sharedDomains, err = execctx.NewSharedDomains(ctx, tx, log.Root(), execctx.WithoutDeferredBranchUpdates(), execctx.WithSequentialCommitment()) + sharedDomains, err = execctx.NewSharedDomains(ctx, tx, log.Root(), execctx.WithoutDeferredBranchUpdates(), execctx.WithHexCommitmentOnly()) if err != nil { return nil, err } @@ -544,7 +544,7 @@ func (g *Generator) GetReceipts(ctx context.Context, cfg *chain.Config, tx kv.Te var stateWriter state.StateWriter if opts.CommitmentHistoryEnabled { - sharedDomains, err = execctx.NewSharedDomains(ctx, tx, log.Root(), execctx.WithoutDeferredBranchUpdates(), execctx.WithSequentialCommitment()) + sharedDomains, err = execctx.NewSharedDomains(ctx, tx, log.Root(), execctx.WithoutDeferredBranchUpdates(), execctx.WithHexCommitmentOnly()) if err != nil { return nil, err } diff --git a/rpc/rpchelper/commitment.go b/rpc/rpchelper/commitment.go index 93ccc515cf6..56dd4b37a46 100644 --- a/rpc/rpchelper/commitment.go +++ b/rpc/rpchelper/commitment.go @@ -94,7 +94,7 @@ func (r *CommitmentReplay) ComputeCustomCommitmentFromStateHistory( } defer ttx.Rollback() - tsd, err := execctx.NewSharedDomains(ctx, ttx, r.logger, execctx.WithoutDeferredBranchUpdates(), execctx.WithSequentialCommitment()) + tsd, err := execctx.NewSharedDomains(ctx, ttx, r.logger, execctx.WithoutDeferredBranchUpdates(), execctx.WithHexCommitmentOnly()) if err != nil { return nil, err } diff --git a/rpc/rpchelper/pbin_commitment_test.go b/rpc/rpchelper/pbin_commitment_test.go new file mode 100644 index 00000000000..a5d6fbdda4f --- /dev/null +++ b/rpc/rpchelper/pbin_commitment_test.go @@ -0,0 +1,50 @@ +// Copyright 2026 The Erigon Authors +// This file is part of Erigon. +// +// Erigon is free software: you can redistribute it and/or modify +// it under the terms of the GNU Lesser General Public License as published by +// the Free Software Foundation, either version 3 of the License, or +// (at your option) any later version. +// +// Erigon is distributed in the hope that it will be useful, +// but WITHOUT ANY WARRANTY; without even the implied warranty of +// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +// GNU Lesser General Public License for more details. +// +// You should have received a copy of the GNU Lesser General Public License +// along with Erigon. If not, see . + +package rpchelper + +import ( + "testing" + + "github.com/stretchr/testify/require" + + "github.com/erigontech/erigon/common/log/v3" + "github.com/erigontech/erigon/db/datadir" + "github.com/erigontech/erigon/db/kv/rawdbv3" + "github.com/erigontech/erigon/db/kv/temporal/temporaltest" + "github.com/erigontech/erigon/db/state/execctx" + "github.com/erigontech/erigon/db/state/statecfg" +) + +// Commitment replay recomputes roots with the hex trie over its own temporary +// aggregator, so it cannot serve a bin datadir. +func TestPBinCommitmentReplayRefusesBin(t *testing.T) { + // No t.Parallel: mutates process-global statecfg flags. + db := temporaltest.NewTestDB(t, datadir.New(t.TempDir())) + tx, err := db.BeginTemporalRo(t.Context()) + require.NoError(t, err) + defer tx.Rollback() + + orig := statecfg.ExperimentalBinCommitment + t.Cleanup(func() { statecfg.ExperimentalBinCommitment = orig }) + statecfg.ExperimentalBinCommitment = true + + // Fresh dirs: the replay resolves erigondb.toml itself, and a hex toml would + // be refused there instead of at the SharedDomains this test pins. + r := NewCommitmentReplay(datadir.New(t.TempDir()), rawdbv3.TxNums, log.New()) + _, err = r.ComputeCustomCommitmentFromStateHistory(t.Context(), tx, 0, nil) + require.ErrorIs(t, err, execctx.ErrBinCommitmentUnsupported) +} From 7d57decf9cd0240b0556c5c96a0615824123c136 Mon Sep 17 00:00:00 2001 From: awskii Date: Thu, 30 Jul 2026 19:01:10 +0700 Subject: [PATCH 31/56] feat: refuse the hex-only commitment paths under the bin variant MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit execution/commitment, commitmentdb, stagedsync, node/eth: four code paths degraded silently under the bin trie — deferred commitment updates (flag accepted, no trie honouring it), the deferred-update take, collapse tracing and BranchChildCount (hex nibble prefix, so a miss read as zero children), and trie-trace capture (records replay into the hex trie only). Each now refuses with commitment.ErrPBinUnsupported: error where the signature has one, panic on the two void setters. ExecV3 no longer requests deferral under bin, so the panic stays unreachable, and startup logs the unsupported paths once. --- docs/plans/20260730-pbin-m1-local-el.md | 18 +-- .../commitmentdb/commitment_context.go | 48 +++++-- .../commitmentdb/pbin_unsupported_test.go | 119 ++++++++++++++++++ execution/commitment/pbin_patricia_hashed.go | 4 + execution/stagedsync/exec3.go | 19 ++- execution/stagedsync/pbin_defer_test.go | 56 +++++++++ node/eth/backend.go | 5 + 7 files changed, 247 insertions(+), 22 deletions(-) create mode 100644 execution/commitment/commitmentdb/pbin_unsupported_test.go create mode 100644 execution/stagedsync/pbin_defer_test.go diff --git a/docs/plans/20260730-pbin-m1-local-el.md b/docs/plans/20260730-pbin-m1-local-el.md index 4240eb2c5b6..32ea4b1c79e 100644 --- a/docs/plans/20260730-pbin-m1-local-el.md +++ b/docs/plans/20260730-pbin-m1-local-el.md @@ -124,7 +124,7 @@ Do not treat these as established: - `pbin_branch.go` record field-bit layout beyond the leaf-kind rejection at `:156-165`. **Verify before designing Task 13's value-in-record field.** - ~~The "grid arrays are restorable-as-zero" argument underpinning Task 5's ~160-byte blob~~ — **proven in Task 5** (see the Task 5 checklist for the read/write-site audit); the root-cell blob landed. - Whether pbin branch records are truly opaque to the pass-through merge path (believed yes with references off, not exercised). -- Task 8's deferral mis-attribution, inferred from comments at `commitment_context.go:150-157` and `:581-583`; no concrete failing sequence was constructed. +- ~~Task 8's deferral mis-attribution~~ — **Task 8**: still no concrete failing sequence, and the exposure is bounded: deferral is only ever requested by `ExecV3` (fork validation / parallel apply), and the fork-validation writes it would mis-route land in a validation overlay that is never flushed. The guards are structural — bin cannot reach the deferred path at all now. ## What Goes Where @@ -249,14 +249,18 @@ Production keeps Keccak-256. This task only makes the **test** path run the whol **Files:** - Modify: `execution/commitment/commitmentdb/commitment_context.go` +- Modify: `execution/commitment/pbin_patricia_hashed.go` (➕ `ErrPBinUnsupported`, the sentinel both packages wrap) - Modify: `execution/stagedsync/exec3.go` +- Modify: `node/eth/backend.go` (➕ startup limitation log) - Create: `execution/commitment/commitmentdb/pbin_unsupported_test.go` - -- [ ] write a failing test asserting `SetLeaveDeferredForCaller` and the deferred-update take reject the bin variant instead of silently no-opping -- [ ] reject the bin variant explicitly where `exec3.go:206-210` enables deferral for fork validation and the parallel apply path -- [ ] make `SetCollapseTracer` (`:415-420`), `BranchChildCount` (`:424-431`) and trace-state capture (`:501-506`) error under bin rather than degrade -- [ ] write tests asserting each of the four paths errors under bin -- [ ] run tests — must pass before task 9 +- Create: `execution/stagedsync/pbin_defer_test.go` (➕) + +- [x] write a failing test asserting `SetLeaveDeferredForCaller` and the deferred-update take reject the bin variant instead of silently no-opping — `TestPBinRefusesDeferredCommitmentUpdates` (enabling side) + `TestPBinComputeCommitmentRefusesDeferredTake` (taking side), both red before: the flag was accepted and the post-`Process` type switch matched no bin trie +- [x] reject the bin variant explicitly where `exec3.go:206-210` enables deferral for fork validation and the parallel apply path — `deferCommitmentUpdates(variant, isForkValidation, parallel, isApplyingBlocks)` excludes bin, so `ExecV3` never makes a request the context panics on. **Not an ExecV3 error**: `ValidateChain` runs fork validation on every `engine_newPayload` (`exec_module.go:589`), so erroring there would make the M1b dev-chain gate unreachable; deferral is a re-org-overhead optimisation and the inline path it falls back to is the default one, over a validation overlay that is never flushed (`exec_module.go:600-602`) +- [x] make `SetCollapseTracer` (`:415-420`), `BranchChildCount` (`:424-431`) and trace-state capture (`:501-506`) error under bin rather than degrade — `BranchChildCount` and the trace capture (in `ComputeCommitment`) return `commitment.ErrPBinUnsupported`; the two void setters (`SetDeferCommitmentUpdates`, `SetCollapseTracer`) panic with the same wrapped error, matching this file's existing misuse convention (`EnableParaTrieDB`, the Task 4 ctor assert) instead of taking a fourth API break for an error return. Both are unreachable under bin in production — their only callers reach a hex-only SharedDomains (Task 7) +- [x] write tests asserting each of the four paths errors under bin — `commitmentdb/pbin_unsupported_test.go`: deferral enable, deferral take, trie-trace capture, collapse tracer, branch child count; each also pins that hex still accepts. ➕ `stagedsync/pbin_defer_test.go` (not in the planned file list) table-tests the exec3 decision +- [x] ➕ log the bin variant's unsupported paths once at startup, after the erigondb resolve so a flagless bin restart says it too (`node/eth/backend.go`) +- [x] run tests — `./execution/commitment/... ./db/state/... ./execution/stagedsync/... ./rpc/jsonrpc/...` green, `make lint` clean twice ### Task 9: Zero-vs-absent in the engine diff --git a/execution/commitment/commitmentdb/commitment_context.go b/execution/commitment/commitmentdb/commitment_context.go index 4b5a603b2a3..9ebb6f8b11a 100644 --- a/execution/commitment/commitmentdb/commitment_context.go +++ b/execution/commitment/commitmentdb/commitment_context.go @@ -135,9 +135,19 @@ func (sdc *SharedDomainsCommitmentContext) EnableTrieWarmup(trieWarmup bool) { // instead of being applied inline. Used during fork validation where the update is // flushed later via FlushPendingUpdate. func (sdc *SharedDomainsCommitmentContext) SetDeferCommitmentUpdates(defer_ bool) { + if defer_ && sdc.variant == commitment.VariantBinPatriciaTrie { + panic(pbinUnsupported("deferred commitment updates")) + } sdc.deferCommitmentUpdates = defer_ } +// pbinUnsupported names a code path only the hex trie implements — deferred +// updates, collapse tracing, hex-prefixed branch reads, trie-trace replay — so +// asking for one under the bin variant fails instead of yielding a zero value. +func pbinUnsupported(what string) error { + return fmt.Errorf("%w: %s", commitment.ErrPBinUnsupported, what) +} + // TakePendingUpdate returns the pending update and clears the field. // Caller takes ownership of the returned value. func (sdc *SharedDomainsCommitmentContext) TakePendingUpdate() *commitment.PendingCommitmentUpdate { @@ -421,6 +431,9 @@ func (sdc *SharedDomainsCommitmentContext) WitnessLean(ctx context.Context, code // during commitment calculation. This is used by witness generation to capture paths // to HashNodes that need resolution when a FullNode is reduced to a single child. func (sdc *SharedDomainsCommitmentContext) SetCollapseTracer(tracer commitment.CollapseTracer) { + if tracer != nil && sdc.variant == commitment.VariantBinPatriciaTrie { + panic(pbinUnsupported("collapse tracing")) + } hexPatriciaHashed, ok := sdc.Trie().(*commitment.HexPatriciaHashed) if ok { hexPatriciaHashed.SetCollapseTracer(tracer) @@ -430,6 +443,9 @@ func (sdc *SharedDomainsCommitmentContext) SetCollapseTracer(tracer commitment.C // BranchChildCount returns the child count of the branch at nibblePrefix, read // from the in-memory commitment domain (post-compute state). func (sdc *SharedDomainsCommitmentContext) BranchChildCount(tx kv.TemporalTx, nibblePrefix []byte) (int, error) { + if sdc.variant == commitment.VariantBinPatriciaTrie { + return 0, pbinUnsupported("branch child count by hex nibble prefix") + } key := nibbles.HexToCompact(nibblePrefix) enc, _, err := sdc.sharedDomains.AsGetter(tx).GetLatest(kv.CommitmentDomain, key) if err != nil { @@ -438,6 +454,20 @@ func (sdc *SharedDomainsCommitmentContext) BranchChildCount(tx kv.TemporalTx, ni return commitment.BranchData(enc).ChildCount(), nil } +// trieTraceFile returns where blockNum's trie trace goes, or "" when tracing is +// off or aimed at another block. TRIE_TRACE_BLOCK alone picks a default path. +func trieTraceFile(blockNum uint64) string { + if dbg.TrieTraceBlock != 0 { + if blockNum != dbg.TrieTraceBlock { + return "" + } + if dbg.TrieTraceFile == "" { + return fmt.Sprintf("/tmp/trie-trace-block-%d.toml", blockNum) + } + } + return dbg.TrieTraceFile +} + // ComputeCommitment Evaluates commitment for gathered updates. // If warmup was set via EnableTrieWarmup, pre-warms MDBX page cache by reading Branch data in parallel before processing. // ComputeCommitment should normally be called via SharedDomains.ComputeCommitment, @@ -447,6 +477,15 @@ func (sdc *SharedDomainsCommitmentContext) ComputeCommitment(ctx context.Context if sdc.pendingUpdate != nil { panic("sdCtx.ComputeCommitment called directly with non-nil pendingUpdate; use SharedDomains.ComputeCommitment wrapper instead") } + traceFile := trieTraceFile(blockNum) + if sdc.variant == commitment.VariantBinPatriciaTrie { + switch { + case sdc.deferCommitmentUpdates: + return nil, pbinUnsupported("deferred commitment updates") + case traceFile != "": + return nil, pbinUnsupported("trie trace capture") + } + } if dbg.KVReadLevelledMetrics { mxCommitmentRunning.Inc() defer mxCommitmentRunning.Dec() @@ -484,16 +523,7 @@ func (sdc *SharedDomainsCommitmentContext) ComputeCommitment(ctx context.Context trieContext := sdc.trieContext(tx, blockNum, txNum, readCtx) - // If trie trace is configured, wrap the context with a recorder. - // Block-targeted: when TrieTraceBlock is set, only record that specific block. var recorder *commitment.RecordingContext - traceFile := dbg.TrieTraceFile - if traceFile == "" && dbg.TrieTraceBlock != 0 && blockNum == dbg.TrieTraceBlock { - // Auto-generate filename when only TRIE_TRACE_BLOCK is set without TRIE_TRACE_FILE. - traceFile = fmt.Sprintf("/tmp/trie-trace-block-%d.toml", blockNum) - } else if dbg.TrieTraceBlock != 0 && blockNum != dbg.TrieTraceBlock { - traceFile = "" // skip recording — not the target block - } if traceFile != "" { recorder = commitment.NewRecordingContext(trieContext) sdc.patriciaTrie.ResetContext(recorder) diff --git a/execution/commitment/commitmentdb/pbin_unsupported_test.go b/execution/commitment/commitmentdb/pbin_unsupported_test.go new file mode 100644 index 00000000000..2f16202755e --- /dev/null +++ b/execution/commitment/commitmentdb/pbin_unsupported_test.go @@ -0,0 +1,119 @@ +// Copyright 2026 The Erigon Authors +// This file is part of Erigon. +// +// Erigon is free software: you can redistribute it and/or modify +// it under the terms of the GNU Lesser General Public License as published by +// the Free Software Foundation, either version 3 of the License, or +// (at your option) any later version. +// +// Erigon is distributed in the hope that it will be useful, +// but WITHOUT ANY WARRANTY; without even the implied warranty of +// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +// GNU Lesser General Public License for more details. +// +// You should have received a copy of the GNU Lesser General Public License +// along with Erigon. If not, see . + +package commitmentdb + +import ( + "testing" + + "github.com/stretchr/testify/require" + + "github.com/erigontech/erigon/common/dbg" + "github.com/erigontech/erigon/execution/commitment" +) + +func pbinRecoveredError(t *testing.T, fn func()) (err error) { + t.Helper() + defer func() { + r := recover() + if r == nil { + return + } + recovered, ok := r.(error) + require.True(t, ok, "panic value must carry the error: %v", r) + err = recovered + }() + fn() + return nil +} + +// TestPBinRefusesDeferredCommitmentUpdates pins the enabling side of the +// deferred-update path: hex and parallel take the request, bin refuses it by +// name. Silently accepting it would leave the flag set with no trie honouring +// it, so Process would apply inline while the caller waited for a flush. +func TestPBinRefusesDeferredCommitmentUpdates(t *testing.T) { + t.Parallel() + + hexCtx := pbinStateTestCtx(t, commitment.VariantHexPatriciaTrie) + hexCtx.SetDeferCommitmentUpdates(true) + require.True(t, hexCtx.deferCommitmentUpdates) + + binCtx := pbinStateTestCtx(t, commitment.VariantBinPatriciaTrie) + err := pbinRecoveredError(t, func() { binCtx.SetDeferCommitmentUpdates(true) }) + require.ErrorIs(t, err, commitment.ErrPBinUnsupported) + require.False(t, binCtx.deferCommitmentUpdates, "the refused request must not leave the flag set") + + require.NoError(t, pbinRecoveredError(t, func() { binCtx.SetDeferCommitmentUpdates(false) })) +} + +// TestPBinComputeCommitmentRefusesDeferredTake covers the taking side: were the +// flag ever set under bin, the post-Process type switch would find no trie +// carrying deferred updates and hand back an empty pendingUpdate. +func TestPBinComputeCommitmentRefusesDeferredTake(t *testing.T) { + t.Parallel() + + binCtx := pbinStateTestCtx(t, commitment.VariantBinPatriciaTrie) + binCtx.deferCommitmentUpdates = true + + _, err := binCtx.ComputeCommitment(t.Context(), nil, false, 1, 1, "test", nil) + require.ErrorIs(t, err, commitment.ErrPBinUnsupported) +} + +// TestPBinComputeCommitmentRefusesTrieTrace: the trace records branch records +// and replays them through the hex trie, so a bin trace would replay as a +// different tree. The trace is env-gated, so refusing costs a normal run nothing. +func TestPBinComputeCommitmentRefusesTrieTrace(t *testing.T) { + prev := dbg.TrieTraceFile + dbg.TrieTraceFile = t.TempDir() + "/trie-trace.toml" + t.Cleanup(func() { dbg.TrieTraceFile = prev }) + + binCtx := pbinStateTestCtx(t, commitment.VariantBinPatriciaTrie) + _, err := binCtx.ComputeCommitment(t.Context(), nil, false, 1, 1, "test", nil) + require.ErrorIs(t, err, commitment.ErrPBinUnsupported) + + hexCtx := pbinStateTestCtx(t, commitment.VariantHexPatriciaTrie) + _, err = hexCtx.ComputeCommitment(t.Context(), nil, false, 1, 1, "test", nil) + require.NoError(t, err) +} + +// TestPBinRefusesCollapseTracer guards the witness path: the tracer only ever +// reaches a HexPatriciaHashed, so under bin it was installed nowhere and the +// caller collected no collapse paths. +func TestPBinRefusesCollapseTracer(t *testing.T) { + t.Parallel() + + tracer := func(hashedKeyPath, branchPrefix []byte) {} + + hexCtx := pbinStateTestCtx(t, commitment.VariantHexPatriciaTrie) + require.NoError(t, pbinRecoveredError(t, func() { hexCtx.SetCollapseTracer(tracer) })) + + binCtx := pbinStateTestCtx(t, commitment.VariantBinPatriciaTrie) + err := pbinRecoveredError(t, func() { binCtx.SetCollapseTracer(tracer) }) + require.ErrorIs(t, err, commitment.ErrPBinUnsupported) + + require.NoError(t, pbinRecoveredError(t, func() { binCtx.SetCollapseTracer(nil) }), "clearing must stay allowed") +} + +// TestPBinBranchChildCountRefusesBin: the prefix is a hex nibble path compacted +// into a commitment key, which addresses no bin record — the read used to miss +// and report a child count of zero. +func TestPBinBranchChildCountRefusesBin(t *testing.T) { + t.Parallel() + + binCtx := pbinStateTestCtx(t, commitment.VariantBinPatriciaTrie) + _, err := binCtx.BranchChildCount(nil, []byte{0x0a, 0x0b}) + require.ErrorIs(t, err, commitment.ErrPBinUnsupported) +} diff --git a/execution/commitment/pbin_patricia_hashed.go b/execution/commitment/pbin_patricia_hashed.go index d3bf1067c41..93711f3b05b 100644 --- a/execution/commitment/pbin_patricia_hashed.go +++ b/execution/commitment/pbin_patricia_hashed.go @@ -129,6 +129,10 @@ var ( errPBinDeleteUnsupported = errors.New("pbin: EIP-8297 defines no deletion") ) +// ErrPBinUnsupported marks a code path only the hex trie implements. Callers +// wrap it with the path name so the bin variant refuses instead of no-opping. +var ErrPBinUnsupported = errors.New("pbin: unsupported under the bin commitment variant") + // pbinRootKey names the record holding the root cell — the one node no descent // can name: every other node is found by the path that reaches it, while the // root's own prefix is stored nowhere else. The sentinel cannot collide with a diff --git a/execution/stagedsync/exec3.go b/execution/stagedsync/exec3.go index 91c12459fa3..83c1591a6fc 100644 --- a/execution/stagedsync/exec3.go +++ b/execution/stagedsync/exec3.go @@ -108,6 +108,18 @@ func restoreTxNum(ctx context.Context, cfg *ExecuteBlockCfg, applyTx kv.Tx, curr return inputTxNum, maxTxNum, offsetFromBlockBeginning, blockNum, nil } +// deferCommitmentUpdates reports whether Process() may leave branch updates as a +// pending update flushed at the block boundary instead of applying them inline. +// Deferring cuts re-org validation overhead; the parallel apply path also needs +// Flush() to carry the pending update across sync cycles. The bin trie has no +// deferred-update path and refuses the request, so it stays on the inline path. +func deferCommitmentUpdates(variant commitment.TrieVariant, isForkValidation, parallel, isApplyingBlocks bool) bool { + if variant == commitment.VariantBinPatriciaTrie { + return false + } + return isForkValidation || (parallel && isApplyingBlocks) +} + func ExecV3(ctx context.Context, execStage *StageState, u Unwinder, cfg ExecuteBlockCfg, doms *execctx.SharedDomains, rwTx kv.TemporalRwTx, @@ -199,12 +211,7 @@ func ExecV3(ctx context.Context, doms.EnableParaTrieDB(cfg.db) doms.EnableTrieWarmup(true) doms.SetDeferCommitmentUpdates(false) - // Enable deferred commitment updates for fork validation and parallel initial sync. - // Deferred updates batch commitment calculations to block boundaries rather than - // per-transaction, significantly reducing re-org validation overhead. - // For the parallel path during initial sync, Flush() now includes pending updates, - // so they are no longer silently discarded between StageLoopIteration cycles. - if isForkValidation || (parallel && isApplyingBlocks) { + if deferCommitmentUpdates(doms.GetCommitmentCtx().Trie().Variant(), isForkValidation, parallel, isApplyingBlocks) { doms.SetDeferCommitmentUpdates(true) } defer doms.SetDeferCommitmentUpdates(false) diff --git a/execution/stagedsync/pbin_defer_test.go b/execution/stagedsync/pbin_defer_test.go new file mode 100644 index 00000000000..6a031ed54e3 --- /dev/null +++ b/execution/stagedsync/pbin_defer_test.go @@ -0,0 +1,56 @@ +// Copyright 2026 The Erigon Authors +// This file is part of Erigon. +// +// Erigon is free software: you can redistribute it and/or modify +// it under the terms of the GNU Lesser General Public License as published by +// the Free Software Foundation, either version 3 of the License, or +// (at your option) any later version. +// +// Erigon is distributed in the hope that it will be useful, +// but WITHOUT ANY WARRANTY; without even the implied warranty of +// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +// GNU Lesser General Public License for more details. +// +// You should have received a copy of the GNU Lesser General Public License +// along with Erigon. If not, see . + +package stagedsync + +import ( + "testing" + + "github.com/stretchr/testify/require" + + "github.com/erigontech/erigon/execution/commitment" +) + +// TestPBinDeferCommitmentUpdatesExcludesBin pins the exec3 side of the deferral +// decision. The commitment context panics on a deferral request under the bin +// variant, so ExecV3 must never make one — while every hex-family variant keeps +// deferring for fork validation and the parallel apply path. +func TestPBinDeferCommitmentUpdatesExcludesBin(t *testing.T) { + t.Parallel() + + for _, tc := range []struct { + name string + variant commitment.TrieVariant + isForkValidation bool + parallel bool + isApplyingBlocks bool + want bool + }{ + {name: "hex fork validation", variant: commitment.VariantHexPatriciaTrie, isForkValidation: true, want: true}, + {name: "hex parallel apply", variant: commitment.VariantHexPatriciaTrie, parallel: true, isApplyingBlocks: true, want: true}, + {name: "hex parallel not applying", variant: commitment.VariantHexPatriciaTrie, parallel: true}, + {name: "hex serial apply", variant: commitment.VariantHexPatriciaTrie, isApplyingBlocks: true}, + {name: "parallel trie fork validation", variant: commitment.VariantParallelHexPatricia, isForkValidation: true, want: true}, + {name: "bin fork validation", variant: commitment.VariantBinPatriciaTrie, isForkValidation: true}, + {name: "bin parallel apply", variant: commitment.VariantBinPatriciaTrie, parallel: true, isApplyingBlocks: true}, + } { + t.Run(tc.name, func(t *testing.T) { + t.Parallel() + got := deferCommitmentUpdates(tc.variant, tc.isForkValidation, tc.parallel, tc.isApplyingBlocks) + require.Equal(t, tc.want, got) + }) + } +} diff --git a/node/eth/backend.go b/node/eth/backend.go index f8060499c91..8335b6df433 100644 --- a/node/eth/backend.go +++ b/node/eth/backend.go @@ -365,6 +365,11 @@ func New(ctx context.Context, stack *node.Node, config *ethconfig.Config, logger return nil, err } + // After the resolve: a flagless restart of a bin datadir adopts the variant there. + if statecfg.ExperimentalBinCommitment { + logger.Warn("EXPERIMENTAL BINARY COMMITMENT TRIE IS ENABLED: roots follow EIP-8297 over Keccak-256 and agree with no other client; witness, eth_getProof, eth_simulateV1, receipt regeneration, deferred commitment updates, collapse tracing and trie traces are unsupported and refuse rather than degrade") + } + var chainConfig *chain.Config var genesis *types.Block if err := rawChainDB.Update(context.Background(), func(tx kv.RwTx) error { From 065d8b08cc1b1edbab6ca30f589a2ef5b9c030b9 Mon Sep 17 00:00:00 2001 From: awskii Date: Thu, 30 Jul 2026 19:16:02 +0700 Subject: [PATCH 32/56] =?UTF-8?q?feat:=20zero-vs-absent=20in=20the=20pbin?= =?UTF-8?q?=20engine=20=E2=80=94=20a=20zeroed=20storage=20slot=20keeps=20i?= =?UTF-8?q?ts=20leaf?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit An absent domain read is the domain's only encoding for both a zero value and a removed key. EIP-8297 has no removal and commits a zero value as a present leaf, so pbin holds the presence bit the domain lacks: a delete landing on a live storage leaf writes 32 zero bytes and keeps the leaf, a delete on an empty cell still contributes nothing, and an account removal stays refused while Q1 is unanswered. --- docs/plans/20260730-pbin-m1-local-el.md | 20 +- execution/commitment/pbin_patricia_hashed.go | 46 ++-- execution/commitment/pbin_process_test.go | 54 +---- execution/commitment/pbin_zerovalue_test.go | 211 +++++++++++++++++++ 4 files changed, 258 insertions(+), 73 deletions(-) create mode 100644 execution/commitment/pbin_zerovalue_test.go diff --git a/docs/plans/20260730-pbin-m1-local-el.md b/docs/plans/20260730-pbin-m1-local-el.md index 32ea4b1c79e..b33f2d6c9ef 100644 --- a/docs/plans/20260730-pbin-m1-local-el.md +++ b/docs/plans/20260730-pbin-m1-local-el.md @@ -111,9 +111,9 @@ Each hazard needs a named test or a structural assert. These are the plan's real Blocking items needing a human or upstream answer. Do not proceed past the task that depends on one without recording the answer here. -- **Q1 (highest value) — what does the reference produce for a removed account?** EIP-161 empty-removal and EIP-6780 destruct both hand pbin a `DeleteUpdate` on a committed leaf. Task 9 would turn that into BASIC_DATA `{version 0, code_size 0, nonce 0, balance 0}` + CODE_HASH `keccak(empty)`. Consistent with eip:345-347 but **not verified against the reference**, and `testdata/eip8297_vectors.json` does not cover it. It silently changes the root. +- **Q1 (highest value) — what does the reference produce for a removed account?** EIP-161 empty-removal and EIP-6780 destruct both hand pbin a `DeleteUpdate` on a committed leaf. Task 9 would turn that into BASIC_DATA `{version 0, code_size 0, nonce 0, balance 0}` + CODE_HASH `keccak(empty)`. Consistent with eip:345-347 but **not verified against the reference**, and `testdata/eip8297_vectors.json` does not cover it. It silently changes the root. ⚠️ **Deferred at Task 9, still unanswered.** Account removal keeps erroring at both sites (`updateCell`, the `loadCellState` account arm); only storage was reinterpreted. Note the `zero_value_present` vector *is* an account-zone BASIC_DATA leaf of 32 zero bytes, so the reference at least admits that leaf shape — it does not say a removal produces it. Unblocks nothing in M1: a dev chain reaches neither removal path. - **Q2 — is the tree a pure function of current state, or of history?** (H8.) eip-8297 is silent. Determines whether recompute-from-domains is a valid oracle at all, hence whether the M1a gate means anything. Possibly an upstream spec question. -- **Q3 — does the reference create a present-zero leaf on `SSTORE 0` to a virgin slot?** Erigon drops it at `execution/state/state_object.go:245-246` before any writer sees it. If yes, that guard must be variant-gated and every zero-SSTORE becomes a state write. +- **Q3 — does the reference create a present-zero leaf on `SSTORE 0` to a virgin slot?** Erigon drops it at `execution/state/state_object.go:245-246` before any writer sees it. If yes, that guard must be variant-gated and every zero-SSTORE becomes a state write. Still open after Task 9, which deliberately kept the virgin case a no-op: a delete only zeroes a cell that already holds a leaf. - **Q4 — confirm `github.com/zeebo/blake3`** as a **test-only** dependency (lower stakes now that production stays Keccak; the in-graph `lukechampine.com/blake3` may simply be enough). Its measured advantage was 13–18% arm64 / 58–78% amd64, not re-verified. **Answered (Task 1):** `zeebo` not added — production stays Keccak so BLAKE3 speed is irrelevant, and `lukechampine.com/blake3` is already a direct dependency used by `pbin_specroots_test.go`; it now backs `pbinBlake3Hash` for all three seam tests. - **Q5 — forward/backward compatibility of a new `erigondb.toml` key** across erigon binaries, given the file is downloader-delivered and **wins over the CLI** (`erigondb_settings.go:74-89`). **Answered (Task 6):** `readErigonDBSettings` uses `go-toml/v2` `Unmarshal`, which ignores unknown keys — older binaries parse a `trie_variant` toml fine. The key is written only when bin, so published/downloader tomls stay byte-identical, and a downloader-delivered hex toml under a bin process is refused at resolve. Residual risk: a binary **predating the key** opens a bin datadir as hex with no guard — inherent to any new key; acceptable while bin is experimental and fresh-datadir-only. @@ -267,13 +267,15 @@ Production keeps Keccak-256. This task only makes the **test** path run the whol **Files:** - Modify: `execution/commitment/pbin_patricia_hashed.go` - Create: `execution/commitment/pbin_zerovalue_test.go` - -- [ ] write a failing test asserting a `DeleteUpdate` on an existing **storage** leaf writes 32 zero bytes and keeps the leaf, matching the reference `zero_value_present` root — this part does **not** depend on Q1 and is what a dev chain actually needs -- [ ] reinterpret `DeleteUpdate` for storage at the three reject sites — `updateCell` (`:257-263`) and both `loadCellState` arms (`:786-791`, `:797-803`) -- [ ] for the **account-removal** encoding only: record the answer to Q1 in this plan first; if unanswered, mark ⚠️, leave account removal rejecting, and continue — a dev chain reaches neither EIP-161 clearing nor the EIP-6780 pre-funded-CREATE2 case -- [ ] leave the domain encoding and the three zero-write `DomainDel` sites untouched -- [ ] write a test asserting `foldDelete` remains unreachable from `Process` (guards H12) -- [ ] run tests — must pass before task 10 +- Modify: `execution/commitment/pbin_process_test.go` (➕ retire the two tests pinning the replaced behaviour) + +- [x] write a failing test asserting a `DeleteUpdate` on an existing **storage** leaf writes 32 zero bytes and keeps the leaf, matching the reference `zero_value_present` root — `TestPBinStorageDeleteKeepsLeafAsPresentZero` (storage zone + account-header zone) and `TestPBinStorageDeleteOnUntouchedSiblingIsPresentZero`, both red before. ⚠️ correction: `zero_value_present`'s single entry is an **account-zone BASIC_DATA** key (`0x00 ‖ stem ‖ 0x00`), so it is Q1's shape, not a storage one, and the engine already reproduces it via `pbin_specengine_test.go`. The storage roots are anchored on the oracle instead — the same tree code that vector pins in `pbin_specroots_test.go`. Each test also asserts the zeroed leaf is *not* dropped: present-zero ≠ absent +- [x] reinterpret `DeleteUpdate` for storage at the three reject sites — `updateCell` and the `loadCellState` storage arm now route through `pbinZeroedLeafUpdate`, which returns a zero `StorageUpdate` for a 52-byte plain key and `errPBinDeleteUnsupported` for an account. The `loadCellState` **account** arm keeps its own explicit rejection. A delete landing on an empty cell stays a no-op (no leaf to zero) — Q3's virgin-slot case is untouched. `TestPBinLoadCellStateAbsentRead` pins the two arms apart directly. The fourth guard, `processKey` (`:184-186`), is deliberately left rejecting: it only sees a non-nil stream update, which `ModeDirect` — the mode bin is hardwired to — never passes +- [x] for the **account-removal** encoding only: record the answer to Q1 in this plan first; if unanswered, mark ⚠️, leave account removal rejecting, and continue — ⚠️ **Q1 remains unanswered**: no reference behaviour for a removed account, and no vector covers it. Account removal still errors; `TestPBinAccountRemovalStillRefused` pins that. A dev chain reaches neither EIP-161 clearing nor the EIP-6780 pre-funded-CREATE2 case, so M1b is not blocked +- [x] leave the domain encoding and the three zero-write `DomainDel` sites untouched — `git diff --stat` for this task touches only `pbin_patricia_hashed.go` and two test files +- [x] write a test asserting `foldDelete` remains unreachable from `Process` (guards H12) — `TestPBinFoldDeleteUnreachableFromProcess`: a run zeroing every stored leaf (both zones) plus an absent key, asserted through `pbinStrictWriteContext` to write no zero-length record. That is foldDelete's only observable — `storeRoot` is the sole other zero-length write and only at the root key +- [x] ➕ retired `TestPBinProcessRejectsDeletedLeaf` / `TestPBinProcessRejectsDeletedSibling` from `pbin_process_test.go`: they pinned the storage behaviour this task replaces, and the two present-zero tests are their successors +- [x] run tests — `go test ./execution/commitment/... -count=1` and `./db/state/... -short` green, `go build ./...` clean, `make lint` clean twice ### Task 10: M1a gate — pbin over a real datadir diff --git a/execution/commitment/pbin_patricia_hashed.go b/execution/commitment/pbin_patricia_hashed.go index 93711f3b05b..8963dbaba39 100644 --- a/execution/commitment/pbin_patricia_hashed.go +++ b/execution/commitment/pbin_patricia_hashed.go @@ -21,9 +21,9 @@ // derivation, behind pbinHasher so the suite can be swapped. // // M0 scope: in-memory Process over the account and storage zones, ModeDirect -// only. Code chunking, deletion and parallel mounting are out — BASIC_DATA -// carries code_size 0, and a delete is rejected rather than applied, whether it -// arrives on the update stream or as an absent state read over a live leaf. +// only. Code chunking and parallel mounting are out — BASIC_DATA carries +// code_size 0. EIP-8297 has no removal: a zeroed storage slot keeps its leaf at +// 32 zero bytes, while an account removal is refused rather than guessed at. package commitment @@ -270,13 +270,17 @@ func (pph *PBinPatriciaHashed) updateCell(plainKey []byte, probe *pbinBitpath, u } // A key with no state reads back as a delete. Landing on an empty slot means - // there simply is no leaf here; landing on one means the leaf has to go, which - // EIP-8297 does not define. + // there simply is no leaf here; landing on one means the leaf keeps its place + // at a zero value, or the removal is one EIP-8297 does not define. if update.Deleted() { - if c.kind == pbinNodeLeaf { - return fmt.Errorf("%w: %x", errPBinDeleteUnsupported, plainKey) + if c.kind != pbinNodeLeaf { + return nil } - return nil + zeroed, err := pbinZeroedLeafUpdate(plainKey) + if err != nil { + return err + } + update = &zeroed } if g.activeRows == 0 { @@ -311,6 +315,19 @@ func (pph *PBinPatriciaHashed) updateCell(plainKey []byte, probe *pbinBitpath, u return nil } +// pbinZeroedLeafUpdate reinterprets an absent read over a live leaf. The domain +// encodes zero and absent the same way, while EIP-8297 has no removal and holds +// a zero value as a present leaf, so a zeroed storage slot keeps its leaf at 32 +// zero bytes. An absent account is a removal the EIP does not describe — its +// encoding is unverified against the reference and would silently change the +// root — so it stays refused. +func pbinZeroedLeafUpdate(plainKey []byte) (Update, error) { + if len(plainKey) != length.Addr+length.Hash { + return Update{}, fmt.Errorf("%w: %x", errPBinDeleteUnsupported, plainKey) + } + return Update{Flags: StorageUpdate}, nil +} + // RootHash hashes whatever the root cell holds. A one-key tree's root is the // leaf itself (eip:133-135) and an empty tree is 32 zero bytes (eip:208), both // of which fall out of hashing the cell rather than special-casing the shape. @@ -798,10 +815,9 @@ func (pph *PBinPatriciaHashed) cellHash(c *pbinCell, path *pbinBitpath) (common. } // loadCellState fills a leaf cell whose plain key arrived from a record and -// whose value therefore did not. -// The leaf is already in the tree, so an absent read means it has to go, which -// EIP-8297 does not define. Applying it would hash a zero-valued leaf and return -// a root with no error. +// whose value therefore did not. The leaf is already in the tree, so an absent +// read is pbinZeroedLeafUpdate's case: a zero value for storage, a refusal for +// an account. func (pph *PBinPatriciaHashed) loadCellState(c *pbinCell) error { if c.accountAddrLen > 0 && !c.loaded.account() { plainKey := c.accountAddr[:c.accountAddrLen] @@ -822,7 +838,11 @@ func (pph *PBinPatriciaHashed) loadCellState(c *pbinCell) error { return fmt.Errorf("pbin: read storage %x: %w", plainKey, err) } if update.Deleted() { - return fmt.Errorf("%w: %x", errPBinDeleteUnsupported, plainKey) + zeroed, err := pbinZeroedLeafUpdate(plainKey) + if err != nil { + return err + } + update = &zeroed } c.setFromUpdate(update) c.loaded = c.loaded.addFlag(cellLoadStorage) diff --git a/execution/commitment/pbin_process_test.go b/execution/commitment/pbin_process_test.go index e3f0cdf8656..ed728f90bc6 100644 --- a/execution/commitment/pbin_process_test.go +++ b/execution/commitment/pbin_process_test.go @@ -310,57 +310,9 @@ func TestPBinProcessMissingStateIsAbsent(t *testing.T) { require.Equal(t, present.oracleRoot(t), root, "keys with no state contribute no leaf") } -// TestPBinProcessRejectsDeletedLeaf is the case an absent state read must not be -// confused with: the key already holds a leaf, so "no state" means the leaf has -// to go — which EIP-8297 does not define. Skipping it would leave the stale leaf -// in the tree and return a root with no error. -func TestPBinProcessRejectsDeletedLeaf(t *testing.T) { - t.Parallel() - - addr := pbinOracleAddr(23) - corpus := new(pbinTestCorpus). - storage(addr, pbinOracleSlot(256), 0x01). - storage(addr, pbinOracleSlot(257), 0x02) - - pph, ms := pbinTestEngine(t) - require.NoError(t, ms.applyPlainUpdates(corpus.plainKeys, corpus.updates)) - pbinTestProcess(t, pph, corpus.plainKeys, corpus.updates) - - gone := new(pbinTestCorpus).storage(addr, pbinOracleSlot(257), 0x02) - require.NoError(t, ms.applyPlainUpdates(gone.plainKeys, []Update{{Flags: DeleteUpdate}})) - - pph.Reset() - upd := WrapKeyUpdates(t, ModeDirect, pbinKeyHasher(), gone.plainKeys, gone.updates) - _, err := pph.Process(context.Background(), upd, "", nil, WarmupConfig{}) - require.ErrorIs(t, err, errPBinDeleteUnsupported) -} - -// TestPBinProcessRejectsDeletedSibling is the same hazard reached through the -// fold rather than the update stream: the vanished leaf is never touched, so it -// is rehydrated from its branch record and hashed with whatever the state read -// returns. Applying an absent read there would hash a zero-valued leaf and -// return a root with no error. -func TestPBinProcessRejectsDeletedSibling(t *testing.T) { - t.Parallel() - - addr := pbinOracleAddr(24) - corpus := new(pbinTestCorpus). - storage(addr, pbinOracleSlot(256), 0x01). - storage(addr, pbinOracleSlot(257), 0x02) - - pph, ms := pbinTestEngine(t) - require.NoError(t, ms.applyPlainUpdates(corpus.plainKeys, corpus.updates)) - pbinTestProcess(t, pph, corpus.plainKeys, corpus.updates) - - gone := new(pbinTestCorpus).storage(addr, pbinOracleSlot(256), 0x01) - require.NoError(t, ms.applyPlainUpdates(gone.plainKeys, []Update{{Flags: DeleteUpdate}})) - - touched := new(pbinTestCorpus).storage(addr, pbinOracleSlot(257), 0x02) - pph.Reset() - upd := WrapKeyUpdates(t, ModeDirect, pbinKeyHasher(), touched.plainKeys, touched.updates) - _, err := pph.Process(context.Background(), upd, "", nil, WarmupConfig{}) - require.ErrorIs(t, err, errPBinDeleteUnsupported) -} +// The absent read over a live leaf — the case this one must not be confused +// with — is pbin_zerovalue_test.go's: storage keeps the leaf at a zero value, +// an account removal stays refused. // TestPBinProcessRepeatedKeyKeepsOneLeaf checks a stem touched twice in one run // still holds a single leaf, so the second visit updates rather than splits. diff --git a/execution/commitment/pbin_zerovalue_test.go b/execution/commitment/pbin_zerovalue_test.go new file mode 100644 index 00000000000..84f66568512 --- /dev/null +++ b/execution/commitment/pbin_zerovalue_test.go @@ -0,0 +1,211 @@ +// Copyright 2026 The Erigon Authors +// This file is part of Erigon. +// +// Erigon is free software: you can redistribute it and/or modify +// it under the terms of the GNU Lesser General Public License as published by +// the Free Software Foundation, either version 3 of the License, or +// (at your option) any later version. +// +// Erigon is distributed in the hope that it will be useful, +// but WITHOUT ANY WARRANTY; without even the implied warranty of +// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +// GNU Lesser General Public License for more details. +// +// You should have received a copy of the GNU Lesser General Public License +// along with Erigon. If not, see . + +package commitment + +import ( + "bytes" + "context" + "testing" + + "github.com/stretchr/testify/require" + + "github.com/erigontech/erigon/common" + "github.com/erigontech/erigon/common/length" +) + +// Zero-vs-absent. The domain has one encoding for both — an absent read — while +// EIP-8297 has no removal and commits a zero value as a present leaf (the +// reference's zero_value_present vector). The engine holds the presence bit the +// domain lacks: a zeroed slot under a live leaf keeps the leaf and commits 32 +// zero bytes, an absent key with no leaf contributes nothing, and an absent +// account over a live leaf is a removal the EIP does not describe (Q1) and stays +// refused. +// +// The expected roots come from the oracle, which the reference's own root +// vectors — zero_value_present among them — pin in pbin_specroots_test.go. + +// TestPBinStorageDeleteKeepsLeafAsPresentZero covers the update-stream side: the +// zeroed slot is touched, so its leaf is in the grid when the absent read lands. +func TestPBinStorageDeleteKeepsLeafAsPresentZero(t *testing.T) { + t.Parallel() + + for _, tc := range []struct { + name string + kept, gone uint64 + }{ + {name: "storage zone", kept: 257, gone: 256}, + {name: "account header zone", kept: 6, gone: 5}, + } { + t.Run(tc.name, func(t *testing.T) { + t.Parallel() + + addr := pbinOracleAddr(41) + stored := new(pbinTestCorpus). + storage(addr, pbinOracleSlot(tc.gone), 0x01). + storage(addr, pbinOracleSlot(tc.kept), 0x02) + + pph, ms := pbinTestEngine(t) + require.NoError(t, ms.applyPlainUpdates(stored.plainKeys, stored.updates)) + before := pbinTestProcess(t, pph, stored.plainKeys, stored.updates) + + zeroed := new(pbinTestCorpus).storage(addr, pbinOracleSlot(tc.gone)) + require.NoError(t, ms.applyPlainUpdates(zeroed.plainKeys, []Update{{Flags: DeleteUpdate}})) + + pph.Reset() + root := pbinTestProcess(t, pph, zeroed.plainKeys, zeroed.updates) + + want := new(pbinTestCorpus). + storage(addr, pbinOracleSlot(tc.gone)). + storage(addr, pbinOracleSlot(tc.kept), 0x02) + require.Equal(t, want.oracleRoot(t), root) + require.NotEqual(t, before, root) + + survivorOnly := new(pbinTestCorpus).storage(addr, pbinOracleSlot(tc.kept), 0x02) + require.NotEqual(t, survivorOnly.oracleRoot(t), root, + "a zeroed slot keeps its leaf: dropping it is a different tree") + }) + } +} + +// TestPBinStorageDeleteOnUntouchedSiblingIsPresentZero is the same rule reached +// through the fold: the zeroed slot is never touched, so its leaf is rehydrated +// from the branch record and hashed with whatever the state read returns. +func TestPBinStorageDeleteOnUntouchedSiblingIsPresentZero(t *testing.T) { + t.Parallel() + + addr := pbinOracleAddr(42) + stored := new(pbinTestCorpus). + storage(addr, pbinOracleSlot(256), 0x01). + storage(addr, pbinOracleSlot(257), 0x02) + + pph, ms := pbinTestEngine(t) + require.NoError(t, ms.applyPlainUpdates(stored.plainKeys, stored.updates)) + pbinTestProcess(t, pph, stored.plainKeys, stored.updates) + + gone := new(pbinTestCorpus).storage(addr, pbinOracleSlot(256)) + require.NoError(t, ms.applyPlainUpdates(gone.plainKeys, []Update{{Flags: DeleteUpdate}})) + + touched := new(pbinTestCorpus).storage(addr, pbinOracleSlot(257), 0x0B) + require.NoError(t, ms.applyPlainUpdates(touched.plainKeys, touched.updates)) + + pph.Reset() + root := pbinTestProcess(t, pph, touched.plainKeys, touched.updates) + + want := new(pbinTestCorpus). + storage(addr, pbinOracleSlot(256)). + storage(addr, pbinOracleSlot(257), 0x0B) + require.Equal(t, want.oracleRoot(t), root) +} + +// TestPBinLoadCellStateAbsentRead pins the two arms apart at the site they share: +// an absent storage read fills the leaf with 32 zero bytes, an absent account +// read refuses. +func TestPBinLoadCellStateAbsentRead(t *testing.T) { + t.Parallel() + + t.Run("storage", func(t *testing.T) { + t.Parallel() + + pph, _ := pbinTestEngine(t) + c := pbinTestEmptyCell() + c.kind = pbinNodeLeaf + c.storageAddrLen = length.Addr + length.Hash + copy(c.storageAddr[:], append(bytes.Clone(pbinOracleAddr(43)), pbinOracleSlot(1000)...)) + + require.NoError(t, pph.loadCellState(&c)) + require.True(t, c.loaded.storage()) + require.False(t, c.Update.Deleted()) + value := pbinEncodeStorageValue(c.Update.Storage[:c.Update.StorageLen]) + require.Equal(t, make([]byte, length.Hash), value[:]) + }) + + t.Run("account", func(t *testing.T) { + t.Parallel() + + pph, _ := pbinTestEngine(t) + c := pbinTestEmptyCell() + c.kind = pbinNodeLeaf + c.accountAddrLen = length.Addr + copy(c.accountAddr[:], pbinOracleAddr(44)) + + require.ErrorIs(t, pph.loadCellState(&c), errPBinDeleteUnsupported) + }) +} + +// TestPBinAccountRemovalStillRefused holds Q1 open: turning an absent account +// into a zero-valued BASIC_DATA leaf is consistent with eip:345-347 but is not +// verified against the reference, and it would silently change the root. +func TestPBinAccountRemovalStillRefused(t *testing.T) { + t.Parallel() + + addr := pbinOracleAddr(45) + stored := new(pbinTestCorpus).account(addr, 3, 7, common.Hash{0x45}) + + pph, ms := pbinTestEngine(t) + require.NoError(t, ms.applyPlainUpdates(stored.plainKeys, stored.updates)) + pbinTestProcess(t, pph, stored.plainKeys, stored.updates) + + require.NoError(t, ms.applyPlainUpdates(stored.plainKeys, []Update{{Flags: DeleteUpdate}})) + pph.Reset() + upd := WrapKeyUpdates(t, ModeDirect, pbinKeyHasher(), stored.plainKeys, stored.updates) + _, err := pph.Process(context.Background(), upd, "", nil, WarmupConfig{}) + require.ErrorIs(t, err, errPBinDeleteUnsupported) +} + +// TestPBinFoldDeleteUnreachableFromProcess guards H12: foldDelete collapses +// nodes the reference leaves in place, and nothing on the Process path may +// reach it. Its only observable is the zero-length record it writes at a +// bit-path key — storeRoot is the sole other zero-length write, and only at the +// root key — so a run that zeroes every leaf it stored must produce none. +func TestPBinFoldDeleteUnreachableFromProcess(t *testing.T) { + t.Parallel() + + addr := pbinOracleAddr(46) + slots := []uint64{0, 1, 63, 256, 257, 258} + stored := new(pbinTestCorpus) + for i, slot := range slots { + stored.storage(addr, pbinOracleSlot(slot), byte(i+1)) + } + stored.account(pbinOracleAddr(47), 1, 2, common.Hash{0x47}) + + pph, ctx, ms := pbinTestStrictEngine(t) + require.NoError(t, ms.applyPlainUpdates(stored.plainKeys, stored.updates)) + pbinTestProcess(t, pph, stored.plainKeys, stored.updates) + + zeroed, want := new(pbinTestCorpus), new(pbinTestCorpus) + for _, slot := range slots { + zeroed.storage(addr, pbinOracleSlot(slot)) + want.storage(addr, pbinOracleSlot(slot)) + } + // An absent key with no leaf of its own is the case a zero write must not be + // confused with: it contributes nothing and leaves no empty row behind. + zeroed.storage(addr, pbinOracleSlot(1<<20)) + want.account(pbinOracleAddr(47), 1, 2, common.Hash{0x47}) + + for i := range zeroed.plainKeys { + require.NoError(t, ms.applyPlainUpdates(zeroed.plainKeys[i:i+1], []Update{{Flags: DeleteUpdate}})) + } + ctx.puts = nil + pph.Reset() + root := pbinTestProcess(t, pph, zeroed.plainKeys, zeroed.updates) + require.Equal(t, want.oracleRoot(t), root) + + require.NotEmpty(t, ctx.puts) + for _, put := range ctx.puts { + require.NotEmpty(t, put.data, "zero-length record at %x: foldDelete ran", put.prefix) + } +} From e2f978f04036e3e8df0c055dcfce93376e312348 Mon Sep 17 00:00:00 2001 From: awskii Date: Thu, 30 Jul 2026 19:35:08 +0700 Subject: [PATCH 33/56] =?UTF-8?q?feat:=20M1a=20gate=20=E2=80=94=20pbin=20o?= =?UTF-8?q?ver=20a=20real=20MDBX=20datadir,=20forward=20run=20vs=20rebuild?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit execution/commitment/backtester, db/state: drive the bin trie over a real MDBX datadir with no consensus. Three arms: forward-run root vs rebuild-from-domains root (full-touch recompute plus RebuildCommitmentFiles over wiped commitment), restart resumption across an aggregator reopen, and byte-for-byte branch records through collation, prune and merge. rebuildCommitmentShard touched the key from next() before testing ok, so at stream exhaustion it touched a 0-length plain key. Hex hashed it into a spurious absent update; pbin panicked. Skip the touch for an empty key. M1a has no header-root oracle: every arm is a self-consistency check over the same engine, so green means deterministic, not correct. --- db/state/squeeze.go | 8 +- docs/plans/20260730-pbin-m1-local-el.md | 17 +- .../commitment/backtester/pbin_m1a_test.go | 399 ++++++++++++++++++ 3 files changed, 414 insertions(+), 10 deletions(-) create mode 100644 execution/commitment/backtester/pbin_m1a_test.go diff --git a/db/state/squeeze.go b/db/state/squeeze.go index 566dbec47d7..71032d1d63c 100644 --- a/db/state/squeeze.go +++ b/db/state/squeeze.go @@ -1166,9 +1166,13 @@ func rebuildCommitmentShard(ctx context.Context, sd *execctx.SharedDomains, tx k sf := time.Now() var processed uint64 + // next() signals "no more keys" as (false, nil) but a shard boundary as + // (false, key), so the key has to be checked separately from ok. for ok, key := next(); ; ok, key = next() { - sd.GetCommitmentCtx().TouchKey(kv.AccountsDomain, string(key), nil) - processed++ + if len(key) > 0 { + sd.GetCommitmentCtx().TouchKey(kv.AccountsDomain, string(key), nil) + processed++ + } if !ok { break } diff --git a/docs/plans/20260730-pbin-m1-local-el.md b/docs/plans/20260730-pbin-m1-local-el.md index b33f2d6c9ef..0c5af43ac45 100644 --- a/docs/plans/20260730-pbin-m1-local-el.md +++ b/docs/plans/20260730-pbin-m1-local-el.md @@ -123,7 +123,7 @@ Do not treat these as established: - `pbin_branch.go` record field-bit layout beyond the leaf-kind rejection at `:156-165`. **Verify before designing Task 13's value-in-record field.** - ~~The "grid arrays are restorable-as-zero" argument underpinning Task 5's ~160-byte blob~~ — **proven in Task 5** (see the Task 5 checklist for the read/write-site audit); the root-cell blob landed. -- Whether pbin branch records are truly opaque to the pass-through merge path (believed yes with references off, not exercised). +- ~~Whether pbin branch records are truly opaque to the pass-through merge path~~ — **exercised in Task 10** with references off: collation, prune and merge round-trip the records byte-for-byte, checked against a db snapshot with a positive count of records provably served from files. - ~~Task 8's deferral mis-attribution~~ — **Task 8**: still no concrete failing sequence, and the exposure is bounded: deferral is only ever requested by `ExecV3` (fork validation / parallel apply), and the fork-validation writes it would mis-route land in a validation overlay that is never flushed. The guards are structural — bin cannot reach the deferred path at all now. ## What Goes Where @@ -281,13 +281,14 @@ Production keeps Keccak-256. This task only makes the **test** path run the whol **Files:** - Create: `execution/commitment/backtester/pbin_m1a_test.go` - -- [ ] write a test driving pbin over a real MDBX datadir via `RebuildCommitmentFiles` or the backtester, with no consensus -- [ ] assert the forward-run root equals the rebuild-from-domains root over the same input -- [ ] assert a restart mid-run resumes to the same root (exercises Task 5) -- [ ] assert collation and merge preserve branch records byte-for-byte -- [ ] record in this plan that M1a has **no header-root oracle** and is not acceptance -- [ ] run tests — must pass before task 11 +- Modify: `db/state/squeeze.go` (➕ phantom empty-key touch in `rebuildCommitmentShard`) + +- [x] write a test driving pbin over a real MDBX datadir via `RebuildCommitmentFiles` or the backtester, with no consensus — `backtester_test` builds its own datadir (real MDBX under a temp dir + real `.kv` domain files) and drives it through `execctx.SharedDomains` with `statecfg.ExperimentalBinCommitment` on; every SD open asserts the trie really is `*PBinPatriciaHashed`, so a hex fallback cannot make the suite vacuous. The `Backtester` type itself is unusable here — it needs a synced datadir with canonical headers +- [x] assert the forward-run root equals the rebuild-from-domains root over the same input — `TestPBinM1AForwardRunMatchesRebuildFromDomains`. Two arms: a full-touch recompute over the same datadir, and `RebuildCommitmentFiles` after wiping every commitment record and file. ➕ **found a bug in shared code**: `rebuildCommitmentShard` touches the key from `next()` before testing `ok`, so at stream exhaustion it touches a 0-length plain key. Hex hashes it into a spurious absent update; pbin panics (a plain key is neither 20 nor 52 bytes). Fixed by skipping the touch for an empty key — `next()` signals exhaustion as `(false, nil)` but a shard boundary as `(false, key)`, so the key has to be checked separately from `ok`. ➕ the comparison point is the **last collated** step boundary, not the last forward root: collation always leaves the newest step in the db, so a files-only rebuild reproduces the root as of `TxNumsInFiles` +- [x] assert a restart mid-run resumes to the same root (exercises Task 5) — `TestPBinM1ARestartResumesToSameRoot`: two halves of one input across an aggregator reopen, second half touching only its own keys, must reach the uninterrupted root; plus a fresh SD restoring the saved root before folding anything. ⚠️ **correction**: this does not exercise Task 5's state blob. `RootHash()` calls `loadRoot()` whenever `rootChecked` is false, so a gutted `SetState` still returns the right root — for pbin the restart carrier is Task 3's root record in the commitment domain, and the blob is a cache. Verified by mutation: gutting `SetState` leaves this test and `TestPBinRestartRoundTripDeepPath` green, and only the blob's own unit tests (`TestPBinStateBlobRoundTripsFlags`, `TestPBinSetStateRejectsForeignBlob`) go red +- [x] assert collation and merge preserve branch records byte-for-byte — `TestPBinM1ABranchRecordsSurviveCollationAndMerge`: latest records snapshotted from the db before collation must read back identically after `BuildFiles` + prune + `MergeLoop`, and again after a folder reopen. Non-vacuity is asserted, not assumed: every record is db-resident before collation, and a positive number (12 of 36) are gone from `TblCommitmentVals` afterwards, so their latest read can only come from the files +- [x] record in this plan that M1a has **no header-root oracle** and is not acceptance — stated in the file's package doc and here: nothing outside the engine validates these roots. Both rebuild arms and the restart arm are self-consistency checks over the same engine, so a green M1a means deterministic, not correct. H8 (stale high code chunks) is also out of reach until Task 12 puts code in the tree, which is where the forward-vs-rebuild comparison first gets a chance to fail for a real reason +- [x] run tests — `go test ./execution/commitment/... ./db/state/... -count=1` and `./execution/stagedsync/... -short` green, `go build ./...` clean, `make lint` clean twice ### Task 11: code_size on Update diff --git a/execution/commitment/backtester/pbin_m1a_test.go b/execution/commitment/backtester/pbin_m1a_test.go new file mode 100644 index 00000000000..00b7c80222b --- /dev/null +++ b/execution/commitment/backtester/pbin_m1a_test.go @@ -0,0 +1,399 @@ +// Copyright 2026 The Erigon Authors +// This file is part of Erigon. +// +// Erigon is free software: you can redistribute it and/or modify +// it under the terms of the GNU Lesser General Public License as published by +// the Free Software Foundation, either version 3 of the License, or +// (at your option) any later version. +// +// Erigon is distributed in the hope that it will be useful, +// but WITHOUT ANY WARRANTY; without even the implied warranty of +// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +// GNU Lesser General Public License for more details. +// +// You should have received a copy of the GNU Lesser General Public License +// along with Erigon. If not, see . + +// These tests drive the bin commitment trie over a real MDBX datadir with no +// consensus layer, so no header validates the roots from outside. The only +// cross-check available is determinism: a forward run and a rebuild that has only +// the account and storage domains to work from must agree. A green run therefore +// says the engine is self-consistent, not that it is correct. +package backtester_test + +import ( + "bytes" + "strings" + "testing" + "time" + + "github.com/c2h5oh/datasize" + "github.com/holiman/uint256" + "github.com/stretchr/testify/require" + + "github.com/erigontech/erigon/common/dir" + "github.com/erigontech/erigon/common/length" + "github.com/erigontech/erigon/common/log/v3" + "github.com/erigontech/erigon/db/datadir" + "github.com/erigontech/erigon/db/kv" + "github.com/erigontech/erigon/db/kv/dbcfg" + "github.com/erigontech/erigon/db/kv/mdbx" + "github.com/erigontech/erigon/db/kv/rawdbv3" + "github.com/erigontech/erigon/db/kv/temporal" + "github.com/erigontech/erigon/db/state" + "github.com/erigontech/erigon/db/state/execctx" + "github.com/erigontech/erigon/db/state/statecfg" + "github.com/erigontech/erigon/execution/commitment" + "github.com/erigontech/erigon/execution/commitment/commitmentdb" + "github.com/erigontech/erigon/execution/types/accounts" +) + +const ( + pbinM1AStepSize = uint64(8) + pbinM1AAccounts = 6 + pbinM1ASlots = 4 +) + +// pbinM1ABinVariant makes PickTrieVariant() resolve to the bin trie. The flag is +// process-wide, so these tests never run in parallel. +func pbinM1ABinVariant(t *testing.T) { + t.Helper() + orig := statecfg.ExperimentalBinCommitment + t.Cleanup(func() { statecfg.ExperimentalBinCommitment = orig }) + statecfg.ExperimentalBinCommitment = true +} + +func pbinM1ANewAgg(t *testing.T, rawDB kv.RwDB, dirs datadir.Dirs, stepSize uint64) *state.Aggregator { + t.Helper() + agg := state.NewTest(dirs).StepSize(stepSize).Logger(log.New()).MustOpen(t.Context(), rawDB) + t.Cleanup(agg.Close) + // Referenced branches rewrite bytes at hex cell offsets during merge. Production + // refuses the combination when resolving settings, which a test aggregator built + // straight from NewTest does not go through. + agg.ForTestReferencesInCommitmentBranches(kv.CommitmentDomain, false) + require.NoError(t, agg.OpenFolder()) + return agg +} + +func pbinM1ANewDatadir(t *testing.T, stepSize uint64) (kv.TemporalRwDB, *state.Aggregator, datadir.Dirs) { + t.Helper() + dirs := datadir.New(t.TempDir()) + rawDB := mdbx.New(dbcfg.ChainDB, log.New()).InMem(t, dirs.Chaindata). + GrowthStep(32 * datasize.MB).MapSize(2 * datasize.GB).MustOpen() + t.Cleanup(rawDB.Close) + + agg := pbinM1ANewAgg(t, rawDB, dirs, stepSize) + db, err := temporal.New(rawDB, agg, nil) + require.NoError(t, err) + t.Cleanup(db.Close) + return db, agg, dirs +} + +// pbinM1AReopen closes the aggregator and reopens it over the same folder — the +// file-visibility half of a node restart. +func pbinM1AReopen(t *testing.T, db kv.TemporalRwDB, agg *state.Aggregator, dirs datadir.Dirs, stepSize uint64) (kv.TemporalRwDB, *state.Aggregator) { + t.Helper() + agg.Close() + newAgg := pbinM1ANewAgg(t, db, dirs, stepSize) + newDB, err := temporal.New(db, newAgg, nil) + require.NoError(t, err) + return newDB, newAgg +} + +func pbinM1AAddr(i int) []byte { + a := make([]byte, length.Addr) + a[0] = 0xa0 + a[1] = byte(i) + a[length.Addr-1] = byte(i*7 + 1) + return a +} + +func pbinM1ASlotKey(addr []byte, j int) []byte { + k := make([]byte, length.Addr+length.Hash) + copy(k, addr) + k[length.Addr] = byte(j) + k[len(k)-1] = byte(j*13 + 3) + return k +} + +// pbinM1ABinSharedDomains opens a SharedDomains and pins that it really runs the +// bin trie — a hex fallback would make every assertion below vacuous. +func pbinM1ABinSharedDomains(t *testing.T, tx kv.TemporalTx) *execctx.SharedDomains { + t.Helper() + sd, err := execctx.NewSharedDomains(t.Context(), tx, log.New()) + require.NoError(t, err) + require.IsType(t, &commitment.PBinPatriciaHashed{}, sd.GetCommitmentCtx().Trie()) + return sd +} + +// pbinM1AForwardRun writes accounts and storage for txNums [fromTx, toTx), saving +// the commitment state at every step boundary. It returns the root at each of those +// boundaries keyed by the boundary txNum, plus the last root. +func pbinM1AForwardRun(t *testing.T, db kv.TemporalRwDB, stepSize, fromTx, toTx uint64) (map[uint64][]byte, []byte) { + t.Helper() + rwTx, err := db.BeginTemporalRw(t.Context()) + require.NoError(t, err) + defer rwTx.Rollback() + + sd := pbinM1ABinSharedDomains(t, rwTx) + defer sd.Close() + + roots := make(map[uint64][]byte) + var last []byte + for txNum := fromTx; txNum < toTx; txNum++ { + for i := range pbinM1AAccounts { + addr := pbinM1AAddr(i) + acc := accounts.Account{ + Nonce: txNum + 1, + Balance: *uint256.NewInt(txNum*1_000 + uint64(i)), + CodeHash: accounts.EmptyCodeHash, + } + prev, _, err := sd.GetLatest(kv.AccountsDomain, rwTx, addr) + require.NoError(t, err) + require.NoError(t, sd.DomainPut(kv.AccountsDomain, rwTx, addr, accounts.SerialiseV3(&acc), txNum, prev)) + + for j := range pbinM1ASlots { + sk := pbinM1ASlotKey(addr, j) + val := []byte{byte(txNum + 1), byte(i + 1), byte(j + 1)} + prev, _, err := sd.GetLatest(kv.StorageDomain, rwTx, sk) + require.NoError(t, err) + require.NoError(t, sd.DomainPut(kv.StorageDomain, rwTx, sk, val, txNum, prev)) + } + } + if (txNum+1)%stepSize == 0 { + last, err = sd.ComputeCommitment(t.Context(), rwTx, true, 0, txNum, "pbin-m1a", nil) + require.NoError(t, err) + require.NotEmpty(t, last) + roots[txNum] = bytes.Clone(last) + } + } + require.NoError(t, sd.Flush(t.Context(), rwTx)) + require.NoError(t, rwTx.Commit()) + return roots, last +} + +// pbinM1ARecomputeRoot re-folds the whole tree from the account and storage +// domains: every leaf is touched, so no leaf value comes from a branch record. +func pbinM1ARecomputeRoot(t *testing.T, db kv.TemporalRwDB) []byte { + t.Helper() + rwTx, err := db.BeginTemporalRw(t.Context()) + require.NoError(t, err) + defer rwTx.Rollback() + + sd := pbinM1ABinSharedDomains(t, rwTx) + defer sd.Close() + + for _, d := range []kv.Domain{kv.AccountsDomain, kv.StorageDomain} { + it, err := rwTx.Debug().RangeLatest(d, nil, nil, -1) + require.NoError(t, err) + for it.HasNext() { + k, _, err := it.Next() + require.NoError(t, err) + sd.GetCommitmentCtx().TouchKey(d, string(k), nil) + } + it.Close() + } + root, err := sd.ComputeCommitment(t.Context(), rwTx, false, 0, 0, "pbin-m1a-recompute", nil) + require.NoError(t, err) + return root +} + +// pbinM1ARestoredRoot returns the root a freshly opened SharedDomains restores +// from the saved commitment state, without folding anything. +func pbinM1ARestoredRoot(t *testing.T, db kv.TemporalRwDB) []byte { + t.Helper() + tx, err := db.BeginTemporalRw(t.Context()) + require.NoError(t, err) + defer tx.Rollback() + + sd := pbinM1ABinSharedDomains(t, tx) + defer sd.Close() + root, err := sd.GetCommitmentCtx().Trie().RootHash() + require.NoError(t, err) + return root +} + +// pbinM1ACollatedTxNum returns the first txNum not yet in the account and storage +// files. Collation always leaves the newest step in the db, so a files-only rebuild +// reproduces the root as of this boundary, not the last one the forward run computed. +func pbinM1ACollatedTxNum(t *testing.T, db kv.TemporalRwDB) uint64 { + t.Helper() + tx, err := db.BeginTemporalRo(t.Context()) + require.NoError(t, err) + defer tx.Rollback() + at := state.AggTx(tx) + accTxNum := at.TxNumsInFiles(kv.AccountsDomain) + require.Equal(t, accTxNum, at.TxNumsInFiles(kv.StorageDomain), + "the rebuild reads both domains at one boundary") + return accTxNum +} + +// pbinM1ABranchRecords reads the latest commitment branch records, skipping the +// commitment-state record. +func pbinM1ABranchRecords(t *testing.T, db kv.TemporalRwDB) map[string][]byte { + t.Helper() + tx, err := db.BeginTemporalRo(t.Context()) + require.NoError(t, err) + defer tx.Rollback() + + out := make(map[string][]byte) + it, err := tx.Debug().RangeLatest(kv.CommitmentDomain, nil, nil, -1) + require.NoError(t, err) + defer it.Close() + for it.HasNext() { + k, v, err := it.Next() + require.NoError(t, err) + if bytes.Equal(k, commitmentdb.KeyCommitmentState) { + continue + } + out[string(k)] = bytes.Clone(v) + } + return out +} + +// pbinM1AFileServedRecords counts the branch records that are gone from the db +// table, so a latest read of them can only come from the collated files. +func pbinM1AFileServedRecords(t *testing.T, db kv.TemporalRwDB, records map[string][]byte) int { + t.Helper() + tx, err := db.BeginTemporalRo(t.Context()) + require.NoError(t, err) + defer tx.Rollback() + + var fromFiles int + for k := range records { + v, err := tx.GetOne(kv.TblCommitmentVals, []byte(k)) + require.NoError(t, err) + if len(v) == 0 { + fromFiles++ + } + } + return fromFiles +} + +// pbinM1AWipeCommitment removes every commitment record from the db tables and +// every commitment file from the snapshot dir, so a rebuild has to derive the tree +// from the account and storage domains alone. +func pbinM1AWipeCommitment(t *testing.T, db kv.TemporalRwDB, agg *state.Aggregator, dirs datadir.Dirs, stepSize uint64) (kv.TemporalRwDB, *state.Aggregator) { + t.Helper() + rwTx, err := db.BeginRw(t.Context()) + require.NoError(t, err) + defer rwTx.Rollback() + tables, err := rwTx.ListTables() + require.NoError(t, err) + commitStr := kv.CommitmentDomain.String() + for _, b := range tables { + if strings.Contains(strings.ToLower(b), commitStr) { + require.NoError(t, rwTx.ClearTable(b)) + } + } + require.NoError(t, rwTx.Commit()) + + // Windows refuses to remove a still-mapped file, so drop the file handles first. + agg.Close() + paths, err := dir.ListFiles(dirs.SnapDomain, ".kv") + require.NoError(t, err) + for _, p := range paths { + if !strings.Contains(p, commitStr) { + continue + } + require.NoError(t, dir.RemoveFile(p)) + base := strings.TrimSuffix(p, ".kv") + for _, ext := range []string{".kvi", ".kvei", ".bt"} { + _ = dir.RemoveFile(base + ext) // best-effort, may not exist + } + } + + newAgg := pbinM1ANewAgg(t, db, dirs, stepSize) + newDB, err := temporal.New(db, newAgg, nil) + require.NoError(t, err) + return newDB, newAgg +} + +// TestPBinM1AForwardRunMatchesRebuildFromDomains is the M1a gate: over the same +// state the incremental forward fold and a rebuild that starts from wiped +// commitment must produce the same root. +func TestPBinM1AForwardRunMatchesRebuildFromDomains(t *testing.T) { + pbinM1ABinVariant(t) + txCount := 4 * pbinM1AStepSize + + db, agg, dirs := pbinM1ANewDatadir(t, pbinM1AStepSize) + stepRoots, forwardRoot := pbinM1AForwardRun(t, db, pbinM1AStepSize, 0, txCount) + require.NoError(t, agg.BuildFiles(txCount)) + + require.Equal(t, forwardRoot, pbinM1ARecomputeRoot(t, db), + "a full-touch recompute over the same datadir must reproduce the forward root") + + collatedTxNum := pbinM1ACollatedTxNum(t, db) + require.Positive(t, collatedTxNum, "collation must produce account and storage files to rebuild from") + wantRoot := stepRoots[collatedTxNum-1] + require.NotEmpty(t, wantRoot, "the collated boundary must be one the forward run computed a root at") + + db, agg = pbinM1AWipeCommitment(t, db, agg, dirs, pbinM1AStepSize) + require.Empty(t, pbinM1ABranchRecords(t, db), "the wipe must leave no commitment records") + + rebuiltRoot, err := state.RebuildCommitmentFiles(t.Context(), db, &rawdbv3.TxNums, log.New(), false) + require.NoError(t, err) + require.Equal(t, wantRoot, rebuiltRoot, "rebuild-from-domains must reproduce the forward root") + + require.NoError(t, agg.OpenFolder()) + require.NoError(t, agg.BuildMissedAccessors(t.Context(), 1)) + require.Equal(t, wantRoot, pbinM1ARestoredRoot(t, db), + "the rebuilt files must carry a trie state that restores to the rebuilt root") + require.Equal(t, forwardRoot, pbinM1ARecomputeRoot(t, db), + "the rebuilt commitment records must fold back to the forward root") +} + +// TestPBinM1ARestartResumesToSameRoot restarts between two halves of the same +// input. The second half touches only its own keys, so the root can only come out +// right if the saved trie state and the persisted branch records both round-trip. +func TestPBinM1ARestartResumesToSameRoot(t *testing.T) { + pbinM1ABinVariant(t) + half := 2 * pbinM1AStepSize + + uninterrupted, _, _ := pbinM1ANewDatadir(t, pbinM1AStepSize) + _, wantRoot := pbinM1AForwardRun(t, uninterrupted, pbinM1AStepSize, 0, 2*half) + + restarted, agg, dirs := pbinM1ANewDatadir(t, pbinM1AStepSize) + _, firstRoot := pbinM1AForwardRun(t, restarted, pbinM1AStepSize, 0, half) + require.NotEqual(t, wantRoot, firstRoot, "the two halves must not write identical state") + + restarted, _ = pbinM1AReopen(t, restarted, agg, dirs, pbinM1AStepSize) + require.Equal(t, firstRoot, pbinM1ARestoredRoot(t, restarted), + "a restart must restore the saved root before folding anything") + + _, resumedRoot := pbinM1AForwardRun(t, restarted, pbinM1AStepSize, half, 2*half) + require.Equal(t, wantRoot, resumedRoot, "a restart mid-run must resume to the uninterrupted root") +} + +// TestPBinM1ABranchRecordsSurviveCollationAndMerge pins that collation and merge +// are byte-transparent for bin branch records. The db is pruned after collation, so +// the post-merge reads come from the files. +func TestPBinM1ABranchRecordsSurviveCollationAndMerge(t *testing.T) { + pbinM1ABinVariant(t) + txCount := 4 * pbinM1AStepSize + + db, agg, dirs := pbinM1ANewDatadir(t, pbinM1AStepSize) + pbinM1AForwardRun(t, db, pbinM1AStepSize, 0, txCount) + + inDB := pbinM1ABranchRecords(t, db) + require.NotEmpty(t, inDB) + require.Zero(t, pbinM1AFileServedRecords(t, db, inDB), "before collation every record lives in the db") + + require.NoError(t, agg.BuildFiles(txCount)) + rwTx, err := db.BeginTemporalRw(t.Context()) + require.NoError(t, err) + defer rwTx.Rollback() + _, err = rwTx.PruneSmallBatches(t.Context(), time.Hour) + require.NoError(t, err) + require.NoError(t, rwTx.Commit()) + require.NoError(t, agg.MergeLoop(t.Context())) + require.Positive(t, pbinM1AFileServedRecords(t, db, inDB), + "pruning must move records out of the db, otherwise the reads below never reach the files") + + require.Equal(t, inDB, pbinM1ABranchRecords(t, db), + "collation and merge must preserve bin branch records byte-for-byte") + + db, _ = pbinM1AReopen(t, db, agg, dirs, pbinM1AStepSize) + require.Equal(t, inDB, pbinM1ABranchRecords(t, db), + "the records must read back identically after a folder reopen") +} From 23dbb63e1f644ea9e9f064973f7b1095a385d6dc Mon Sep 17 00:00:00 2001 From: awskii Date: Thu, 30 Jul 2026 19:55:07 +0700 Subject: [PATCH 34/56] feat: code_size on Update, read from the CodeDomain for the bin trie MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit EIP-8297 packs code_size into the BASIC_DATA leaf, and pbin was forcing it to zero, so every code-bearing account hashed wrong. Update gains a CodeSize field carried under the CodeUpdate flag, TrieContext.Account resolves it from the CodeDomain, and the leaf encoder passes it through. The read is gated on the variant: only the bin trie hashes code_size, so hex takes no extra domain read per code-bearing account. code_size follows the account's own code hash rather than CodeDomain presence, so a cleared EIP-7702 delegation's residue stays out of the root; the mirror case — a code hash with no code behind it — errors instead of hashing as zero. --- docs/plans/20260730-pbin-m1-local-el.md | 17 +- execution/commitment/commitment.go | 19 ++- .../commitmentdb/commitment_context.go | 35 ++-- .../commitmentdb/pbin_codesize_test.go | 156 ++++++++++++++++++ .../commitment/hex_patricia_hashed_test.go | 2 + execution/commitment/pbin_codesize_test.go | 119 +++++++++++++ execution/commitment/pbin_hash.go | 4 +- execution/commitment/pbin_patricia_hashed.go | 8 +- execution/commitment/pbin_process_test.go | 8 +- execution/commitment/pbin_specengine_test.go | 6 +- 10 files changed, 341 insertions(+), 33 deletions(-) create mode 100644 execution/commitment/commitmentdb/pbin_codesize_test.go create mode 100644 execution/commitment/pbin_codesize_test.go diff --git a/docs/plans/20260730-pbin-m1-local-el.md b/docs/plans/20260730-pbin-m1-local-el.md index 0c5af43ac45..cfefda1b3ca 100644 --- a/docs/plans/20260730-pbin-m1-local-el.md +++ b/docs/plans/20260730-pbin-m1-local-el.md @@ -297,14 +297,15 @@ Production keeps Keccak-256. This task only makes the **test** path run the whol - Modify: `execution/commitment/commitmentdb/commitment_context.go` - Modify: `execution/commitment/pbin_hash.go` - Create: `execution/commitment/pbin_codesize_test.go` - -- [ ] write a failing test asserting BASIC_DATA for a code-bearing account carries the real `code_size`, checked against `basic_data_vectors` -- [ ] add the `code_size` field to `Update` plus handling in `Reset`/`Copy`/`Merge`/`Encode`/`Decode`/`String` -- [ ] populate it at `TrieContext.Account` (`:1026-1070`) by reading `kv.CodeDomain` unconditionally -- [ ] delete the wrong comment at `pbin_hash.go:138-139` and pass the real size instead of `0` -- [ ] decide and test the cleared-7702-residue case explicitly — the existing benign-residue license no longer holds (guards H9) -- [ ] write a test asserting the push side is inert for pbin, so nobody patches `calc_state.go` expecting code to arrive -- [ ] run tests — must pass before task 12 +- Create: `execution/commitment/commitmentdb/pbin_codesize_test.go` (➕ the read side) + +- [x] write a failing test asserting BASIC_DATA for a code-bearing account carries the real `code_size`, checked against `basic_data_vectors` — `TestPBinBasicDataLeafCarriesCodeSize` drives `pbinLeafValue` over every vector; `TestPBinEngineRootCarriesCodeSize` then takes the size through the whole engine (context read → cell merge → leaf hash) and asserts a size-less variant of the same account roots differently. Both red before +- [x] add the `code_size` field to `Update` plus handling in `Reset`/`Copy`/`Merge`/`Encode`/`Decode`/`String` — `CodeSize uint64`, carried under the existing `CodeUpdate` flag at every hook (a size and a hash describe the same code, so a merge can never take one from the old account and the other from the new). Encode appends a varint inside the `CodeUpdate` block; ➕ `TestUpdate_EncodeDecode`/`TestUpdate_Merge` in `hex_patricia_hashed_test.go` gained the field, not in the planned file list +- [x] populate it at `TrieContext.Account` (`:1026-1070`) by reading `kv.CodeDomain` unconditionally — "unconditionally" in the sense that matters: the read no longer hides behind `dbg.AssertEnabled`. It is gated on `TrieContext.readCodeSize`, set from the variant at the one construction site the bin trie can reach (`trieContext`), so hex takes no extra domain read per code-bearing account. The warmup/concurrent factories are deliberately left alone: they need `paraTrieDB` and only ever serve page-cache warmup or a `*ParallelPatriciaHashed` fold, neither of which bin can reach +- [x] delete the wrong comment at `pbin_hash.go:138-139` and pass the real size instead of `0` +- [x] decide and test the cleared-7702-residue case explicitly — the existing benign-residue license no longer holds (guards H9) — **decision: code_size follows the account's own code hash, never CodeDomain presence.** A code-less account keeps code_size 0 whatever residue a cleared delegation left behind, so the tolerated inconsistency stays out of the root (`TestPBinTrieContextIgnoresClearedDelegationResidue`). The mirror case cannot be tolerated: a code-bearing account with no code behind it would hash as code_size 0 and produce a silently wrong root, so it errors (`TestPBinTrieContextRefusesCodeBearingAccountWithoutCode`). That is only reachable under bin — the overlay callers it would otherwise break (`eth_simulateV1`) are already refused by Task 7 +- [x] write a test asserting the push side is inert for pbin, so nobody patches `calc_state.go` expecting code to arrive — `TestPBinPushSideNeverDeliversCode`: the bin variant overrides the requested mode to `ModeDirect`, and a `TouchCode` touch reaches `HashSort` as a nil update. Pinning test — it passes against current behaviour by design and fails if the push side ever starts carrying values +- [x] run tests — `./execution/commitment/... ./db/state/... ./execution/state/genesiswrite ./db/integrity` and `./execution/stagedsync/... -short` green, `go build ./...` clean, `make lint` clean twice. ➕ the tests for the read side live in `commitmentdb/pbin_codesize_test.go` (the trie context is in that package), not in the planned `execution/commitment/pbin_codesize_test.go`; the wiring test `TestPBinSharedDomainsReadsCodeSizeUnderBin` pins variant → read and is non-vacuous by mutation ### Task 12: chunkify_code and header code chunks diff --git a/execution/commitment/commitment.go b/execution/commitment/commitment.go index c49b83f9354..59e6949bc8b 100644 --- a/execution/commitment/commitment.go +++ b/execution/commitment/commitment.go @@ -2206,6 +2206,9 @@ type Update struct { Flags UpdateFlags Balance uint256.Int Nonce uint64 + // CodeSize travels with CodeHash and is read only by the binary trie, whose + // BASIC_DATA leaf packs it (eip-8297). + CodeSize uint64 } func (u *Update) Reset() { @@ -2214,6 +2217,7 @@ func (u *Update) Reset() { u.Nonce = 0 u.StorageLen = 0 u.CodeHash = empty.CodeHash + u.CodeSize = 0 } // Copy creates a deep copy of the Update. @@ -2227,6 +2231,7 @@ func (u *Update) Copy() *Update { StorageLen: u.StorageLen, Flags: u.Flags, Nonce: u.Nonce, + CodeSize: u.CodeSize, } c.Balance.Set(&u.Balance) return c @@ -2251,6 +2256,7 @@ func (u *Update) Merge(b *Update) { if b.Flags&CodeUpdate != 0 { u.Flags |= CodeUpdate copy(u.CodeHash[:], b.CodeHash[:]) + u.CodeSize = b.CodeSize } if b.Flags&StorageUpdate != 0 { u.Flags |= StorageUpdate @@ -2271,6 +2277,8 @@ func (u *Update) Encode(buf []byte, numBuf []byte) []byte { } if u.Flags&CodeUpdate != 0 { buf = append(buf, u.CodeHash[:]...) + n := binary.PutUvarint(numBuf, u.CodeSize) + buf = append(buf, numBuf[:n]...) } if u.Flags&StorageUpdate != 0 { n := binary.PutUvarint(numBuf, uint64(u.StorageLen)) @@ -2323,6 +2331,15 @@ func (u *Update) Decode(buf []byte, pos int) (int, error) { } copy(u.CodeHash[:], buf[pos:pos+32]) pos += length.Hash + var n int + u.CodeSize, n = binary.Uvarint(buf[pos:]) + if n == 0 { + return 0, errors.New("decode Update: buffer too small for codeSize") + } + if n < 0 { + return 0, errors.New("decode Update: codeSize overflow") + } + pos += n } if u.Flags&StorageUpdate != 0 { l, n := binary.Uvarint(buf[pos:]) @@ -2356,7 +2373,7 @@ func (u *Update) String() string { sb.WriteString(fmt.Sprintf(", Nonce: [%d]", u.Nonce)) } if u.Flags&CodeUpdate != 0 { - sb.WriteString(fmt.Sprintf(", CodeHash: [%x]", u.CodeHash)) + sb.WriteString(fmt.Sprintf(", CodeHash: [%x], CodeSize: [%d]", u.CodeHash, u.CodeSize)) } if u.Flags&StorageUpdate != 0 { sb.WriteString(fmt.Sprintf(", Storage: [%x]", u.Storage[:u.StorageLen])) diff --git a/execution/commitment/commitmentdb/commitment_context.go b/execution/commitment/commitmentdb/commitment_context.go index 9ebb6f8b11a..1b1a8208977 100644 --- a/execution/commitment/commitmentdb/commitment_context.go +++ b/execution/commitment/commitmentdb/commitment_context.go @@ -274,12 +274,13 @@ func NewSharedDomainsCommitmentContext(sd sd, mode commitment.Mode, tmpDir strin // exclusively. Warmup/concurrent-mount readers get their own via the factories. func (sdc *SharedDomainsCommitmentContext) trieContext(tx kv.TemporalTx, blockNum, txNum uint64, readCtx context.Context) *TrieContext { mainTtx := &TrieContext{ - getter: sdc.sharedDomains.AsGetter(tx), - putter: sdc.sharedDomains.AsPutDel(tx), - stepSize: sdc.sharedDomains.StepSize(), - txNum: txNum, - blockNum: blockNum, - traceW: sdc.traceW, + getter: sdc.sharedDomains.AsGetter(tx), + putter: sdc.sharedDomains.AsPutDel(tx), + stepSize: sdc.sharedDomains.StepSize(), + txNum: txNum, + blockNum: blockNum, + traceW: sdc.traceW, + readCodeSize: sdc.variant == commitment.VariantBinPatriciaTrie, } if sdc.stateReader != nil { mainTtx.stateReader = sdc.stateReader.CloneForWorker(readCtx, tx) @@ -985,8 +986,13 @@ type TrieContext struct { traceW io.Writer // nil = disabled; traces branch reads/writes (see [SDC] lines) stateReader StateReader localCollector *etl.Collector // per-goroutine collector for concurrent PutBranch + // readCodeSize makes Account resolve the account's code length. Only the + // binary trie hashes code_size, and the extra CodeDomain read is not free. + readCodeSize bool } +func (sdc *TrieContext) SetReadCodeSize(v bool) { sdc.readCodeSize = v } + // NewTrieContextRo creates a read-only TrieContext suitable for TrieReader lookups. // Only Branch() is functional; PutBranch/Account/Storage will return errors or nil. func NewTrieContextRo(reader StateReader, stepSize uint64) *TrieContext { @@ -1058,11 +1064,13 @@ func (sdc *TrieContext) Account(plainKey []byte) (u *commitment.Update, err erro u.CodeHash = acc.CodeHash.Value() } - // Verify only code-bearing accounts whose code is actually in the domain, - // and never fold the read into u. A cleared EIP-7702 delegation leaves a - // benign CodeDomain residue on a code-less account, and eth_simulateV1 - // overrides put code in an overlay the domain read doesn't see. - if dbg.AssertEnabled && !acc.IsEmptyCodeHash() { + // The read is keyed on the account's own code hash, never on what the + // CodeDomain happens to hold: a cleared EIP-7702 delegation leaves a residue + // there that no longer belongs to the account, so a code-less account keeps + // code_size 0. A code-bearing account with no code behind it would hash as + // code_size 0 instead — an eth_simulateV1 overlay the domain read doesn't + // see, or a truncated datadir — so it is an error rather than a wrong root. + if (sdc.readCodeSize || dbg.AssertEnabled) && !acc.IsEmptyCodeHash() { code, _, err := sdc.readDomain(kv.CodeDomain, plainKey) if err != nil { return nil, err @@ -1071,6 +1079,11 @@ func (sdc *TrieContext) Account(plainKey []byte) (u *commitment.Update, err erro if codeHash := crypto.Keccak256Hash(code); acc.CodeHash.Value() != codeHash { return nil, fmt.Errorf("code hash mismatch: account '%x' != codeHash '%x'", acc.CodeHash, codeHash[:]) } + } else if sdc.readCodeSize { + return nil, fmt.Errorf("code missing for account '%x' with codeHash '%x'", plainKey, acc.CodeHash.Value()) + } + if sdc.readCodeSize { + u.CodeSize = uint64(len(code)) } } return u, nil diff --git a/execution/commitment/commitmentdb/pbin_codesize_test.go b/execution/commitment/commitmentdb/pbin_codesize_test.go new file mode 100644 index 00000000000..03f142b2483 --- /dev/null +++ b/execution/commitment/commitmentdb/pbin_codesize_test.go @@ -0,0 +1,156 @@ +// Copyright 2026 The Erigon Authors +// This file is part of Erigon. +// +// Erigon is free software: you can redistribute it and/or modify +// it under the terms of the GNU Lesser General Public License as published by +// the Free Software Foundation, either version 3 of the License, or +// (at your option) any later version. +// +// Erigon is distributed in the hope that it will be useful, +// but WITHOUT ANY WARRANTY; without even the implied warranty of +// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +// GNU Lesser General Public License for more details. +// +// You should have received a copy of the GNU Lesser General Public License +// along with Erigon. If not, see . + +package commitmentdb_test + +import ( + "testing" + + "github.com/holiman/uint256" + "github.com/stretchr/testify/require" + + "github.com/erigontech/erigon/common" + "github.com/erigontech/erigon/common/crypto" + "github.com/erigontech/erigon/common/empty" + "github.com/erigontech/erigon/common/length" + "github.com/erigontech/erigon/common/log/v3" + "github.com/erigontech/erigon/db/kv" + "github.com/erigontech/erigon/db/state/execctx" + "github.com/erigontech/erigon/execution/commitment" + "github.com/erigontech/erigon/execution/commitment/commitmentdb" + "github.com/erigontech/erigon/execution/types/accounts" +) + +func pbinCodeSizeAddr(i byte) []byte { + a := make([]byte, length.Addr) + a[0], a[length.Addr-1] = 0xc0, i + return a +} + +// pbinCodeSizeSharedDomains opens a SharedDomains over a fresh datadir holding +// one account and, when non-nil, one CodeDomain entry for it. +func pbinCodeSizeSharedDomains(t *testing.T, opts []execctx.SharedDomainOption, addr []byte, acc *accounts.Account, code []byte) (*execctx.SharedDomains, kv.TemporalTx) { + t.Helper() + db := pbinNewTestDb(t) + tx, err := db.BeginTemporalRw(t.Context()) + require.NoError(t, err) + t.Cleanup(tx.Rollback) + + sd, err := execctx.NewSharedDomains(t.Context(), tx, log.New(), opts...) + require.NoError(t, err) + t.Cleanup(sd.Close) + + require.NoError(t, sd.DomainPut(kv.AccountsDomain, tx, addr, accounts.SerialiseV3(acc), 0, nil)) + if code != nil { + require.NoError(t, sd.DomainPut(kv.CodeDomain, tx, addr, code, 0, nil)) + } + return sd, tx +} + +// pbinCodeSizeTrieContext builds a read context over that state, with the code +// size read switched the way the named variant would switch it. +func pbinCodeSizeTrieContext(t *testing.T, readCodeSize bool, addr []byte, acc *accounts.Account, code []byte) *commitmentdb.TrieContext { + t.Helper() + sd, tx := pbinCodeSizeSharedDomains(t, nil, addr, acc, code) + ttx := commitmentdb.NewTrieContextRo(commitmentdb.NewLatestStateReader(tx, sd), sd.StepSize()) + ttx.SetReadCodeSize(readCodeSize) + return ttx +} + +func pbinCodeSizeAccount(codeHash common.Hash) *accounts.Account { + return &accounts.Account{Nonce: 3, Balance: *uint256.NewInt(77), CodeHash: accounts.InternCodeHash(codeHash)} +} + +// TestPBinTrieContextAccountReadsCodeSize is where BASIC_DATA's code_size comes +// from: the length of the account's code in the CodeDomain. +func TestPBinTrieContextAccountReadsCodeSize(t *testing.T) { + t.Parallel() + + code := []byte{0x60, 0x00, 0x60, 0x00, 0xfd} + addr := pbinCodeSizeAddr(1) + ttx := pbinCodeSizeTrieContext(t, true, addr, pbinCodeSizeAccount(crypto.Keccak256Hash(code)), code) + + u, err := ttx.Account(addr) + require.NoError(t, err) + require.Equal(t, uint64(len(code)), u.CodeSize) + require.NotZero(t, u.Flags&commitment.CodeUpdate) +} + +// TestPBinTrieContextLeavesCodeSizeZeroForHex pins the gate: the hex trie does +// not hash code_size, so it must not pay for the extra CodeDomain read. +func TestPBinTrieContextLeavesCodeSizeZeroForHex(t *testing.T) { + t.Parallel() + + code := []byte{0x60, 0x00, 0x60, 0x00, 0xfd} + addr := pbinCodeSizeAddr(2) + ttx := pbinCodeSizeTrieContext(t, false, addr, pbinCodeSizeAccount(crypto.Keccak256Hash(code)), code) + + u, err := ttx.Account(addr) + require.NoError(t, err) + require.Zero(t, u.CodeSize) +} + +// TestPBinTrieContextIgnoresClearedDelegationResidue decides H9: a cleared +// EIP-7702 delegation leaves code in the CodeDomain that no longer belongs to +// the account. code_size follows the account's own code hash, so the residue +// changes nothing — otherwise a tolerated inconsistency would move the root. +func TestPBinTrieContextIgnoresClearedDelegationResidue(t *testing.T) { + t.Parallel() + + residue := []byte{0xef, 0x01, 0x00} + addr := pbinCodeSizeAddr(3) + ttx := pbinCodeSizeTrieContext(t, true, addr, pbinCodeSizeAccount(empty.CodeHash), residue) + + u, err := ttx.Account(addr) + require.NoError(t, err) + require.Zero(t, u.CodeSize) + require.Equal(t, empty.CodeHash, u.CodeHash) +} + +// TestPBinTrieContextRefusesCodeBearingAccountWithoutCode is H9's other half: a +// code hash with no code behind it (an eth_simulateV1 overlay, a truncated +// datadir) would hash as code_size 0 and silently produce a wrong root. +func TestPBinTrieContextRefusesCodeBearingAccountWithoutCode(t *testing.T) { + t.Parallel() + + addr := pbinCodeSizeAddr(4) + ttx := pbinCodeSizeTrieContext(t, true, addr, pbinCodeSizeAccount(common.Hash{0xAB}), nil) + + _, err := ttx.Account(addr) + require.ErrorContains(t, err, "code missing") +} + +// TestPBinSharedDomainsReadsCodeSizeUnderBin ties the variant to the read: only +// the bin trie needs code_size, so only a bin SharedDomains must insist the code +// is there. +func TestPBinSharedDomainsReadsCodeSizeUnderBin(t *testing.T) { + t.Parallel() + + cfg := commitment.DefaultTrieConfig() + cfg.Variant = commitment.VariantBinPatriciaTrie + + addr := pbinCodeSizeAddr(5) + acc := pbinCodeSizeAccount(common.Hash{0xAB}) + sd, tx := pbinCodeSizeSharedDomains(t, []execctx.SharedDomainOption{execctx.WithTrieConfig(cfg)}, addr, acc, nil) + require.IsType(t, &commitment.PBinPatriciaHashed{}, sd.GetCommitmentCtx().Trie()) + + _, err := sd.ComputeCommitment(t.Context(), tx, false, 0, 0, "pbin-codesize", nil) + require.ErrorContains(t, err, "code missing") + + hexSd, hexTx := pbinCodeSizeSharedDomains(t, nil, addr, acc, nil) + _, err = hexSd.ComputeCommitment(t.Context(), hexTx, false, 0, 0, "hex-codesize", nil) + require.NoError(t, err, "hex does not hash code_size and must not start requiring the code") +} diff --git a/execution/commitment/hex_patricia_hashed_test.go b/execution/commitment/hex_patricia_hashed_test.go index eea014c3d9d..37d5a276190 100644 --- a/execution/commitment/hex_patricia_hashed_test.go +++ b/execution/commitment/hex_patricia_hashed_test.go @@ -956,6 +956,7 @@ func TestUpdate_EncodeDecode(t *testing.T) { {Flags: BalanceUpdate, Balance: *uint256.NewInt(123), CodeHash: empty.CodeHash}, {Flags: BalanceUpdate | NonceUpdate, Balance: *uint256.NewInt(45639015), Nonce: 123, CodeHash: empty.CodeHash}, {Flags: BalanceUpdate | NonceUpdate | CodeUpdate, Balance: *uint256.NewInt(45639015), Nonce: 123, + CodeSize: 24576, CodeHash: common.Hash{ 0x01, 0x02, 0x03, 0x04, 0x05, 0x06, 0x07, 0x08, 0x09, 0x0A, 0x0B, 0x0C, 0x0D, 0x0E, 0x0F, 0x10, @@ -978,6 +979,7 @@ func TestUpdate_EncodeDecode(t *testing.T) { require.Equal(t, update.Balance, decoded.Balance, i) require.Equal(t, update.Nonce, decoded.Nonce, i) require.Equal(t, update.CodeHash, decoded.CodeHash, i) + require.Equal(t, update.CodeSize, decoded.CodeSize, i) require.Equal(t, update.Storage, decoded.Storage, i) require.Equal(t, update.StorageLen, decoded.StorageLen, i) } diff --git a/execution/commitment/pbin_codesize_test.go b/execution/commitment/pbin_codesize_test.go new file mode 100644 index 00000000000..cc634af8a43 --- /dev/null +++ b/execution/commitment/pbin_codesize_test.go @@ -0,0 +1,119 @@ +// Copyright 2026 The Erigon Authors +// This file is part of Erigon. +// +// Erigon is free software: you can redistribute it and/or modify +// it under the terms of the GNU Lesser General Public License as published by +// the Free Software Foundation, either version 3 of the License, or +// (at your option) any later version. +// +// Erigon is distributed in the hope that it will be useful, +// but WITHOUT ANY WARRANTY; without even the implied warranty of +// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +// GNU Lesser General Public License for more details. +// +// You should have received a copy of the GNU Lesser General Public License +// along with Erigon. If not, see . + +package commitment + +import ( + "context" + "testing" + + "github.com/holiman/uint256" + "github.com/stretchr/testify/require" + + "github.com/erigontech/erigon/common" +) + +// TestPBinBasicDataLeafCarriesCodeSize checks the BASIC_DATA leaf the engine +// builds for a code-bearing account against the reference's own packings: the +// code size has to reach the leaf value, not be forced to zero. +func TestPBinBasicDataLeafCarriesCodeSize(t *testing.T) { + t.Parallel() + v := loadPBinSpecVectors(t) + require.NotEmpty(t, v.BasicData) + + addr := pbinOracleAddr(1) + key := pbinTreeKeyAccount(addr, pbinBasicDataLeafKey) + + for _, tc := range v.BasicData { + bal, err := uint256.FromDecimal(tc.Balance) + require.NoError(t, err) + + u := Update{Flags: NonceUpdate | BalanceUpdate, Nonce: tc.Nonce, Balance: *bal, CodeSize: tc.CodeSize} + got, err := pbinLeafValue(key, &u) + require.NoError(t, err) + require.Equal(t, mustHex(t, tc.Value), got[:], + "BASIC_DATA leaf mismatch for code_size=%d nonce=%d balance=%s", tc.CodeSize, tc.Nonce, tc.Balance) + } +} + +// TestPBinEngineRootCarriesCodeSize drives a code-bearing account through the +// whole engine, so the size has to survive the context read, the cell merge and +// the leaf hash — not just the value encoder. +func TestPBinEngineRootCarriesCodeSize(t *testing.T) { + t.Parallel() + + addr := pbinOracleAddr(9) + codeHash := common.Hash{0xC0, 0xDE} + withCode := new(pbinTestCorpus).accountWithCode(addr, 4, 500, codeHash, 6358) + + _, root := withCode.process(t) + require.Equal(t, withCode.oracleRoot(t), root) + + sizeless := new(pbinTestCorpus).accountWithCode(addr, 4, 500, codeHash, 0) + require.NotEqual(t, sizeless.oracleRoot(t), root, "code_size must reach the root") +} + +// TestPBinUpdateCodeSizeSurvivesCopyAndReset pins the two Update lifecycle +// hooks the engine relies on: a copied update keeps the size, a reset one drops +// it so a pooled cell cannot inherit a stale code size. +func TestPBinUpdateCodeSizeSurvivesCopyAndReset(t *testing.T) { + t.Parallel() + + u := Update{Flags: CodeUpdate, CodeHash: common.Hash{0x01}, CodeSize: 24576} + require.Equal(t, uint64(24576), u.Copy().CodeSize) + + u.Reset() + require.Zero(t, u.CodeSize) +} + +// TestPBinUpdateCodeSizeMergesWithCodeHash pins that the size travels with the +// hash: they describe the same code, so a merge must never leave one of them +// from the old account and the other from the new. +func TestPBinUpdateCodeSizeMergesWithCodeHash(t *testing.T) { + t.Parallel() + + dst := Update{Flags: CodeUpdate, CodeHash: common.Hash{0x01}, CodeSize: 100} + dst.Merge(&Update{Flags: CodeUpdate, CodeHash: common.Hash{0x02}, CodeSize: 200}) + require.Equal(t, common.Hash{0x02}, dst.CodeHash) + require.Equal(t, uint64(200), dst.CodeSize) +} + +// TestPBinPushSideNeverDeliversCode pins that Updates.TouchCode cannot feed the +// bin trie: the variant is hardwired to ModeDirect, which interns plain keys +// only and hands the trie a nil update. Everything the tree hashes comes from +// the read side, so patching the push side to carry code would be dead code. +func TestPBinPushSideNeverDeliversCode(t *testing.T) { + t.Parallel() + + cfg := DefaultTrieConfig() + cfg.Variant = VariantBinPatriciaTrie + trie, upd := InitializeTrieAndUpdates(ModeUpdate, t.TempDir(), cfg) + defer upd.Close() + + require.IsType(t, &PBinPatriciaHashed{}, trie) + require.Equal(t, ModeDirect, upd.Mode(), "the bin variant overrides the requested mode") + + addr := pbinOracleAddr(3) + upd.TouchPlainKey(string(addr), []byte{0x60, 0x00, 0x60, 0x00}, upd.TouchCode) + + keys := 0 + require.NoError(t, upd.HashSort(context.Background(), nil, func(treeKey, plainKey []byte, u *Update) error { + keys++ + require.Nil(t, u, "ModeDirect delivers no update, so TouchCode cannot reach the trie") + return nil + })) + require.Equal(t, 1, keys) +} diff --git a/execution/commitment/pbin_hash.go b/execution/commitment/pbin_hash.go index e5f66480666..3c51a213931 100644 --- a/execution/commitment/pbin_hash.go +++ b/execution/commitment/pbin_hash.go @@ -135,9 +135,7 @@ func pbinLeafValue(key []byte, u *Update) ([pbinValueLength]byte, error) { } switch subIndex := key[len(key)-1]; { case subIndex == pbinBasicDataLeafKey: - // code_size stays zero while code chunking is out of scope: the shared - // Update carries no code size and adding one is an external API change. - return pbinEncodeBasicData(u.Nonce, &u.Balance, 0) + return pbinEncodeBasicData(u.Nonce, &u.Balance, u.CodeSize) case subIndex == pbinCodeHashLeafKey: return pbinCodeHashValue(u.CodeHash), nil case subIndex >= pbinHeaderStorageOffset && subIndex < pbinCodeOffset: diff --git a/execution/commitment/pbin_patricia_hashed.go b/execution/commitment/pbin_patricia_hashed.go index 8963dbaba39..268c0adca7f 100644 --- a/execution/commitment/pbin_patricia_hashed.go +++ b/execution/commitment/pbin_patricia_hashed.go @@ -21,9 +21,11 @@ // derivation, behind pbinHasher so the suite can be swapped. // // M0 scope: in-memory Process over the account and storage zones, ModeDirect -// only. Code chunking and parallel mounting are out — BASIC_DATA carries -// code_size 0. EIP-8297 has no removal: a zeroed storage slot keeps its leaf at -// 32 zero bytes, while an account removal is refused rather than guessed at. +// only. BASIC_DATA carries the account's real code_size, but the CODE_ZONE +// chunks it describes are not in the tree yet, so a code-bearing account hashes +// incompletely. Parallel mounting is out. EIP-8297 has no removal: a zeroed +// storage slot keeps its leaf at 32 zero bytes, while an account removal is +// refused rather than guessed at. package commitment diff --git a/execution/commitment/pbin_process_test.go b/execution/commitment/pbin_process_test.go index ed728f90bc6..f1fec733bc3 100644 --- a/execution/commitment/pbin_process_test.go +++ b/execution/commitment/pbin_process_test.go @@ -39,7 +39,11 @@ type pbinTestCorpus struct { } func (c *pbinTestCorpus) account(addr []byte, nonce, balance uint64, codeHash common.Hash) *pbinTestCorpus { - u := Update{Flags: NonceUpdate | BalanceUpdate | CodeUpdate, Nonce: nonce, CodeHash: codeHash} + return c.accountWithCode(addr, nonce, balance, codeHash, 0) +} + +func (c *pbinTestCorpus) accountWithCode(addr []byte, nonce, balance uint64, codeHash common.Hash, codeSize uint64) *pbinTestCorpus { + u := Update{Flags: NonceUpdate | BalanceUpdate | CodeUpdate, Nonce: nonce, CodeHash: codeHash, CodeSize: codeSize} u.Balance.SetUint64(balance) c.plainKeys = append(c.plainKeys, bytes.Clone(addr)) c.updates = append(c.updates, u) @@ -63,7 +67,7 @@ func (c *pbinTestCorpus) entries(t *testing.T) []pbinOracleEntry { u := &c.updates[i] switch len(plainKey) { case length.Addr: - basic, err := pbinEncodeBasicData(u.Nonce, &u.Balance, 0) + basic, err := pbinEncodeBasicData(u.Nonce, &u.Balance, u.CodeSize) require.NoError(t, err) code := pbinCodeHashValue(u.CodeHash) entries = append(entries, diff --git a/execution/commitment/pbin_specengine_test.go b/execution/commitment/pbin_specengine_test.go index 3e579c0cebe..6f5547aa9a1 100644 --- a/execution/commitment/pbin_specengine_test.go +++ b/execution/commitment/pbin_specengine_test.go @@ -54,13 +54,9 @@ func pbinLeafFromVector(key, value []byte, seq int) (pbinEngineLeaf, bool) { } switch sub := key[len(key)-1]; { case sub == pbinBasicDataLeafKey: - // BASIC_DATA is rebuilt from nonce and balance with code_size forced to - // zero, so a value carrying a code size cannot be reproduced. - if binary.BigEndian.Uint32(value[pbinBasicDataCodeSizeOffset:]) != 0 { - return l, false - } l.plainKey = account l.update.Flags = BalanceUpdate | NonceUpdate + l.update.CodeSize = uint64(binary.BigEndian.Uint32(value[pbinBasicDataCodeSizeOffset:])) l.update.Nonce = binary.BigEndian.Uint64(value[pbinBasicDataNonceOffset:]) l.update.Balance = *new(uint256.Int).SetBytes(value[pbinBasicDataBalanceOffset:]) return l, true From 7c4917a3aacdb908112dffa937c7ad06aa119ce8 Mon Sep 17 00:00:00 2001 From: awskii Date: Thu, 30 Jul 2026 20:57:22 +0700 Subject: [PATCH 35/56] feat: header code chunks in the pbin tree MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Code enters the binary tree. pbinChunkifyCode implements EIP-8297's chunking (eip:374-397) — pad to 31 before the pushdata scan, residual pushdata carries across chunk boundaries — checked against all five reference chunkify vectors plus a straddling PUSH32 and a 7702 designator. The code zone is now explicit at the three sites where a 34-byte code key used to pass as an account key: pbinZoneKeyLength names the one length per zone and is the authority in pbinTreeKey, leafCellHash and pbinLeafValue. Chunks 0..127 are emitted at stem exit rather than during the account's own visit: they occupy the header's top sub-indices, so an inline fan-out would descend past a header storage slot the stream has not delivered yet, and coming back for it would rewrite a record the fold had already written. followAndUpdate now refuses a non-ascending visit outright, which is what makes that structural rather than documented. A chunk's value lives in the branch record (pbinFieldLeafValue): no state domain holds a chunk, and an untouched chunk leaf that is the direct sibling of a touched one has to reload its own bytes. The code the chunks come from is read through an optional Code() on the trie context and cross-checked against the code_size the BASIC_DATA leaf hashes. Answers Q2 of the plan: the tree is a function of history for code chunks. A redeploy to shorter code leaves the chunks above the new length in place, so a forward run and a recompute from the state domains reach two internally consistent, different roots. Both are pinned, residue included. Code needing more than the 128 chunks the header holds is refused with ErrPBinUnsupported until the code zone lands. --- docs/plans/20260730-pbin-m1-local-el.md | 28 +- .../commitmentdb/commitment_context.go | 10 + .../commitment/commitmentdb/pbin_code_test.go | 104 +++++ .../commitment/patricia_state_mock_test.go | 21 +- execution/commitment/pbin_branch.go | 39 +- execution/commitment/pbin_code.go | 85 ++++ execution/commitment/pbin_code_test.go | 368 ++++++++++++++++++ execution/commitment/pbin_codesize_test.go | 23 +- execution/commitment/pbin_hash.go | 27 +- execution/commitment/pbin_hazard_test.go | 8 +- execution/commitment/pbin_keys.go | 40 +- execution/commitment/pbin_patricia_hashed.go | 139 ++++++- execution/commitment/pbin_process_test.go | 32 +- execution/commitment/pbin_specvectors_test.go | 7 + 14 files changed, 886 insertions(+), 45 deletions(-) create mode 100644 execution/commitment/commitmentdb/pbin_code_test.go create mode 100644 execution/commitment/pbin_code.go create mode 100644 execution/commitment/pbin_code_test.go diff --git a/docs/plans/20260730-pbin-m1-local-el.md b/docs/plans/20260730-pbin-m1-local-el.md index cfefda1b3ca..84365ce366e 100644 --- a/docs/plans/20260730-pbin-m1-local-el.md +++ b/docs/plans/20260730-pbin-m1-local-el.md @@ -97,13 +97,13 @@ Each hazard needs a named test or a structural assert. These are the plan's real | H2 | **Root record lost to empty-key iteration truncation** — `loadRoot` treats absent as an empty tree (`pbin_patricia_hashed.go:349-351`); looks like a fresh datadir | 2 | round-trip a stored root through a real domain iteration | | H3 | **A hash call site bypassing the injectable seam** — with Keccak in production and BLAKE3 only in tests, a site hardcoding either one drifts silently. Also a pooled engine inheriting a stale `hasher.sum` | 1 | full 32-byte key equality in `TestPBinSpecKeyRouting` under BLAKE3 — a hardcoded site breaks the vectors; `Release()` must clear `hasher.sum` | | H4 | **Variant mismatch across processes** — genesis hex + exec pbin, flagless restart, rpcdaemon defaulting to hex, `integration commitment rebuild` overwriting pbin records | 6, 7 | persisted `trie_variant` + refusal on disagreement | -| H5 | **Backwards visit from the header-chunk fan-out** — `fold` writes with `prevData = nil` and the record replaces its predecessor outright; re-descending a folded row rewrites it with a `touchMap` that no longer names the previously-touched bit | 12 | assert monotonic visit order; test a batch touching a header slot *and* code on one account | +| H5 | **Backwards visit from the header-chunk fan-out** — `fold` writes with `prevData = nil` and the record replaces its predecessor outright; re-descending a folded row rewrites it with a `touchMap` that no longer names the previously-touched bit | 12 | ✔ `followAndUpdate` refuses a non-ascending visit (`errPBinVisitOrder`); chunks emitted at stem exit — `TestPBinVisitOrderIsMonotonic`, `TestPBinCodeChunksFollowHeaderSlots` | | H6 | **State-blob depth truncation** — `byte(depth)` truncates ≥256; paths reach 528 bits | 5 | restart round-trip with a >256-bit path | | H7 | **Code key misread as storage** — a 52-byte length-discriminated code key read as `(addr, slot)` | 13 | tag-discriminated by construction + test that a code key never routes to the storage zone | -| H8 | **Stale high code chunks after a shortening redeploy** — header chunks overwrite in place and are never removed, so a forward run keeps residue while a rebuild emits only `ceil(code_size/31)`. Two internally-consistent, different roots. **Breaks recompute-from-domains as an oracle** | 12 | shortening-redeploy test comparing forward-run vs rebuild. See Q2 | +| H8 | **Stale high code chunks after a shortening redeploy** — header chunks overwrite in place and are never removed, so a forward run keeps residue while a rebuild emits only `ceil(code_size/31)`. Two internally-consistent, different roots. **Breaks recompute-from-domains as an oracle** | 12 | ✔ confirmed real, not fixed: `TestPBinShorteningRedeployKeepsStaleChunks` pins both roots and the exact residue. Q2 answered — the tree is history-dependent for code | | H9 | **Unconditional `CodeDomain` read promotes tolerated inconsistency to root divergence** — cleared 7702 residue, `eth_simulateV1` overlays. The existing code documents the residue as benign (`commitment_context.go:1054-1057`); PBT removes that license | 11 | decide and test the residue case explicitly | | H10 | **`ReplacePlainKeys` over pbin records** if references are ever enabled — rewrites bytes at hex cell offsets during background merge. Inert by default, one flag away, no variant check in that path | 6 | refuse the combination | -| H11 | **Overflow-chunk sibling rehash via `CodeStore` by-hash** → cache *miss* (not error) → zero-valued chunk leaf | 13 | avoided entirely by value-in-record | +| H11 | **Overflow-chunk sibling rehash via `CodeStore` by-hash** → cache *miss* (not error) → zero-valued chunk leaf | 13 | ✔ dissolved in Task 12: `pbinFieldLeafValue` puts the chunk in the record, so no by-hash lookup exists to miss | | H13 | **Root verification switched off leaves nothing validating the node path** — a silently wrong chain looks healthy. Only relevant when the toggle is used, i.e. against foreign headers; a self-produced chain keeps the check | 6 | default ON so it is opt-out not opt-in; loud startup log when off; a bin run against foreign headers without the toggle must fail at block 1, not degrade | | H12 | **`foldDelete` "enabled" to make a test pass** — collapses nodes the reference leaves in place | 10 | guarded by plan text + a test asserting it stays unreachable from `Process` | @@ -112,7 +112,7 @@ Each hazard needs a named test or a structural assert. These are the plan's real Blocking items needing a human or upstream answer. Do not proceed past the task that depends on one without recording the answer here. - **Q1 (highest value) — what does the reference produce for a removed account?** EIP-161 empty-removal and EIP-6780 destruct both hand pbin a `DeleteUpdate` on a committed leaf. Task 9 would turn that into BASIC_DATA `{version 0, code_size 0, nonce 0, balance 0}` + CODE_HASH `keccak(empty)`. Consistent with eip:345-347 but **not verified against the reference**, and `testdata/eip8297_vectors.json` does not cover it. It silently changes the root. ⚠️ **Deferred at Task 9, still unanswered.** Account removal keeps erroring at both sites (`updateCell`, the `loadCellState` account arm); only storage was reinterpreted. Note the `zero_value_present` vector *is* an account-zone BASIC_DATA leaf of 32 zero bytes, so the reference at least admits that leaf shape — it does not say a removal produces it. Unblocks nothing in M1: a dev chain reaches neither removal path. -- **Q2 — is the tree a pure function of current state, or of history?** (H8.) eip-8297 is silent. Determines whether recompute-from-domains is a valid oracle at all, hence whether the M1a gate means anything. Possibly an upstream spec question. +- **Q2 — is the tree a pure function of current state, or of history?** (H8.) eip-8297 is silent. Determines whether recompute-from-domains is a valid oracle at all, hence whether the M1a gate means anything. Possibly an upstream spec question. **Answered (Task 12): of history, for code chunks only.** Accounts and storage are a pure function of current state — every leaf's value comes from a domain read. Code chunks are not: eip:439-443 says EVM execution never removes entries, so a redeploy to shorter code leaves the chunks above the new length in the tree holding the old code's bytes, and nothing in the current state records that they exist. A forward run commits them; a recompute from the domains emits only `ceil(code_size/31)`. Both roots are internally consistent and different — `TestPBinShorteningRedeployKeepsStaleChunks` pins both, including the exact residue. Consequences: **recompute-from-domains is not an oracle for a code-bearing account** (the M1a gate stays valid only because its datadir is code-free), and `integration commitment rebuild` over a chain that has seen a shortening redeploy will not reproduce the chain's roots. Reachable in practice by an EIP-7702 delegation clear and by a metamorphic CREATE2 redeploy; not by the M1b dev chain. Zeroing the tail instead would need the previous code length, which no read on the commitment path has. Still worth raising upstream — the same divergence exists for any client that rebuilds state from a snapshot. - **Q3 — does the reference create a present-zero leaf on `SSTORE 0` to a virgin slot?** Erigon drops it at `execution/state/state_object.go:245-246` before any writer sees it. If yes, that guard must be variant-gated and every zero-SSTORE becomes a state write. Still open after Task 9, which deliberately kept the virgin case a no-op: a delete only zeroes a cell that already holds a leaf. - **Q4 — confirm `github.com/zeebo/blake3`** as a **test-only** dependency (lower stakes now that production stays Keccak; the in-graph `lukechampine.com/blake3` may simply be enough). Its measured advantage was 13–18% arm64 / 58–78% amd64, not re-verified. **Answered (Task 1):** `zeebo` not added — production stays Keccak so BLAKE3 speed is irrelevant, and `lukechampine.com/blake3` is already a direct dependency used by `pbin_specroots_test.go`; it now backs `pbinBlake3Hash` for all three seam tests. - **Q5 — forward/backward compatibility of a new `erigondb.toml` key** across erigon binaries, given the file is downloader-delivered and **wins over the CLI** (`erigondb_settings.go:74-89`). **Answered (Task 6):** `readErigonDBSettings` uses `go-toml/v2` `Unmarshal`, which ignores unknown keys — older binaries parse a `trie_variant` toml fine. The key is written only when bin, so published/downloader tomls stay byte-identical, and a downloader-delivered hex toml under a bin process is refused at resolve. Residual risk: a binary **predating the key** opens a bin datadir as hex with no guard — inherent to any new key; acceptable while bin is experimental and fresh-datadir-only. @@ -121,7 +121,7 @@ Blocking items needing a human or upstream answer. Do not proceed past the task Do not treat these as established: -- `pbin_branch.go` record field-bit layout beyond the leaf-kind rejection at `:156-165`. **Verify before designing Task 13's value-in-record field.** +- ~~`pbin_branch.go` record field-bit layout beyond the leaf-kind rejection at `:156-165`~~ — **read and extended in Task 12.** Layout: `uint16 touchMap ‖ uint16 afterMap`, then one cell body per bit set in afterMap, ascending. A cell body is `byte fields ‖ uvarint prefixBitLen ‖ packed prefix bits ‖ present fields in bit order`. Field bits: `leaf=1, branch=2, accountAddr=4, storageAddr=8, hash=16, leafValue=32`. Every variable-length field is `uvarint len ‖ bytes` but each has one legal length, which `pbinDecodeFixedVal` enforces, so the record has exactly one spelling. Exactly one kind bit and, for a leaf, exactly one of `accountAddr | storageAddr | leafValue`; a branch carrying `leafValue` is rejected. Task 13's overflow chunks need no new field — they reuse `leafValue`. - ~~The "grid arrays are restorable-as-zero" argument underpinning Task 5's ~160-byte blob~~ — **proven in Task 5** (see the Task 5 checklist for the read/write-site audit); the root-cell blob landed. - ~~Whether pbin branch records are truly opaque to the pass-through merge path~~ — **exercised in Task 10** with references off: collation, prune and merge round-trip the records byte-for-byte, checked against a db snapshot with a positive count of records provably served from files. - ~~Task 8's deferral mis-attribution~~ — **Task 8**: still no concrete failing sequence, and the exposure is bounded: deferral is only ever requested by `ExecV3` (fork validation / parallel apply), and the fork-validation writes it would mis-route land in a validation overlay that is never flushed. The guards are structural — bin cannot reach the deferred path at all now. @@ -316,13 +316,17 @@ Production keeps Keccak-256. This task only makes the **test** path run the whol - Modify: `execution/commitment/pbin_patricia_hashed.go` - Create: `execution/commitment/pbin_code_test.go` -- [ ] write failing tests for `pbinChunkifyCode` against `chunkify_vectors`, covering pushdata straddling a chunk boundary and a 7702 designator -- [ ] write a failing test for a batch touching both a header storage slot and code on one account, asserting monotonic visit order (guards H5) -- [ ] write a failing shortening-redeploy test comparing forward-run and rebuild roots (guards H8); if they differ, record Q2's answer before proceeding -- [ ] implement `pbinChunkifyCode` per eip:374-397 exactly — pad to 31 before the scan, carry residual pushdata across boundaries -- [ ] add `pbinCodeZone` and make the zone explicit at the three places a code key currently passes by accident (`pbin_keys.go:62-66`, `pbin_hash.go:132-148`, `pbin_hash.go:117-119`) -- [ ] emit header chunks 0..127 with a stem-exit flush or as their own sorted stream keys, never mid-fan-out -- [ ] run tests — must pass before task 13 +- [x] write failing tests for `pbinChunkifyCode` against `chunkify_vectors`, covering pushdata straddling a chunk boundary and a 7702 designator — `TestPBinChunkifyCodeVectors` (all 5 reference vectors), `TestPBinChunkifyCodePushdataStraddlesBoundary` (PUSH32 as the last byte of chunk 0: chunk 1 saturates at 31, chunk 2 still carries 1), `TestPBinChunkifyCode7702Designator`, plus `TestPBinChunkifyCodeChunkCount` pinning ceil(len/31) at the header/overflow boundary. ➕ the `chunkify_vectors` decode landed in `pbin_specvectors_test.go` (where `loadPBinSpecVectors` lives), not in the planned file list +- [x] write a failing test for a batch touching both a header storage slot and code on one account, asserting monotonic visit order (guards H5) — `TestPBinCodeChunksFollowHeaderSlots` (code + slots 5, 63 and a storage-zone slot on one account) and `TestPBinVisitOrderIsMonotonic`. The assert is structural and lives in `followAndUpdate`: every visit must exceed the last, so an inline fan-out fails loudly instead of rewriting a folded row's record. It is cleared at the start of each `Process` — a run starts back at the root, so the previous run's last key bounds nothing +- [x] write a failing shortening-redeploy test comparing forward-run and rebuild roots (guards H8); if they differ, record Q2's answer before proceeding — `TestPBinShorteningRedeployKeepsStaleChunks`, both the leaf-sibling case (2 chunks → 1) and the whole-subtree case (7 → 2). **They differ, and Q2 is answered below: the tree is a function of history for code chunks.** The forward root is pinned to the exact residue (new chunks plus the old code's chunks above the new length), so the divergence is a stated behaviour rather than an unexplained mismatch +- [x] implement `pbinChunkifyCode` per eip:374-397 exactly — pad to 31 before the scan, carry residual pushdata across boundaries +- [x] add `pbinCodeZone` and make the zone explicit at the three places a code key currently passes by accident (`pbin_keys.go:62-66`, `pbin_hash.go:132-148`, `pbin_hash.go:117-119`) — one `pbinZoneKeyLength(zone)` names the length per zone and is the authority at all three: `pbinTreeKey` panics on an unallocated zone instead of defaulting to the account length, `leafCellHash` requires the key's length to match its own zone byte, and `pbinLeafValue` switches on the zone before it looks at a sub-index. Tests: `TestPBinZoneKeyLengthIsExplicit`, `TestPBinLeafValueRoutesByZone`, `TestPBinLeafCellHashChecksZoneLength` +- [x] emit header chunks 0..127 with a stem-exit flush or as their own sorted stream keys, never mid-fan-out — stem-exit flush (`pbinPendingCode`): the account visit queues the chunks, and they are emitted when the next stream key leaves the 33-byte stem, or at the end of the stream. Chunk sub-indices are the highest in a stem, so a stem the stream has left is one no key returns to; queueing over an unflushed stem is a loud error, not a silent overwrite +- [x] ➕ **a code chunk's value lives in the branch record** (`pbinFieldLeafValue`, `pbin_branch.go` — Task 13's file, taken early because the forward run needs it). No state domain holds a chunk: chunking is a property of the tree, not of the account, and the reference never rewrites a chunk it has written. An untouched chunk leaf that is the direct sibling of a touched one must therefore reload its own bytes — `TestPBinCodeChunksSurviveAsRecordSiblings`. This dissolves H11 (no by-hash reverse lookup exists) and means a chunk leaf carries no plain key at all; `pbinDecodeCell` now requires a leaf to name exactly one value source of the three +- [x] ➕ the code read: `pbinCodeContext` (an optional interface on `PatriciaContext`, additive — not a fourth API break) implemented by `commitmentdb.TrieContext.Code` over `kv.CodeDomain`. A context that cannot serve code refuses a code-bearing account (`TestPBinCodelessContextRefusesCodeBearingAccount`), and the code the chunks come from is cross-checked against the `code_size` the BASIC_DATA leaf hashes (`TestPBinCodeSizeMustMatchTheCodeBehindIt`), since those are two separate domain reads. Read-side tests in `commitmentdb/pbin_code_test.go`, non-vacuous by mutation (a `Code` returning nothing fails all three) +- [x] ➕ code past the account header (>128 chunks, >3968 bytes) is refused with `ErrPBinUnsupported` until Task 13 — `TestPBinCodeBeyondHeaderRefused` and `TestPBinSharedDomainsRefusesCodeBeyondHeader`. The dev deposit contract is 6358 bytes, so **M1b stays blocked on Task 13**, as planned +- [x] ➕ `TestPBinEngineRootCarriesCodeSize` (Task 11) had a code_size with no code behind it, which this task makes an error. It now runs real code and isolates the size claim at the leaf-set level: the same leaf set with BASIC_DATA re-packed at code_size 0 roots differently +- [x] run tests — `go test ./execution/commitment/... ./db/state/... -count=1` green, `./execution/stagedsync/... ./execution/state/genesiswrite ./db/integrity -short` green, `go build ./...` clean, `make lint` clean twice ### Task 13: CODE_ZONE overflow chunks diff --git a/execution/commitment/commitmentdb/commitment_context.go b/execution/commitment/commitmentdb/commitment_context.go index 1b1a8208977..d50d19255c2 100644 --- a/execution/commitment/commitmentdb/commitment_context.go +++ b/execution/commitment/commitmentdb/commitment_context.go @@ -1089,6 +1089,16 @@ func (sdc *TrieContext) Account(plainKey []byte) (u *commitment.Update, err erro return u, nil } +// Code serves the bytecode the binary trie chunks into leaves. Only that trie +// asks for it; the hex trie hashes an account's code hash and never its bytes. +func (sdc *TrieContext) Code(plainKey []byte) ([]byte, error) { + code, _, err := sdc.readDomain(kv.CodeDomain, plainKey) + if err != nil { + return nil, err + } + return code, nil +} + func (sdc *TrieContext) Storage(plainKey []byte) (u *commitment.Update, err error) { enc, _, err := sdc.readDomain(kv.StorageDomain, plainKey) if err != nil { diff --git a/execution/commitment/commitmentdb/pbin_code_test.go b/execution/commitment/commitmentdb/pbin_code_test.go new file mode 100644 index 00000000000..380b7e8e1cf --- /dev/null +++ b/execution/commitment/commitmentdb/pbin_code_test.go @@ -0,0 +1,104 @@ +// Copyright 2026 The Erigon Authors +// This file is part of Erigon. +// +// Erigon is free software: you can redistribute it and/or modify +// it under the terms of the GNU Lesser General Public License as published by +// the Free Software Foundation, either version 3 of the License, or +// (at your option) any later version. +// +// Erigon is distributed in the hope that it will be useful, +// but WITHOUT ANY WARRANTY; without even the implied warranty of +// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +// GNU Lesser General Public License for more details. +// +// You should have received a copy of the GNU Lesser General Public License +// along with Erigon. If not, see . + +package commitmentdb_test + +import ( + "testing" + + "github.com/stretchr/testify/require" + + "github.com/erigontech/erigon/common/crypto" + "github.com/erigontech/erigon/db/state/execctx" + "github.com/erigontech/erigon/execution/commitment" +) + +func pbinTestCode(n int) []byte { + code := make([]byte, n) + for i := range code { + code[i] = byte(n + i) + } + return code +} + +// TestPBinTrieContextCodeReadsCodeDomain pins the read the binary trie's code +// chunking rests on. Chunk leaves hold bytecode, which no other trie needs and +// no account read returns. +func TestPBinTrieContextCodeReadsCodeDomain(t *testing.T) { + t.Parallel() + + code := pbinTestCode(100) + addr := pbinCodeSizeAddr(6) + ttx := pbinCodeSizeTrieContext(t, true, addr, pbinCodeSizeAccount(crypto.Keccak256Hash(code)), code) + + got, err := ttx.Code(addr) + require.NoError(t, err) + require.Equal(t, code, got) + + absent, err := ttx.Code(pbinCodeSizeAddr(7)) + require.NoError(t, err) + require.Empty(t, absent) +} + +// TestPBinSharedDomainsCommitsCodeBearingAccount is the wiring end to end: the +// engine chunks code it reads through the trie context, and cross-checks the +// chunk count against the code_size it hashes, so a context that cannot serve +// code fails the commit rather than committing a code-less tree. Chunk values +// themselves are pinned against the reference tree in the commitment package. +func TestPBinSharedDomainsCommitsCodeBearingAccount(t *testing.T) { + t.Parallel() + + cfg := commitment.DefaultTrieConfig() + cfg.Variant = commitment.VariantBinPatriciaTrie + + code := pbinTestCode(1000) + addr := pbinCodeSizeAddr(8) + acc := pbinCodeSizeAccount(crypto.Keccak256Hash(code)) + + sd, tx := pbinCodeSizeSharedDomains(t, []execctx.SharedDomainOption{execctx.WithTrieConfig(cfg)}, addr, acc, code) + require.IsType(t, &commitment.PBinPatriciaHashed{}, sd.GetCommitmentCtx().Trie()) + + withCode, err := sd.ComputeCommitment(t.Context(), tx, false, 0, 0, "pbin-code", nil) + require.NoError(t, err) + + // The same account with one byte of code roots differently: the chunk leaves + // are part of what is committed, not a side table. + short := pbinTestCode(1) + shortSd, shortTx := pbinCodeSizeSharedDomains(t, + []execctx.SharedDomainOption{execctx.WithTrieConfig(cfg)}, addr, pbinCodeSizeAccount(crypto.Keccak256Hash(short)), short) + withShortCode, err := shortSd.ComputeCommitment(t.Context(), shortTx, false, 0, 0, "pbin-code-short", nil) + require.NoError(t, err) + require.NotEqual(t, withShortCode, withCode) +} + +// TestPBinSharedDomainsRefusesCodeBeyondHeader pins the M1 boundary at the +// domain layer: chunks past the account header belong in the code zone, and +// until that lands a contract needing them is refused rather than committed +// short. +func TestPBinSharedDomainsRefusesCodeBeyondHeader(t *testing.T) { + t.Parallel() + + cfg := commitment.DefaultTrieConfig() + cfg.Variant = commitment.VariantBinPatriciaTrie + + code := pbinTestCode(128*31 + 1) + addr := pbinCodeSizeAddr(9) + sd, tx := pbinCodeSizeSharedDomains(t, + []execctx.SharedDomainOption{execctx.WithTrieConfig(cfg)}, addr, pbinCodeSizeAccount(crypto.Keccak256Hash(code)), code) + + _, err := sd.ComputeCommitment(t.Context(), tx, false, 0, 0, "pbin-code-overflow", nil) + require.ErrorIs(t, err, commitment.ErrPBinUnsupported) +} diff --git a/execution/commitment/patricia_state_mock_test.go b/execution/commitment/patricia_state_mock_test.go index 9b6d9d330e7..3a3d6e6750f 100644 --- a/execution/commitment/patricia_state_mock_test.go +++ b/execution/commitment/patricia_state_mock_test.go @@ -43,15 +43,17 @@ type MockState struct { mu sync.RWMutex // to protect sm and cm for concurrent trie sm map[string][]byte // backbone of the state cm map[string]BranchData // backbone of the commitments + code map[string][]byte // bytecode by account plain key, what CodeDomain holds numBuf [binary.MaxVarintLen64]byte } func NewMockState(t testing.TB) *MockState { t.Helper() return &MockState{ - t: t, - sm: make(map[string][]byte), - cm: make(map[string]BranchData), + t: t, + sm: make(map[string][]byte), + cm: make(map[string]BranchData), + code: make(map[string][]byte), } } @@ -158,6 +160,19 @@ func (ms *MockState) Storage(plainKey []byte) (*Update, error) { return &ex, nil } +// Code stands in for the CodeDomain read the binary trie's code chunking needs. +func (ms *MockState) Code(plainKey []byte) ([]byte, error) { + if ms.concurrent.Load() { + ms.mu.RLock() + defer ms.mu.RUnlock() + } + return ms.code[string(plainKey)], nil +} + +func (ms *MockState) setCode(addr, code []byte) { + ms.code[string(addr)] = bytes.Clone(code) +} + func (ms *MockState) TxNum() uint64 { return 0 } // applyPlainUpdates is called sequentially outside of the trie, so it needs no locking. diff --git a/execution/commitment/pbin_branch.go b/execution/commitment/pbin_branch.go index c45b2888e1e..5325d71a733 100644 --- a/execution/commitment/pbin_branch.go +++ b/execution/commitment/pbin_branch.go @@ -36,9 +36,15 @@ const ( pbinFieldAccountAddr pbinCellFields = 4 pbinFieldStorageAddr pbinCellFields = 8 pbinFieldHash pbinCellFields = 16 + // pbinFieldLeafValue carries the leaf's own 32 bytes. A code chunk is the one + // value no state domain holds — chunking is a property of the tree, not of the + // account — so the record is where it lives. + pbinFieldLeafValue pbinCellFields = 32 - pbinFieldsAll = pbinFieldLeaf | pbinFieldBranch | pbinFieldAccountAddr | pbinFieldStorageAddr | pbinFieldHash - pbinFieldKind = pbinFieldLeaf | pbinFieldBranch + pbinFieldsAll = pbinFieldLeaf | pbinFieldBranch | pbinFieldAccountAddr | pbinFieldStorageAddr | + pbinFieldHash | pbinFieldLeafValue + pbinFieldKind = pbinFieldLeaf | pbinFieldBranch + pbinFieldValue = pbinFieldAccountAddr | pbinFieldStorageAddr | pbinFieldLeafValue ) var ( @@ -92,6 +98,9 @@ func pbinAppendCell(dst []byte, c *pbinCell) ([]byte, error) { if c.storageAddrLen > 0 { fields |= pbinFieldStorageAddr } + if c.kind == pbinNodeLeaf && fields&pbinFieldValue == 0 { + fields |= pbinFieldLeafValue + } if c.hashLen > 0 { fields |= pbinFieldHash } @@ -106,6 +115,13 @@ func pbinAppendCell(dst []byte, c *pbinCell) ([]byte, error) { if fields&pbinFieldStorageAddr != 0 { dst = pbinAppendLenAndVal(dst, c.storageAddr[:c.storageAddrLen]) } + if fields&pbinFieldLeafValue != 0 { + value, err := pbinCodeChunkValue(&c.Update) + if err != nil { + return nil, err + } + dst = pbinAppendLenAndVal(dst, value[:]) + } if fields&pbinFieldHash != 0 { dst = pbinAppendLenAndVal(dst, c.hash[:c.hashLen]) } @@ -156,15 +172,18 @@ func pbinDecodeCell(data []byte, pos int, c *pbinCell) (int, error) { switch fields & pbinFieldKind { case pbinFieldLeaf: c.kind = pbinNodeLeaf - // A leaf without a plain key hashes a zero-valued state instead of failing, - // so the shape is rejected here rather than reaching the hasher. - switch fields & (pbinFieldAccountAddr | pbinFieldStorageAddr) { - case pbinFieldAccountAddr, pbinFieldStorageAddr: + // A leaf whose value has no source hashes a zero-valued state instead of + // failing, so the shape is rejected here rather than reaching the hasher. + switch fields & pbinFieldValue { + case pbinFieldAccountAddr, pbinFieldStorageAddr, pbinFieldLeafValue: default: - return 0, fmt.Errorf("%w: leaf cell fields %08b name no single plain key", errPBinMalformedBranch, fields) + return 0, fmt.Errorf("%w: leaf cell fields %08b name no single value source", errPBinMalformedBranch, fields) } case pbinFieldBranch: c.kind = pbinNodeBranch + if fields&pbinFieldLeafValue != 0 { + return 0, fmt.Errorf("%w: branch cell carries a leaf value", errPBinMalformedBranch) + } default: return 0, fmt.Errorf("%w: cell fields %08b name no single node kind", errPBinMalformedBranch, fields) } @@ -185,6 +204,12 @@ func pbinDecodeCell(data []byte, pos int, c *pbinCell) (int, error) { } c.storageAddrLen = length.Addr + length.Hash } + if fields&pbinFieldLeafValue != 0 { + if pos, err = pbinDecodeFixedVal(data, pos, c.Storage[:], pbinValueLength); err != nil { + return 0, err + } + c.Flags, c.StorageLen = StorageUpdate, pbinValueLength + } if fields&pbinFieldHash != 0 { if pos, err = pbinDecodeFixedVal(data, pos, c.hash[:], length.Hash); err != nil { return 0, err diff --git a/execution/commitment/pbin_code.go b/execution/commitment/pbin_code.go new file mode 100644 index 00000000000..a77c7cc73e8 --- /dev/null +++ b/execution/commitment/pbin_code.go @@ -0,0 +1,85 @@ +// Copyright 2026 The Erigon Authors +// This file is part of Erigon. +// +// Erigon is free software: you can redistribute it and/or modify +// it under the terms of the GNU Lesser General Public License as published by +// the Free Software Foundation, either version 3 of the License, or +// (at your option) any later version. +// +// Erigon is distributed in the hope that it will be useful, +// but WITHOUT ANY WARRANTY; without even the implied warranty of +// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +// GNU Lesser General Public License for more details. +// +// You should have received a copy of the GNU Lesser General Public License +// along with Erigon. If not, see . + +package commitment + +import "fmt" + +// EIP-8297's code embedding (eip:349-397). +const ( + // pbinChunkDataLen is how much code one chunk holds; byte 0 of the 32-byte + // value carries the PUSHDATA count instead. + pbinChunkDataLen = pbinValueLength - 1 + + // pbinHeaderCodeChunks are the chunks the account header holds, at + // sub-indices CODE_OFFSET..255. Higher chunks live in the code zone. + pbinHeaderCodeChunks = pbinStemSubtreeWidth - pbinCodeOffset + + pbinPushOffset = 95 + pbinPush1 = pbinPushOffset + 1 + pbinPush32 = pbinPushOffset + 32 +) + +// pbinChunkifyCode splits code into the tree's chunk values (eip:374-397). Byte +// 0 of a chunk counts how many of its leading bytes are PUSHDATA, so the scan +// runs over the whole code and residual PUSHDATA carries across chunk +// boundaries. Padding to a multiple of 31 happens before the scan, which is what +// makes a PUSH whose data runs off the end count against the padded tail. +func pbinChunkifyCode(code []byte) [][pbinValueLength]byte { + if len(code) == 0 { + return nil + } + padded := code + if rem := len(code) % pbinChunkDataLen; rem != 0 { + padded = make([]byte, len(code)+pbinChunkDataLen-rem) + copy(padded, code) + } + + // pushdataAt[i] is how many bytes from i on are still PUSHDATA. The spec sizes + // it a whole chunk past the code so a PUSH32 on the last byte has room. + pushdataAt := make([]byte, len(padded)+pbinValueLength) + for pos := 0; pos < len(padded); { + var pushdata int + if padded[pos] >= pbinPush1 && padded[pos] <= pbinPush32 { + pushdata = int(padded[pos]) - pbinPushOffset + } + pos++ + for x := range pushdata { + pushdataAt[pos+x] = byte(pushdata - x) + } + pos += pushdata + } + + chunks := make([][pbinValueLength]byte, 0, len(padded)/pbinChunkDataLen) + for pos := 0; pos < len(padded); pos += pbinChunkDataLen { + var chunk [pbinValueLength]byte + chunk[0] = min(pushdataAt[pos], pbinChunkDataLen) + copy(chunk[1:], padded[pos:pos+pbinChunkDataLen]) + chunks = append(chunks, chunk) + } + return chunks +} + +// pbinCodeChunkValue is the chunk value a code-chunk leaf hashes. Unlike a +// storage value it is positional — byte 0 is the PUSHDATA count — so a short +// value cannot be left-padded into place and is an error instead. +func pbinCodeChunkValue(u *Update) ([pbinValueLength]byte, error) { + if u.StorageLen != pbinValueLength { + return [pbinValueLength]byte{}, fmt.Errorf("%w: code chunk leaf holds %d value bytes, want %d", + errPBinCellHash, u.StorageLen, pbinValueLength) + } + return u.Storage, nil +} diff --git a/execution/commitment/pbin_code_test.go b/execution/commitment/pbin_code_test.go new file mode 100644 index 00000000000..09ccdcc070c --- /dev/null +++ b/execution/commitment/pbin_code_test.go @@ -0,0 +1,368 @@ +// Copyright 2026 The Erigon Authors +// This file is part of Erigon. +// +// Erigon is free software: you can redistribute it and/or modify +// it under the terms of the GNU Lesser General Public License as published by +// the Free Software Foundation, either version 3 of the License, or +// (at your option) any later version. +// +// Erigon is distributed in the hope that it will be useful, +// but WITHOUT ANY WARRANTY; without even the implied warranty of +// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +// GNU Lesser General Public License for more details. +// +// You should have received a copy of the GNU Lesser General Public License +// along with Erigon. If not, see . + +package commitment + +import ( + "bytes" + "context" + "fmt" + "testing" + + "github.com/stretchr/testify/require" +) + +// TestPBinChunkifyCodeVectors is the external check on chunk_code (eip:374-397): +// the reference's own chunkings, hash-independent because chunking is pure byte +// layout. +func TestPBinChunkifyCodeVectors(t *testing.T) { + t.Parallel() + v := loadPBinSpecVectors(t) + require.NotEmpty(t, v.Chunkify) + + for _, tc := range v.Chunkify { + t.Run(tc.Name, func(t *testing.T) { + t.Parallel() + got := pbinChunkifyCode(mustHex(t, tc.Code)) + require.Len(t, got, len(tc.Chunks)) + for i, want := range tc.Chunks { + require.Equal(t, mustHex(t, want), got[i][:], "chunk %d", i) + } + }) + } +} + +// TestPBinChunkifyCodePushdataStraddlesBoundary pins the part of the scan a +// per-chunk implementation gets wrong: PUSHDATA that begins in one chunk and +// runs into the next, so the later chunk's byte 0 counts bytes pushed by an +// opcode it does not contain. +func TestPBinChunkifyCodePushdataStraddlesBoundary(t *testing.T) { + t.Parallel() + + // PUSH32 at offset 30 is the last byte of chunk 0, so all 32 of its data + // bytes land in chunk 1 and 31 of them are still PUSHDATA at chunk 2. + code := append(make([]byte, 30), pbinPush32) + code = append(code, bytes.Repeat([]byte{0xEE}, 32)...) + + chunks := pbinChunkifyCode(code) + require.Len(t, chunks, 3) + require.EqualValues(t, 0, chunks[0][0], "chunk 0 starts on an opcode") + require.EqualValues(t, 31, chunks[1][0], "a full chunk of PUSHDATA saturates at 31") + require.EqualValues(t, 1, chunks[2][0], "one PUSHDATA byte carries into chunk 2") +} + +// TestPBinChunkifyCode7702Designator covers the shortest code the tree holds: +// an EIP-7702 designator is 23 bytes, one padded chunk whose first byte is the +// 0xEF marker rather than PUSHDATA. +func TestPBinChunkifyCode7702Designator(t *testing.T) { + t.Parallel() + + designator := append([]byte{0xEF, 0x01, 0x00}, bytes.Repeat([]byte{0xAB}, 20)...) + require.Len(t, designator, 23) + + chunks := pbinChunkifyCode(designator) + require.Len(t, chunks, 1) + require.EqualValues(t, 0, chunks[0][0]) + require.Equal(t, designator, chunks[0][1:1+len(designator)]) + require.Equal(t, make([]byte, pbinChunkDataLen-len(designator)), chunks[0][1+len(designator):], + "the tail is zero-padded, not left uninitialised") +} + +func TestPBinChunkifyCodeEmpty(t *testing.T) { + t.Parallel() + require.Empty(t, pbinChunkifyCode(nil)) + require.Empty(t, pbinChunkifyCode([]byte{})) +} + +// TestPBinChunkifyCodeChunkCount pins the sizing the header/overflow split rests +// on: chunks are ceil(len/31), and MaxCodeSize needs more than the 128 the +// account header holds. +func TestPBinChunkifyCodeChunkCount(t *testing.T) { + t.Parallel() + + for _, tc := range []struct{ size, chunks int }{ + {size: 1, chunks: 1}, + {size: 31, chunks: 1}, + {size: 32, chunks: 2}, + {size: pbinHeaderCodeChunks * pbinChunkDataLen, chunks: pbinHeaderCodeChunks}, + {size: pbinHeaderCodeChunks*pbinChunkDataLen + 1, chunks: pbinHeaderCodeChunks + 1}, + {size: 24576, chunks: 793}, + } { + require.Len(t, pbinChunkifyCode(make([]byte, tc.size)), tc.chunks, "code of %d bytes", tc.size) + } +} + +// pbinTestCode is deterministic filler of a given length. Every byte is below +// PUSH1, so no chunk carries PUSHDATA and a root mismatch cannot be blamed on +// the scan the vector tests already pin. The fill depends on the length, so two +// different lengths never share a chunk. +func pbinTestCode(n int) []byte { + code := make([]byte, n) + for i := range code { + code[i] = byte(n+i) % pbinPush1 + } + return code +} + +// TestPBinEngineEmitsHeaderCodeChunks is the first half of code in the tree: a +// code-bearing account's chunks have to reach the leaf set the reference tree +// builds for it, at the header sub-indices CODE_OFFSET.. . +func TestPBinEngineEmitsHeaderCodeChunks(t *testing.T) { + t.Parallel() + + addr := pbinOracleAddr(11) + code := pbinTestCode(200) + corpus := new(pbinTestCorpus).accountWithCodeBytes(addr, 4, 500, code) + + // Non-vacuity: the corpus states the fan-out independently of the engine. + require.Equal(t, 2+7, corpus.leafCount(t), "two header leaves plus ceil(200/31) chunks") + + _, root := corpus.process(t) + require.Equal(t, corpus.oracleRoot(t), root) +} + +// TestPBinCodeChunksFollowHeaderSlots guards H5: chunks sit at the top +// sub-indices of the stem, so emitting them at the account's own visit descends +// past a header storage slot the stream has not delivered yet, and the fold that +// comes back for it rewrites a record it had already written. +func TestPBinCodeChunksFollowHeaderSlots(t *testing.T) { + t.Parallel() + + addr := pbinOracleAddr(12) + corpus := new(pbinTestCorpus). + accountWithCodeBytes(addr, 1, 2, pbinTestCode(200)). + storage(addr, pbinOracleSlot(5), 0x77). + storage(addr, pbinOracleSlot(63), 0x88). + storage(addr, pbinOracleSlot(1000), 0x99) + + _, root := corpus.process(t) + require.Equal(t, corpus.oracleRoot(t), root) +} + +// TestPBinVisitOrderIsMonotonic is the structural assert behind H5: the grid only +// walks forward, so a visit that revisits a key already left behind is a bug in +// the caller's ordering, not something the fold can absorb. +func TestPBinVisitOrderIsMonotonic(t *testing.T) { + t.Parallel() + + pph, _ := pbinTestEngine(t) + addr := pbinOracleAddr(13) + u := Update{Flags: NonceUpdate} + + require.NoError(t, pph.followAndUpdate(pbinTreeKeyAccount(addr, pbinCodeHashLeafKey), addr, &u)) + err := pph.followAndUpdate(pbinTreeKeyAccount(addr, pbinBasicDataLeafKey), addr, &u) + require.ErrorIs(t, err, errPBinVisitOrder) +} + +// TestPBinCodeChunksSurviveAsRecordSiblings pins that a chunk leaf carries its +// own value: an untouched chunk sibling of a touched one has to hash from the +// branch record, and no state domain holds a chunk. +func TestPBinCodeChunksSurviveAsRecordSiblings(t *testing.T) { + t.Parallel() + + addr := pbinOracleAddr(14) + // Two chunks, then a shorter code touching only chunk 0: chunk 1 stays behind + // as a direct leaf sibling, which is the one shape that must reload its value. + deploy := new(pbinTestCorpus).accountWithCodeBytes(addr, 1, 10, pbinTestCode(62)) + stale := pbinChunkifyCode(pbinTestCode(62))[1] + redeploy := new(pbinTestCorpus).accountWithCodeBytes(addr, 2, 10, pbinTestCode(31)) + + _, _, root := pbinTestBatches(t, deploy, redeploy) + + want := append(redeploy.entries(t), pbinOracleEntry{ + key: pbinTreeKeyCodeChunk(addr, 1), + value: stale[:], + }) + wantRoot := pbinOracleRoot(want) + require.Equal(t, wantRoot[:], root, "the untouched chunk keeps the value the record holds") +} + +// TestPBinShorteningRedeployKeepsStaleChunks records the answer to Q2 as a test +// (guards H8). EIP-8297 has no removal, so a redeploy to shorter code leaves the +// chunks above the new length in place: a forward run commits them, a recompute +// from the state domains cannot know they exist. The two roots are each +// internally consistent and different, which is what makes recompute-from-domains +// invalid as an oracle for a code-bearing account. +func TestPBinShorteningRedeployKeepsStaleChunks(t *testing.T) { + t.Parallel() + + for _, tc := range []struct{ before, after int }{ + {before: 62, after: 31}, // 2 chunks down to 1: the residue is a leaf sibling + {before: 200, after: 62}, // 7 down to 2: the residue is a whole subtree + } { + t.Run(fmt.Sprintf("%d bytes down to %d", tc.before, tc.after), func(t *testing.T) { + t.Parallel() + + addr := pbinOracleAddr(15) + long, short := pbinTestCode(tc.before), pbinTestCode(tc.after) + deploy := new(pbinTestCorpus).accountWithCodeBytes(addr, 1, 10, long) + redeploy := new(pbinTestCorpus).accountWithCodeBytes(addr, 2, 10, short) + + _, _, forward := pbinTestBatches(t, deploy, redeploy) + + _, rebuilt := new(pbinTestCorpus).accountWithCodeBytes(addr, 2, 10, short).process(t) + require.NotEqual(t, rebuilt, forward, + "a rebuild from state cannot reproduce the stale chunks the forward run kept") + + // The residue is exactly the chunks the old code had and the new one does not. + want := redeploy.entries(t) + oldChunks := pbinChunkifyCode(long) + for i := len(pbinChunkifyCode(short)); i < len(oldChunks); i++ { + want = append(want, pbinOracleEntry{key: pbinTreeKeyCodeChunk(addr, i), value: oldChunks[i][:]}) + } + wantRoot := pbinOracleRoot(want) + require.Equal(t, wantRoot[:], forward) + }) + } +} + +// TestPBinCodeBeyondHeaderRefused pins the M1 boundary: chunks past the header +// belong in the code zone, which overflow support brings, and until then a +// contract that needs them is refused rather than committed short. +func TestPBinCodeBeyondHeaderRefused(t *testing.T) { + t.Parallel() + + addr := pbinOracleAddr(16) + code := pbinTestCode(pbinHeaderCodeChunks*pbinChunkDataLen + 1) + corpus := new(pbinTestCorpus).accountWithCodeBytes(addr, 1, 10, code) + + pph, ms := pbinTestEngine(t) + corpus.applyTo(t, ms) + upd := WrapKeyUpdates(t, ModeDirect, pbinKeyHasher(), corpus.plainKeys, corpus.updates) + _, err := pph.Process(context.Background(), upd, "", nil, WarmupConfig{}) + require.ErrorIs(t, err, ErrPBinUnsupported) +} + +// TestPBinCodelessContextRefusesCodeBearingAccount pins that the code read is +// not optional: a context that cannot serve code cannot commit an account whose +// chunks the tree needs. +func TestPBinCodelessContextRefusesCodeBearingAccount(t *testing.T) { + t.Parallel() + + addr := pbinOracleAddr(17) + corpus := new(pbinTestCorpus).accountWithCodeBytes(addr, 1, 10, pbinTestCode(62)) + + ms := NewMockState(t) + corpus.applyTo(t, ms) + pph := NewPBinPatriciaHashed(pbinCodelessContext{ms}) + upd := WrapKeyUpdates(t, ModeDirect, pbinKeyHasher(), corpus.plainKeys, corpus.updates) + + _, err := pph.Process(context.Background(), upd, "", nil, WarmupConfig{}) + require.ErrorIs(t, err, ErrPBinUnsupported) +} + +// pbinCodelessContext is a PatriciaContext with no code read, which embedding the +// interface rather than the concrete state is what produces. +type pbinCodelessContext struct{ PatriciaContext } + +// TestPBinCodeSizeMustMatchTheCodeBehindIt pins that the two reads agree: the +// BASIC_DATA size and the chunks come from separate reads, and a size that +// disagrees with the code would commit a leaf set no reference tree holds. +func TestPBinCodeSizeMustMatchTheCodeBehindIt(t *testing.T) { + t.Parallel() + + addr := pbinOracleAddr(18) + corpus := new(pbinTestCorpus).accountWithCodeBytes(addr, 1, 10, pbinTestCode(62)) + + pph, ms := pbinTestEngine(t) + corpus.applyTo(t, ms) + ms.setCode(addr, pbinTestCode(31)) // the account still says 62 bytes + + upd := WrapKeyUpdates(t, ModeDirect, pbinKeyHasher(), corpus.plainKeys, corpus.updates) + _, err := pph.Process(context.Background(), upd, "", nil, WarmupConfig{}) + require.Error(t, err) +} + +// TestPBinZoneKeyLengthIsExplicit pins that the code zone is recognised rather +// than passing as an account key because both are 34 bytes, and that the zones +// the embedding has not allocated are refused. +func TestPBinZoneKeyLengthIsExplicit(t *testing.T) { + t.Parallel() + + for _, tc := range []struct { + zone byte + want int + known bool + }{ + {zone: pbinAccountZone, want: pbinAccountKeyLength, known: true}, + {zone: pbinCodeZone, want: pbinCodeKeyLength, known: true}, + {zone: pbinStorageZone, want: pbinStorageKeyLength, known: true}, + {zone: 0x02}, {zone: 0x7F}, {zone: 0xFE}, + } { + got, known := pbinZoneKeyLength(tc.zone) + require.Equal(t, tc.known, known, "zone %#x", tc.zone) + require.Equal(t, tc.want, got, "zone %#x", tc.zone) + } + + require.Panics(t, func() { pbinTreeKey(0x02, make([]byte, 32), 0) }, "an unallocated zone has no key length") + require.Len(t, pbinTreeKey(pbinCodeZone, make([]byte, 32), 0), pbinCodeKeyLength) +} + +// TestPBinLeafValueRoutesByZone pins the second place a code key used to pass by +// accident: the leaf value is picked by the key's zone, so a code-zone key must +// not be read as an account header sub-index. +func TestPBinLeafValueRoutesByZone(t *testing.T) { + t.Parallel() + + chunk := pbinChunkifyCode(pbinTestCode(31))[0] + u := Update{Flags: StorageUpdate, StorageLen: pbinValueLength} + copy(u.Storage[:], chunk[:]) + + // A code-zone key at sub-index 0 would be BASIC_DATA if the zone were ignored. + got, err := pbinLeafValue(pbinTreeKey(pbinCodeZone, make([]byte, 32), 0), &u) + require.NoError(t, err) + require.Equal(t, chunk[:], got[:]) + + // The same is true inside the account zone: sub-indices at CODE_OFFSET and + // above are chunks, not storage. + addr := pbinOracleAddr(19) + got, err = pbinLeafValue(pbinTreeKeyCodeChunk(addr, 0), &u) + require.NoError(t, err) + require.Equal(t, chunk[:], got[:]) + + // A chunk leaf holding fewer than 32 value bytes cannot be left-padded into + // place the way a storage value can: byte 0 is the PUSHDATA count. + short := Update{Flags: StorageUpdate, StorageLen: 4} + _, err = pbinLeafValue(pbinTreeKeyCodeChunk(addr, 1), &short) + require.ErrorIs(t, err, errPBinCellHash) +} + +// TestPBinLeafCellHashChecksZoneLength pins the third site: a leaf's key length +// has to match its own zone, so a 34-byte storage key or a 66-byte code key is +// rejected instead of hashing. +func TestPBinLeafCellHashChecksZoneLength(t *testing.T) { + t.Parallel() + + var h pbinHasher + u := Update{Flags: StorageUpdate, StorageLen: pbinValueLength} + + for _, tc := range []struct { + name string + key []byte + }{ + {name: "storage zone at account length", key: append([]byte{pbinStorageZone}, make([]byte, pbinAccountKeyLength-1)...)}, + {name: "code zone at storage length", key: append([]byte{pbinCodeZone}, make([]byte, pbinStorageKeyLength-1)...)}, + {name: "unallocated zone", key: append([]byte{0x02}, make([]byte, pbinAccountKeyLength-1)...)}, + } { + t.Run(tc.name, func(t *testing.T) { + t.Parallel() + c := pbinCell{kind: pbinNodeLeaf, prefix: pbinPathFromBytes(tc.key), Update: u} + var path pbinBitpath + _, err := h.cellHash(&c, &path) + require.ErrorIs(t, err, errPBinCellHash) + }) + } +} diff --git a/execution/commitment/pbin_codesize_test.go b/execution/commitment/pbin_codesize_test.go index cc634af8a43..db1ec78b472 100644 --- a/execution/commitment/pbin_codesize_test.go +++ b/execution/commitment/pbin_codesize_test.go @@ -17,6 +17,7 @@ package commitment import ( + "bytes" "context" "testing" @@ -56,14 +57,28 @@ func TestPBinEngineRootCarriesCodeSize(t *testing.T) { t.Parallel() addr := pbinOracleAddr(9) - codeHash := common.Hash{0xC0, 0xDE} - withCode := new(pbinTestCorpus).accountWithCode(addr, 4, 500, codeHash, 6358) + code := pbinTestCode(1000) + withCode := new(pbinTestCorpus).accountWithCodeBytes(addr, 4, 500, code) _, root := withCode.process(t) require.Equal(t, withCode.oracleRoot(t), root) - sizeless := new(pbinTestCorpus).accountWithCode(addr, 4, 500, codeHash, 0) - require.NotEqual(t, sizeless.oracleRoot(t), root, "code_size must reach the root") + // The same leaf set with BASIC_DATA packed at code_size 0 isolates the size: + // every other leaf, the chunks included, stays where it was. + sizeless, err := pbinEncodeBasicData(4, uint256.NewInt(500), 0) + require.NoError(t, err) + entries := withCode.entries(t) + basicDataKey := pbinTreeKeyAccount(addr, pbinBasicDataLeafKey) + patched := 0 + for i := range entries { + if bytes.Equal(entries[i].key, basicDataKey) { + entries[i].value, patched = sizeless[:], patched+1 + } + } + require.Equal(t, 1, patched) + + want := pbinOracleRoot(entries) + require.NotEqual(t, want[:], root, "code_size must reach the root") } // TestPBinUpdateCodeSizeSurvivesCopyAndReset pins the two Update lifecycle diff --git a/execution/commitment/pbin_hash.go b/execution/commitment/pbin_hash.go index 3c51a213931..6d8893e2dd0 100644 --- a/execution/commitment/pbin_hash.go +++ b/execution/commitment/pbin_hash.go @@ -114,12 +114,18 @@ func (h *pbinHasher) leafCellHash(c *pbinCell, path *pbinBitpath) (common.Hash, return common.Hash{}, fmt.Errorf("%w: leaf key of %d+%d bits overflows", errPBinCellHash, full.bitLen, c.prefix.bitLen) } full.append(&c.prefix) - if full.bitLen != pbinAccountKeyLength*8 && full.bitLen != pbinStorageKeyLength*8 { - return common.Hash{}, fmt.Errorf("%w: leaf key of %d bits is neither zone length", errPBinCellHash, full.bitLen) + if full.bitLen%8 != 0 { + return common.Hash{}, fmt.Errorf("%w: leaf key of %d bits is not whole bytes", errPBinCellHash, full.bitLen) } buf := full.appendPackedBits(append(h.buf[:0], pbinLeafTag)) - value, err := pbinLeafValue(buf[1:], &c.Update) + key := buf[1:] + // The length is fixed per zone, which is what keeps keys prefix-free: a key of + // another zone's length is not a key at all (eip:284-288). + if want, known := pbinZoneKeyLength(key[0]); !known || len(key) != want { + return common.Hash{}, fmt.Errorf("%w: leaf key %x is no key of zone %#x", errPBinCellHash, key, key[0]) + } + value, err := pbinLeafValue(key, &c.Update) if err != nil { return common.Hash{}, err } @@ -127,11 +133,18 @@ func (h *pbinHasher) leafCellHash(c *pbinCell, path *pbinBitpath) (common.Hash, } // pbinLeafValue picks the encoding the key's own position names: the zone byte -// separates storage from the account header, and within the header the -// sub-index selects between BASIC_DATA, CODE_HASH and a header-resident slot. +// separates storage and code from the account header, and within the header the +// sub-index selects between BASIC_DATA, CODE_HASH, a header-resident slot and a +// header-resident code chunk. func pbinLeafValue(key []byte, u *Update) ([pbinValueLength]byte, error) { - if key[0] == pbinStorageZone { + switch key[0] { + case pbinStorageZone: return pbinEncodeStorageValue(u.Storage[:u.StorageLen]), nil + case pbinCodeZone: + return pbinCodeChunkValue(u) + case pbinAccountZone: + default: + return [pbinValueLength]byte{}, fmt.Errorf("%w: zone %#x names no leaf", errPBinCellHash, key[0]) } switch subIndex := key[len(key)-1]; { case subIndex == pbinBasicDataLeafKey: @@ -140,6 +153,8 @@ func pbinLeafValue(key []byte, u *Update) ([pbinValueLength]byte, error) { return pbinCodeHashValue(u.CodeHash), nil case subIndex >= pbinHeaderStorageOffset && subIndex < pbinCodeOffset: return pbinEncodeStorageValue(u.Storage[:u.StorageLen]), nil + case subIndex >= pbinCodeOffset: + return pbinCodeChunkValue(u) default: return [pbinValueLength]byte{}, fmt.Errorf("%w: account-zone sub-index %d names no leaf", errPBinCellHash, subIndex) } diff --git a/execution/commitment/pbin_hazard_test.go b/execution/commitment/pbin_hazard_test.go index 236bdcf8f59..adff8f48789 100644 --- a/execution/commitment/pbin_hazard_test.go +++ b/execution/commitment/pbin_hazard_test.go @@ -34,7 +34,7 @@ func pbinTestBatches(t *testing.T, batches ...*pbinTestCorpus) (*PBinPatriciaHas pph, ms := pbinTestEngine(t) var root []byte for _, b := range batches { - require.NoError(t, ms.applyPlainUpdates(b.plainKeys, b.updates)) + b.applyTo(t, ms) root = bytes.Clone(pbinTestProcess(t, pph, b.plainKeys, b.updates)) } return pph, ms, root @@ -48,6 +48,12 @@ func pbinTestUnion(batches ...*pbinTestCorpus) *pbinTestCorpus { for _, b := range batches { u.plainKeys = append(u.plainKeys, b.plainKeys...) u.updates = append(u.updates, b.updates...) + for addr, code := range b.codes { + if u.codes == nil { + u.codes = make(map[string][]byte) + } + u.codes[addr] = code + } } return u } diff --git a/execution/commitment/pbin_keys.go b/execution/commitment/pbin_keys.go index 27fbaacc52f..2eded2a65c4 100644 --- a/execution/commitment/pbin_keys.go +++ b/execution/commitment/pbin_keys.go @@ -31,14 +31,33 @@ const ( pbinCodeHashLeafKey = 1 pbinHeaderStorageOffset = 64 pbinCodeOffset = 128 + pbinStemSubtreeWidth = 256 pbinAccountZone = 0x00 + pbinCodeZone = 0x01 pbinStorageZone = 0xFF pbinAccountKeyLength = 34 + pbinCodeKeyLength = 34 pbinStorageKeyLength = 66 ) +// pbinZoneKeyLength is the one key length a zone admits, which is what keeps its +// keys prefix-free (eip:284-288). An unknown zone has no length: the embedding +// allocates 0x02..0xFE to nothing yet. +func pbinZoneKeyLength(zone byte) (int, bool) { + switch zone { + case pbinAccountZone: + return pbinAccountKeyLength, true + case pbinCodeZone: + return pbinCodeKeyLength, true + case pbinStorageZone: + return pbinStorageKeyLength, true + default: + return 0, false + } +} + // pbinAddr32 widens a legacy address to the spec's Address32 by left-padding // with zero bytes (eip:291-296). func pbinAddr32(addr []byte) [32]byte { @@ -59,9 +78,9 @@ func pbinTreeKey(zone byte, treePosition []byte, subIndex byte) []byte { key = append(key, treePosition...) key = append(key, subIndex) - want := pbinAccountKeyLength - if zone == pbinStorageZone { - want = pbinStorageKeyLength + want, known := pbinZoneKeyLength(zone) + if !known { + panic(fmt.Sprintf("pbin: zone %#x names no key space", zone)) } if len(key) != want { panic(fmt.Sprintf("pbin: zone %#x key of %d bytes, want %d", zone, len(key), want)) @@ -83,6 +102,14 @@ func pbinTreeKeyStorage(addr, slot []byte) []byte { return c.storageKey(addr, slot) } +// pbinTreeKeyCodeChunk returns the key for code chunk chunkID of addr +// (eip:355-367). Chunks the account header holds share the account's own stem; +// higher chunks are content-addressed by code hash in the code zone. +func pbinTreeKeyCodeChunk(addr []byte, chunkID int) []byte { + var c pbinDigestCache + return c.codeChunkKey(addr, chunkID) +} + // pbinKeyHasher returns a keyHasher deriving the primary leaf's tree key: // BASIC_DATA for an account, the slot's own leaf for storage. The CODE_HASH // sibling shares the stem and is written by the engine during the same visit, @@ -167,6 +194,13 @@ func (c *pbinDigestCache) accountKey(addr []byte, subIndex byte) []byte { return pbinTreeKey(pbinAccountZone, c.stemDigest(&addr32)[:], subIndex) } +func (c *pbinDigestCache) codeChunkKey(addr []byte, chunkID int) []byte { + if chunkID < 0 || chunkID >= pbinHeaderCodeChunks { + panic(fmt.Sprintf("pbin: code chunk %d lives outside the account header", chunkID)) + } + return c.accountKey(addr, byte(pbinCodeOffset+chunkID)) +} + func (c *pbinDigestCache) storageKey(addr, slot []byte) []byte { addr32 := pbinAddr32(addr) slot32 := pbinSlot32(slot) diff --git a/execution/commitment/pbin_patricia_hashed.go b/execution/commitment/pbin_patricia_hashed.go index 268c0adca7f..bacff827438 100644 --- a/execution/commitment/pbin_patricia_hashed.go +++ b/execution/commitment/pbin_patricia_hashed.go @@ -20,12 +20,13 @@ // this engine uses Keccak-256 both for node hashing and for tree-key // derivation, behind pbinHasher so the suite can be swapped. // -// M0 scope: in-memory Process over the account and storage zones, ModeDirect -// only. BASIC_DATA carries the account's real code_size, but the CODE_ZONE -// chunks it describes are not in the tree yet, so a code-bearing account hashes -// incompletely. Parallel mounting is out. EIP-8297 has no removal: a zeroed -// storage slot keeps its leaf at 32 zero bytes, while an account removal is -// refused rather than guessed at. +// Scope: Process over the account and storage zones, ModeDirect only. Code is +// chunked into the account header's own chunk leaves; a contract needing more +// than the header holds is refused until the code zone lands. Parallel mounting +// is out. EIP-8297 has no removal: a zeroed storage slot keeps its leaf at 32 +// zero bytes, an account removal is refused rather than guessed at, and code +// chunks above a shortened redeploy's length stay in the tree — the tree is a +// function of history there, not of current state. package commitment @@ -54,7 +55,11 @@ type PBinPatriciaHashed struct { branchEncoder pbinBranchEncoder counters pbinCounters - siblingKey [pbinAccountKeyLength]byte // scratch for the CODE_HASH key of the account being visited + siblingKey [pbinAccountKeyLength]byte // scratch for the CODE_HASH key of the account being visited + pendingCode pbinPendingCode + + lastKey [pbinStorageKeyLength]byte // the deepest key visited so far, which the next one must exceed + lastKeyLen int16 traceW io.Writer // nil = disabled @@ -105,6 +110,8 @@ func (pph *PBinPatriciaHashed) Reset() { pph.currentKey = pbinBitpath{} pph.rootChecked, pph.rootTouched, pph.rootPresent = false, false, false pph.rootPrev = nil + pph.pendingCode = pbinPendingCode{} + pph.lastKeyLen = 0 } // setHashSuite swaps H on both seams at once — node hashing on this engine and @@ -129,8 +136,16 @@ func (pph *PBinPatriciaHashed) Release() { var ( errPBinMissingBranch = errors.New("pbin: branch record missing") errPBinDeleteUnsupported = errors.New("pbin: EIP-8297 defines no deletion") + errPBinVisitOrder = errors.New("pbin: visit order is not ascending") ) +// pbinCodeContext is the read code chunking needs. PatriciaContext hands out +// account state, not the bytecode the chunk leaves hold, so a context that +// cannot serve code cannot commit a code-bearing account. +type pbinCodeContext interface { + Code(plainKey []byte) ([]byte, error) +} + // ErrPBinUnsupported marks a code path only the hex trie implements. Callers // wrap it with the path name so the bin variant refuses instead of no-opping. var ErrPBinUnsupported = errors.New("pbin: unsupported under the bin commitment variant") @@ -151,6 +166,9 @@ var pbinRootKey = []byte{0x08} // no page cache to pre-warm. func (pph *PBinPatriciaHashed) Process(ctx context.Context, updates *Updates, logPrefix string, onProgress func(*CommitProgress), warmup WarmupConfig) ([]byte, error) { var processed uint64 + // Each run is its own ascending stream: the grid is back at the root, so the + // key the previous run ended on bounds nothing. + pph.lastKeyLen = 0 err := updates.HashSort(ctx, nil, func(treeKey, plainKey []byte, stateUpdate *Update) error { if err := pph.processKey(treeKey, plainKey, stateUpdate); err != nil { return err @@ -161,6 +179,9 @@ func (pph *PBinPatriciaHashed) Process(ctx context.Context, updates *Updates, lo if err != nil { return nil, fmt.Errorf("pbin: process %s: %w", logPrefix, err) } + if err = pph.flushPendingCode(); err != nil { + return nil, fmt.Errorf("pbin: process %s: %w", logPrefix, err) + } for pph.grid.activeRows > 0 { if err = pph.fold(); err != nil { return nil, fmt.Errorf("pbin: final fold: %w", err) @@ -182,10 +203,15 @@ func (pph *PBinPatriciaHashed) Process(ctx context.Context, updates *Updates, lo // processKey routes one update into the tree. An account fans out to two leaves // visited back to back — BASIC_DATA and the CODE_HASH sibling at the next // sub-index — which is what lets the shared keyHasher stay a one-key function. +// Its code chunks sit at the top of the same stem and are held back until the +// stream leaves it. func (pph *PBinPatriciaHashed) processKey(treeKey, plainKey []byte, stateUpdate *Update) error { if stateUpdate != nil && stateUpdate.Deleted() { return fmt.Errorf("%w: update for %x", errPBinDeleteUnsupported, plainKey) } + if err := pph.flushPendingCodeBefore(treeKey); err != nil { + return err + } update := stateUpdate if update == nil { var err error @@ -203,7 +229,89 @@ func (pph *PBinPatriciaHashed) processKey(treeKey, plainKey []byte, stateUpdate if err != nil { return err } - return pph.followAndUpdate(codeKey, plainKey, update) + if err = pph.followAndUpdate(codeKey, plainKey, update); err != nil { + return err + } + return pph.queueCode(treeKey, plainKey, update) +} + +// pbinPendingCode is one account's code fan-out, waiting for the stream to leave +// its stem. Chunks occupy the header's top sub-indices, so emitting them at the +// account's own visit would descend past a header storage slot the stream has not +// delivered yet, and coming back for it would rewrite a record the fold had +// already written. +type pbinPendingCode struct { + stem [pbinAccountKeyLength - 1]byte + plainKey [length.Addr]byte + chunks [][pbinValueLength]byte +} + +// queueCode reads the account's code and holds its chunks until the stem is done. +// The size the BASIC_DATA leaf hashes and the code the chunks come from are two +// reads, so they are checked against each other rather than trusted apart. +func (pph *PBinPatriciaHashed) queueCode(basicDataKey, plainKey []byte, update *Update) error { + if update.CodeSize == 0 { + return nil + } + if len(pph.pendingCode.chunks) != 0 { + return fmt.Errorf("pbin: code for %x queued while %x is still pending: the stem exit was missed", + plainKey, pph.pendingCode.plainKey[:]) + } + code, err := pph.codeOf(plainKey) + if err != nil { + return err + } + if uint64(len(code)) != update.CodeSize { + return fmt.Errorf("pbin: account %x says %d code bytes, the code domain holds %d", + plainKey, update.CodeSize, len(code)) + } + chunks := pbinChunkifyCode(code) + if len(chunks) > pbinHeaderCodeChunks { + return fmt.Errorf("%w: %x needs %d code chunks, the account header holds %d", + ErrPBinUnsupported, plainKey, len(chunks), pbinHeaderCodeChunks) + } + pph.pendingCode.chunks = chunks + copy(pph.pendingCode.stem[:], basicDataKey) + copy(pph.pendingCode.plainKey[:], plainKey) + return nil +} + +func (pph *PBinPatriciaHashed) codeOf(plainKey []byte) ([]byte, error) { + ctx, ok := pph.ctx.(pbinCodeContext) + if !ok { + return nil, fmt.Errorf("%w: %T serves no code, needed to chunk account %x", + ErrPBinUnsupported, pph.ctx, plainKey) + } + code, err := ctx.Code(plainKey) + if err != nil { + return nil, fmt.Errorf("pbin: read code %x: %w", plainKey, err) + } + return code, nil +} + +// flushPendingCodeBefore emits the held-back chunks when treeKey leaves their +// stem. Chunk sub-indices are the highest in a stem, so a stem the stream has +// left is a stem no key can return to. +func (pph *PBinPatriciaHashed) flushPendingCodeBefore(treeKey []byte) error { + if len(pph.pendingCode.chunks) == 0 || bytes.HasPrefix(treeKey, pph.pendingCode.stem[:]) { + return nil + } + return pph.flushPendingCode() +} + +func (pph *PBinPatriciaHashed) flushPendingCode() error { + p := &pph.pendingCode + var key [pbinAccountKeyLength]byte + copy(key[:], p.stem[:]) + for i := range p.chunks { + key[pbinAccountKeyLength-1] = byte(pbinCodeOffset + i) + update := Update{Flags: StorageUpdate, StorageLen: pbinValueLength, Storage: p.chunks[i]} + if err := pph.followAndUpdate(key[:], nil, &update); err != nil { + return err + } + } + p.chunks = nil + return nil } func (pph *PBinPatriciaHashed) stateOf(plainKey []byte) (*Update, error) { @@ -235,7 +343,16 @@ func (pph *PBinPatriciaHashed) codeHashKey(basicDataKey []byte) ([]byte, error) // followAndUpdate moves the grid onto treeKey and writes the update into the // cell that lands there. +// +// Visits must ascend. The grid only walks forward: a fold writes the row's record +// outright, so returning to a folded row rewrites it under a touch map that no +// longer names what the first write touched. func (pph *PBinPatriciaHashed) followAndUpdate(treeKey, plainKey []byte, update *Update) error { + if pph.lastKeyLen > 0 && bytes.Compare(treeKey, pph.lastKey[:pph.lastKeyLen]) <= 0 { + return fmt.Errorf("%w: %x after %x", errPBinVisitOrder, treeKey, pph.lastKey[:pph.lastKeyLen]) + } + pph.lastKeyLen = int16(copy(pph.lastKey[:], treeKey)) + probe := pbinPathFromBytes(treeKey) for !probe.hasPrefix(&pph.currentKey) { if err := pph.fold(); err != nil { @@ -302,6 +419,12 @@ func (pph *PBinPatriciaHashed) updateCell(plainKey []byte, probe *pbinBitpath, u } switch len(plainKey) { + case 0: + // A code chunk has no plain key: no state domain holds one, so the leaf + // carries its own value and the branch record persists it. + if _, err := pbinCodeChunkValue(update); err != nil { + return err + } case length.Addr: c.accountAddrLen = int16(len(plainKey)) copy(c.accountAddr[:], plainKey) diff --git a/execution/commitment/pbin_process_test.go b/execution/commitment/pbin_process_test.go index f1fec733bc3..1d5f2bde6b4 100644 --- a/execution/commitment/pbin_process_test.go +++ b/execution/commitment/pbin_process_test.go @@ -22,6 +22,7 @@ import ( "errors" "testing" + keccak "github.com/erigontech/fastkeccak" "github.com/stretchr/testify/require" "github.com/erigontech/erigon/common" @@ -36,6 +37,7 @@ import ( type pbinTestCorpus struct { plainKeys [][]byte updates []Update + codes map[string][]byte } func (c *pbinTestCorpus) account(addr []byte, nonce, balance uint64, codeHash common.Hash) *pbinTestCorpus { @@ -50,6 +52,17 @@ func (c *pbinTestCorpus) accountWithCode(addr []byte, nonce, balance uint64, cod return c } +// accountWithCodeBytes is the code-bearing account with its code behind it: the +// hash and size come from the code, and the tree gains one leaf per chunk. +func (c *pbinTestCorpus) accountWithCodeBytes(addr []byte, nonce, balance uint64, code []byte) *pbinTestCorpus { + c.accountWithCode(addr, nonce, balance, keccak.Sum256(code), uint64(len(code))) + if c.codes == nil { + c.codes = make(map[string][]byte) + } + c.codes[string(addr)] = bytes.Clone(code) + return c +} + func (c *pbinTestCorpus) storage(addr, slot []byte, value ...byte) *pbinTestCorpus { u := Update{Flags: StorageUpdate, StorageLen: int8(len(value))} copy(u.Storage[:], value) @@ -73,6 +86,12 @@ func (c *pbinTestCorpus) entries(t *testing.T) []pbinOracleEntry { entries = append(entries, pbinOracleEntry{key: pbinTreeKeyAccount(plainKey, pbinBasicDataLeafKey), value: basic[:]}, pbinOracleEntry{key: pbinTreeKeyAccount(plainKey, pbinCodeHashLeafKey), value: code[:]}) + for j, chunk := range pbinChunkifyCode(c.codes[string(plainKey)]) { + entries = append(entries, pbinOracleEntry{ + key: pbinTreeKeyCodeChunk(plainKey, j), + value: chunk[:], + }) + } case length.Addr + length.Hash: value := pbinEncodeStorageValue(u.Storage[:u.StorageLen]) entries = append(entries, pbinOracleEntry{ @@ -98,10 +117,21 @@ func (c *pbinTestCorpus) oracleRoot(t *testing.T) []byte { func (c *pbinTestCorpus) process(t *testing.T) (*PBinPatriciaHashed, []byte) { t.Helper() pph, ms := pbinTestEngine(t) - require.NoError(t, ms.applyPlainUpdates(c.plainKeys, c.updates)) + c.applyTo(t, ms) return pph, pbinTestProcess(t, pph, c.plainKeys, c.updates) } +// applyTo writes the corpus into state, code included: the engine reads code +// through the context, so a code-bearing account with no code behind it is a +// state the corpus must not produce. +func (c *pbinTestCorpus) applyTo(t *testing.T, ms *MockState) { + t.Helper() + require.NoError(t, ms.applyPlainUpdates(c.plainKeys, c.updates)) + for addr, code := range c.codes { + ms.setCode([]byte(addr), code) + } +} + func pbinTestProcess(t *testing.T, pph *PBinPatriciaHashed, plainKeys [][]byte, updates []Update) []byte { t.Helper() upd := WrapKeyUpdates(t, ModeDirect, pbinKeyHasher(), plainKeys, updates) diff --git a/execution/commitment/pbin_specvectors_test.go b/execution/commitment/pbin_specvectors_test.go index 0c944966c73..5ad831b0f1a 100644 --- a/execution/commitment/pbin_specvectors_test.go +++ b/execution/commitment/pbin_specvectors_test.go @@ -33,6 +33,13 @@ type pbinSpecVectors struct { Key string `json:"key"` } `json:"slots"` } `json:"embedding_vectors"` + Chunkify []pbinSpecChunkifyVector `json:"chunkify_vectors"` +} + +type pbinSpecChunkifyVector struct { + Name string `json:"name"` + Code string `json:"code"` + Chunks []string `json:"chunks"` } func loadPBinSpecVectors(t *testing.T) pbinSpecVectors { From 43eed08843e38f35bde533a77280370759b0feb7 Mon Sep 17 00:00:00 2001 From: awskii Date: Thu, 30 Jul 2026 21:17:29 +0700 Subject: [PATCH 36/56] feat: CODE_ZONE overflow chunks in the pbin tree --- docs/plans/20260730-pbin-m1-local-el.md | 24 +-- .../commitment/commitmentdb/pbin_code_test.go | 22 ++- execution/commitment/pbin_branch.go | 2 +- execution/commitment/pbin_code.go | 12 +- execution/commitment/pbin_code_test.go | 17 -- execution/commitment/pbin_hash.go | 13 +- execution/commitment/pbin_keys.go | 34 +++- execution/commitment/pbin_overflow_test.go | 186 ++++++++++++++++++ execution/commitment/pbin_patricia_hashed.go | 77 +++++++- execution/commitment/pbin_process_test.go | 11 +- execution/commitment/pbin_specengine_test.go | 25 ++- execution/commitment/pbin_verify_test.go | 16 +- 12 files changed, 373 insertions(+), 66 deletions(-) create mode 100644 execution/commitment/pbin_overflow_test.go diff --git a/docs/plans/20260730-pbin-m1-local-el.md b/docs/plans/20260730-pbin-m1-local-el.md index 84365ce366e..413e7776f5b 100644 --- a/docs/plans/20260730-pbin-m1-local-el.md +++ b/docs/plans/20260730-pbin-m1-local-el.md @@ -15,7 +15,7 @@ Defaulting to ON is the safety property: a bin run against foreign headers fails This needs no overlay or migration mechanism — verified: dev pins no genesis hash (`execution/chain/spec/genesis.go:141-171`), the dev beacon takes `Eth1Data` from the runtime-computed EL genesis hash (`cmd/utils/flags.go:2243-2250`), and the header root and block-0 exec root come from the same function (`genesiswrite.ComputeGenesisCommitment` → `sd.ComputeCommitment`, `genesis_write.go:468`), so they flip together. -But dev is not a cheap target. Its alloc (`execution/chain/spec/allocs/dev.json`) has 18 entries, 7 code-bearing, and the deposit contract `0x00000000219ab540...705Fa` is 6358 bytes = 206 chunks = 128 header + **78 CODE_ZONE overflow chunks**. Overflow keys are `key_hash(code_hash ‖ tree_index)`, which cannot be derived from a 20-byte plain key — so the one unavoidable API break lands on day one. The contract cannot be dropped: dev is PoS-from-genesis (`TerminalTotalDifficulty: 0`, `CancunTime: 0`, `DepositContract` set, `genesis.go:157-162`). +But dev is not a cheap target. Its alloc (`execution/chain/spec/allocs/dev.json`) has 18 entries, 7 code-bearing, and the deposit contract `0x00000000219ab540...705Fa` is 6358 bytes = 206 chunks = 128 header + **78 CODE_ZONE overflow chunks**. Overflow keys are `key_hash(code_hash ‖ tree_index)`, which cannot be derived from a 20-byte plain key. The plan expected that to force a plain-key namespace break on day one; it did not — Task 12 put the chunk value in the branch record, so a chunk leaf has no plain key and the engine derives the overflow key itself (Task 13). The contract cannot be dropped: dev is PoS-from-genesis (`TerminalTotalDifficulty: 0`, `CancunTime: 0`, `DepositContract` set, `genesis.go:157-162`). **M1a is a mandatory intermediate gate, not acceptance.** pbin over a real MDBX datadir with no consensus, via `RebuildCommitmentFiles` (`db/state/squeeze.go:876`) or `backtester` (`execution/commitment/backtester/backtester.go:199-215`). It is the only place collation, merge, restart and branch-record round-trip get exercised without consensus noise — but **it has no header-root oracle**. A wrong root there surfaces only as non-determinism between a forward run and a rebuild. Do not mistake a green M1a for a correct engine. @@ -31,7 +31,7 @@ But dev is not a cheap target. Its alloc (`execution/chain/spec/allocs/dev.json` - **testing approach**: TDD — the failing test comes first in every task. - **CRITICAL naming rule** (carried from M0): `package commitment` already declares `cell`, `fold`, `unfold`, `computeCellHash` and more. **Every new package-level identifier MUST carry a `pbin` prefix.** A collision is a compile error, so this applies to every task. -- **The M0 "no external API changes" rule is relaxed, but only for three sanctioned breaks** — Task 7 (option semantics), Task 6 (new persisted toml key), Task 13 (plain-key namespace). Everything else stays additive. If a task appears to need a fourth break, stop and record it with ⚠️ rather than proceeding. +- **The M0 "no external API changes" rule is relaxed, but only for three sanctioned breaks** — Task 7 (option semantics), Task 6 (new persisted toml key), Task 13 (plain-key namespace). Everything else stays additive. If a task appears to need a fourth break, stop and record it with ⚠️ rather than proceeding. **Two of the three were taken**: Task 13 turned out additive (see its checklist), so the plain-key namespace is unchanged. - complete each task fully before the next; small focused changes - **every task MUST include new/updated tests**, listed as separate checklist items - **all tests must pass before starting the next task** @@ -99,7 +99,7 @@ Each hazard needs a named test or a structural assert. These are the plan's real | H4 | **Variant mismatch across processes** — genesis hex + exec pbin, flagless restart, rpcdaemon defaulting to hex, `integration commitment rebuild` overwriting pbin records | 6, 7 | persisted `trie_variant` + refusal on disagreement | | H5 | **Backwards visit from the header-chunk fan-out** — `fold` writes with `prevData = nil` and the record replaces its predecessor outright; re-descending a folded row rewrites it with a `touchMap` that no longer names the previously-touched bit | 12 | ✔ `followAndUpdate` refuses a non-ascending visit (`errPBinVisitOrder`); chunks emitted at stem exit — `TestPBinVisitOrderIsMonotonic`, `TestPBinCodeChunksFollowHeaderSlots` | | H6 | **State-blob depth truncation** — `byte(depth)` truncates ≥256; paths reach 528 bits | 5 | restart round-trip with a >256-bit path | -| H7 | **Code key misread as storage** — a 52-byte length-discriminated code key read as `(addr, slot)` | 13 | tag-discriminated by construction + test that a code key never routes to the storage zone | +| H7 | **Code key misread as storage** — a 52-byte length-discriminated code key read as `(addr, slot)` | 13 | ✔ dissolved: a chunk leaf carries no plain key, so no code key ever enters the plain-key namespace. `TestPBinCodeKeyNeverRoutesToTheStorageZone` + the verifier's zone assert on record-resident leaves | | H8 | **Stale high code chunks after a shortening redeploy** — header chunks overwrite in place and are never removed, so a forward run keeps residue while a rebuild emits only `ceil(code_size/31)`. Two internally-consistent, different roots. **Breaks recompute-from-domains as an oracle** | 12 | ✔ confirmed real, not fixed: `TestPBinShorteningRedeployKeepsStaleChunks` pins both roots and the exact residue. Q2 answered — the tree is history-dependent for code | | H9 | **Unconditional `CodeDomain` read promotes tolerated inconsistency to root divergence** — cleared 7702 residue, `eth_simulateV1` overlays. The existing code documents the residue as benign (`commitment_context.go:1054-1057`); PBT removes that license | 11 | decide and test the residue case explicitly | | H10 | **`ReplacePlainKeys` over pbin records** if references are ever enabled — rewrites bytes at hex cell offsets during background merge. Inert by default, one flag away, no variant check in that path | 6 | refuse the combination | @@ -324,7 +324,7 @@ Production keeps Keccak-256. This task only makes the **test** path run the whol - [x] emit header chunks 0..127 with a stem-exit flush or as their own sorted stream keys, never mid-fan-out — stem-exit flush (`pbinPendingCode`): the account visit queues the chunks, and they are emitted when the next stream key leaves the 33-byte stem, or at the end of the stream. Chunk sub-indices are the highest in a stem, so a stem the stream has left is one no key returns to; queueing over an unflushed stem is a loud error, not a silent overwrite - [x] ➕ **a code chunk's value lives in the branch record** (`pbinFieldLeafValue`, `pbin_branch.go` — Task 13's file, taken early because the forward run needs it). No state domain holds a chunk: chunking is a property of the tree, not of the account, and the reference never rewrites a chunk it has written. An untouched chunk leaf that is the direct sibling of a touched one must therefore reload its own bytes — `TestPBinCodeChunksSurviveAsRecordSiblings`. This dissolves H11 (no by-hash reverse lookup exists) and means a chunk leaf carries no plain key at all; `pbinDecodeCell` now requires a leaf to name exactly one value source of the three - [x] ➕ the code read: `pbinCodeContext` (an optional interface on `PatriciaContext`, additive — not a fourth API break) implemented by `commitmentdb.TrieContext.Code` over `kv.CodeDomain`. A context that cannot serve code refuses a code-bearing account (`TestPBinCodelessContextRefusesCodeBearingAccount`), and the code the chunks come from is cross-checked against the `code_size` the BASIC_DATA leaf hashes (`TestPBinCodeSizeMustMatchTheCodeBehindIt`), since those are two separate domain reads. Read-side tests in `commitmentdb/pbin_code_test.go`, non-vacuous by mutation (a `Code` returning nothing fails all three) -- [x] ➕ code past the account header (>128 chunks, >3968 bytes) is refused with `ErrPBinUnsupported` until Task 13 — `TestPBinCodeBeyondHeaderRefused` and `TestPBinSharedDomainsRefusesCodeBeyondHeader`. The dev deposit contract is 6358 bytes, so **M1b stays blocked on Task 13**, as planned +- [x] ➕ code past the account header (>128 chunks, >3968 bytes) is refused with `ErrPBinUnsupported` until Task 13 — `TestPBinCodeBeyondHeaderRefused` and `TestPBinSharedDomainsRefusesCodeBeyondHeader`. The dev deposit contract is 6358 bytes, so **M1b stays blocked on Task 13**, as planned. (Task 13 lifted the refusal and replaced both tests with their committing counterparts) - [x] ➕ `TestPBinEngineRootCarriesCodeSize` (Task 11) had a code_size with no code behind it, which this task makes an error. It now runs real code and isolates the size claim at the leaf-set level: the same leaf set with BASIC_DATA re-packed at code_size 0 roots differently - [x] run tests — `go test ./execution/commitment/... ./db/state/... -count=1` green, `./execution/stagedsync/... ./execution/state/genesiswrite ./db/integrity -short` green, `go build ./...` clean, `make lint` clean twice @@ -337,13 +337,15 @@ Production keeps Keccak-256. This task only makes the **test** path run the whol - Modify: `execution/commitment/pbin_specengine_test.go` - Create: `execution/commitment/pbin_overflow_test.go` -- [ ] verify the `pbin_branch.go` record field-bit layout before designing the new field; record the layout in this plan -- [ ] write a failing test asserting `full_header_stem` reproduces through the **engine**, and empty the asserted exclusion list in `pbin_specengine_test.go` -- [ ] write a failing test asserting a code key never routes to the storage zone (guards H7) -- [ ] add a tag-discriminated third plain-key shape recognised by `pbinKeyHasher` and `updateCell`; never discriminate by length -- [ ] add a `pbinCellFields` bit carrying the 32-byte chunk value in the branch record, so no reverse lookup is needed (guards H11) -- [ ] extend `pbinDecodeCell` and `loadCellState` for the new shape -- [ ] run tests — all 7 engine vectors must pass before task 14 +- [x] verify the `pbin_branch.go` record field-bit layout before designing the new field; record the layout in this plan — read in Task 12 and re-checked here; recorded in Thin/Unverified and restated: `uint16 touchMap ‖ uint16 afterMap`, then one cell body per bit set in afterMap ascending. A cell body is `byte fields ‖ uvarint prefixBitLen ‖ packed prefix bits ‖ present fields in bit order`, field bits `leaf=1, branch=2, accountAddr=4, storageAddr=8, hash=16, leafValue=32`. Every variable-length field is `uvarint len ‖ bytes` with exactly one legal length per field, so a record has one spelling. **No new field was needed** — overflow chunks reuse `pbinFieldLeafValue`, which Task 12 added +- [x] write a failing test asserting `full_header_stem` reproduces through the **engine**, and empty the asserted exclusion list in `pbin_specengine_test.go` — 7/7. The vector fills a whole stem, so it also covers the sub-indices 2..63 the embedding reserves. Those and the code chunks now share one rule in `pbinLeafValue`: **the sub-index picks a packing only where there is state to pack**; a position with no defined packing can only hold a value that is already 32 bytes, which is the one the record carries (`pbinRecordLeafValue`, renamed from `pbinCodeChunkValue`). The state-derived positions keep their own encodings unchanged +- [x] write a failing test asserting a code key never routes to the storage zone (guards H7) — `TestPBinCodeKeyNeverRoutesToTheStorageZone`: every derived overflow key is zone `0x01` at `CODE_KEY_LENGTH`, and the stream's `pbinKeyHasher` refuses every length that is not a plain key, including the 34-byte code key and its own 64-byte preimage. The structural half is in the verifier: a record-resident leaf must sit in the code zone or at an account sub-index ≥ `CODE_OFFSET`, never in the storage zone — exercised by 5 subtests, checked non-vacuous by mutation +- [x] ⚠️ **scope change: no third plain-key shape exists.** Task 12 settled that a chunk leaf carries no plain key at all — its value lives in the record — so nothing routes a code key through `pbinKeyHasher` or the plain-key arm of `updateCell`, and there is nothing to tag-discriminate. The overflow key is derived inside the engine from `code_hash ‖ tree_index` (`pbinTreeKeyCodeOverflow`), which never meets the plain-key namespace. **The third sanctioned API break was therefore not taken** — Task 13 is additive +- [x] add a `pbinCellFields` bit carrying the 32-byte chunk value in the branch record, so no reverse lookup is needed (guards H11) — landed in Task 12 as `pbinFieldLeafValue`; overflow chunks needed no further field +- [x] extend `pbinDecodeCell` and `loadCellState` for the new shape — also landed in Task 12: `pbinDecodeCell` requires a leaf to name exactly one value source, and `loadCellState` leaves a plain-keyless leaf alone because there is no state read to make +- [x] ➕ emit the code-zone chunks as one sorted block between the account-header keys and the storage-zone ones. Overflow keys are content-addressed, so they follow neither the stream's order nor the account that produced them: they accumulate across the whole account-zone pass and flush at the first key of a higher zone (or at end of stream), sorted and deduped — two accounts running the same bytecode name the same leaves, which is the point of content-addressing. `TestPBinOverflowChunksFollowEveryAccountZoneKey` and `TestPBinOverflowChunksAreSharedByIdenticalCode`; both go red under a dropped sort and under a stem-exit flush +- [x] ➕ `TestPBinCodeOverflowKeyMatchesSpec` diffs the derivation against a transcription of `get_tree_key_for_code_chunk` at the header/zone boundary, both ends of a code stem and the last chunk `MaxCodeSize` produces. The refusals the header-only boundary carried are gone: `TestPBinCodeBeyondHeaderRefused` and `TestPBinSharedDomainsRefusesCodeBeyondHeader` became `TestPBinEngineCommitsOverflowCodeChunks` and `TestPBinSharedDomainsCommitsCodeBeyondHeader` +- [x] run tests — all 7 engine vectors pass; `go test ./execution/commitment/... ./db/state/... -count=1` green, `./execution/stagedsync/... ./execution/state/genesiswrite ./db/integrity -short` green, `go build ./...` clean, `make lint` clean twice ### Task 14: M1b gate — --chain=dev from genesis diff --git a/execution/commitment/commitmentdb/pbin_code_test.go b/execution/commitment/commitmentdb/pbin_code_test.go index 380b7e8e1cf..1faa703bd74 100644 --- a/execution/commitment/commitmentdb/pbin_code_test.go +++ b/execution/commitment/commitmentdb/pbin_code_test.go @@ -84,11 +84,10 @@ func TestPBinSharedDomainsCommitsCodeBearingAccount(t *testing.T) { require.NotEqual(t, withShortCode, withCode) } -// TestPBinSharedDomainsRefusesCodeBeyondHeader pins the M1 boundary at the -// domain layer: chunks past the account header belong in the code zone, and -// until that lands a contract needing them is refused rather than committed -// short. -func TestPBinSharedDomainsRefusesCodeBeyondHeader(t *testing.T) { +// TestPBinSharedDomainsCommitsCodeBeyondHeader is the domain-layer half of the +// code zone: a contract whose code outgrows the account header commits, and the +// chunks past the header are part of what it commits. +func TestPBinSharedDomainsCommitsCodeBeyondHeader(t *testing.T) { t.Parallel() cfg := commitment.DefaultTrieConfig() @@ -99,6 +98,15 @@ func TestPBinSharedDomainsRefusesCodeBeyondHeader(t *testing.T) { sd, tx := pbinCodeSizeSharedDomains(t, []execctx.SharedDomainOption{execctx.WithTrieConfig(cfg)}, addr, pbinCodeSizeAccount(crypto.Keccak256Hash(code)), code) - _, err := sd.ComputeCommitment(t.Context(), tx, false, 0, 0, "pbin-code-overflow", nil) - require.ErrorIs(t, err, commitment.ErrPBinUnsupported) + overflowing, err := sd.ComputeCommitment(t.Context(), tx, false, 0, 0, "pbin-code-overflow", nil) + require.NoError(t, err) + + // Dropping the one byte that spills into the code zone must change the root: + // the overflow chunk is committed, not silently left out. + header := code[:len(code)-1] + headerSd, headerTx := pbinCodeSizeSharedDomains(t, + []execctx.SharedDomainOption{execctx.WithTrieConfig(cfg)}, addr, pbinCodeSizeAccount(crypto.Keccak256Hash(header)), header) + withinHeader, err := headerSd.ComputeCommitment(t.Context(), headerTx, false, 0, 0, "pbin-code-header", nil) + require.NoError(t, err) + require.NotEqual(t, withinHeader, overflowing) } diff --git a/execution/commitment/pbin_branch.go b/execution/commitment/pbin_branch.go index 5325d71a733..4b66401422f 100644 --- a/execution/commitment/pbin_branch.go +++ b/execution/commitment/pbin_branch.go @@ -116,7 +116,7 @@ func pbinAppendCell(dst []byte, c *pbinCell) ([]byte, error) { dst = pbinAppendLenAndVal(dst, c.storageAddr[:c.storageAddrLen]) } if fields&pbinFieldLeafValue != 0 { - value, err := pbinCodeChunkValue(&c.Update) + value, err := pbinRecordLeafValue(&c.Update) if err != nil { return nil, err } diff --git a/execution/commitment/pbin_code.go b/execution/commitment/pbin_code.go index a77c7cc73e8..9438945393e 100644 --- a/execution/commitment/pbin_code.go +++ b/execution/commitment/pbin_code.go @@ -73,12 +73,14 @@ func pbinChunkifyCode(code []byte) [][pbinValueLength]byte { return chunks } -// pbinCodeChunkValue is the chunk value a code-chunk leaf hashes. Unlike a -// storage value it is positional — byte 0 is the PUSHDATA count — so a short -// value cannot be left-padded into place and is an error instead. -func pbinCodeChunkValue(u *Update) ([pbinValueLength]byte, error) { +// pbinRecordLeafValue is the value a leaf carries itself rather than deriving +// from state — a code chunk, or a sub-index the embedding reserves and defines +// no packing for. Unlike a storage value it is not left-padded into place: a +// chunk is positional, byte 0 being the PUSHDATA count, so a short value is an +// error. +func pbinRecordLeafValue(u *Update) ([pbinValueLength]byte, error) { if u.StorageLen != pbinValueLength { - return [pbinValueLength]byte{}, fmt.Errorf("%w: code chunk leaf holds %d value bytes, want %d", + return [pbinValueLength]byte{}, fmt.Errorf("%w: record-resident leaf holds %d value bytes, want %d", errPBinCellHash, u.StorageLen, pbinValueLength) } return u.Storage, nil diff --git a/execution/commitment/pbin_code_test.go b/execution/commitment/pbin_code_test.go index 09ccdcc070c..51f24e04e26 100644 --- a/execution/commitment/pbin_code_test.go +++ b/execution/commitment/pbin_code_test.go @@ -229,23 +229,6 @@ func TestPBinShorteningRedeployKeepsStaleChunks(t *testing.T) { } } -// TestPBinCodeBeyondHeaderRefused pins the M1 boundary: chunks past the header -// belong in the code zone, which overflow support brings, and until then a -// contract that needs them is refused rather than committed short. -func TestPBinCodeBeyondHeaderRefused(t *testing.T) { - t.Parallel() - - addr := pbinOracleAddr(16) - code := pbinTestCode(pbinHeaderCodeChunks*pbinChunkDataLen + 1) - corpus := new(pbinTestCorpus).accountWithCodeBytes(addr, 1, 10, code) - - pph, ms := pbinTestEngine(t) - corpus.applyTo(t, ms) - upd := WrapKeyUpdates(t, ModeDirect, pbinKeyHasher(), corpus.plainKeys, corpus.updates) - _, err := pph.Process(context.Background(), upd, "", nil, WarmupConfig{}) - require.ErrorIs(t, err, ErrPBinUnsupported) -} - // TestPBinCodelessContextRefusesCodeBearingAccount pins that the code read is // not optional: a context that cannot serve code cannot commit an account whose // chunks the tree needs. diff --git a/execution/commitment/pbin_hash.go b/execution/commitment/pbin_hash.go index 6d8893e2dd0..122a98d17f7 100644 --- a/execution/commitment/pbin_hash.go +++ b/execution/commitment/pbin_hash.go @@ -134,14 +134,14 @@ func (h *pbinHasher) leafCellHash(c *pbinCell, path *pbinBitpath) (common.Hash, // pbinLeafValue picks the encoding the key's own position names: the zone byte // separates storage and code from the account header, and within the header the -// sub-index selects between BASIC_DATA, CODE_HASH, a header-resident slot and a -// header-resident code chunk. +// sub-index selects between BASIC_DATA, CODE_HASH and a header-resident slot. +// Every other position holds a value the record already carries whole. func pbinLeafValue(key []byte, u *Update) ([pbinValueLength]byte, error) { switch key[0] { case pbinStorageZone: return pbinEncodeStorageValue(u.Storage[:u.StorageLen]), nil case pbinCodeZone: - return pbinCodeChunkValue(u) + return pbinRecordLeafValue(u) case pbinAccountZone: default: return [pbinValueLength]byte{}, fmt.Errorf("%w: zone %#x names no leaf", errPBinCellHash, key[0]) @@ -153,9 +153,10 @@ func pbinLeafValue(key []byte, u *Update) ([pbinValueLength]byte, error) { return pbinCodeHashValue(u.CodeHash), nil case subIndex >= pbinHeaderStorageOffset && subIndex < pbinCodeOffset: return pbinEncodeStorageValue(u.Storage[:u.StorageLen]), nil - case subIndex >= pbinCodeOffset: - return pbinCodeChunkValue(u) default: - return [pbinValueLength]byte{}, fmt.Errorf("%w: account-zone sub-index %d names no leaf", errPBinCellHash, subIndex) + // Code chunks from CODE_OFFSET on, and the sub-indices below + // HEADER_STORAGE_OFFSET the embedding reserves (eip:255-257): neither is + // packed from state, so the value has to be a full 32 bytes already. + return pbinRecordLeafValue(u) } } diff --git a/execution/commitment/pbin_keys.go b/execution/commitment/pbin_keys.go index 2eded2a65c4..c0b593d5d49 100644 --- a/execution/commitment/pbin_keys.go +++ b/execution/commitment/pbin_keys.go @@ -17,11 +17,13 @@ package commitment import ( + "encoding/binary" "fmt" "sync" keccak "github.com/erigontech/fastkeccak" + "github.com/erigontech/erigon/common" "github.com/erigontech/erigon/common/length" ) @@ -102,14 +104,23 @@ func pbinTreeKeyStorage(addr, slot []byte) []byte { return c.storageKey(addr, slot) } -// pbinTreeKeyCodeChunk returns the key for code chunk chunkID of addr -// (eip:355-367). Chunks the account header holds share the account's own stem; -// higher chunks are content-addressed by code hash in the code zone. +// pbinTreeKeyCodeChunk returns the key for a code chunk the account header holds +// (eip:355-367). Those chunks share the account's own stem; higher ones go +// through pbinTreeKeyCodeOverflow. func pbinTreeKeyCodeChunk(addr []byte, chunkID int) []byte { var c pbinDigestCache return c.codeChunkKey(addr, chunkID) } +// pbinTreeKeyCodeOverflow returns the code-zone key for a chunk past the account +// header (eip:355-367). Those chunks are content-addressed by code hash, so +// accounts running the same bytecode name the same leaves and the key cannot be +// derived from an address at all. +func pbinTreeKeyCodeOverflow(codeHash common.Hash, chunkID int) []byte { + var c pbinDigestCache + return c.codeOverflowKey(codeHash, chunkID) +} + // pbinKeyHasher returns a keyHasher deriving the primary leaf's tree key: // BASIC_DATA for an account, the slot's own leaf for storage. The CODE_HASH // sibling shares the stem and is written by the engine during the same visit, @@ -201,6 +212,23 @@ func (c *pbinDigestCache) codeChunkKey(addr []byte, chunkID int) []byte { return c.accountKey(addr, byte(pbinCodeOffset+chunkID)) } +// codeOverflowKey splits the chunk's overflow index into a tree index and a +// sub-index, hashing code_hash ‖ tree_index into the code-zone stem the chunk +// sits under. The digest is not memoized: one contract spans at most a handful +// of tree indexes, and the cache's entries are bound to an address these keys do +// not have. +func (c *pbinDigestCache) codeOverflowKey(codeHash common.Hash, chunkID int) []byte { + if chunkID < pbinHeaderCodeChunks { + panic(fmt.Sprintf("pbin: code chunk %d is a header chunk, not a code-zone one", chunkID)) + } + overflow := chunkID - pbinHeaderCodeChunks + var preimage [2 * length.Hash]byte + copy(preimage[:], codeHash[:]) + binary.BigEndian.PutUint64(preimage[2*length.Hash-8:], uint64(overflow/pbinStemSubtreeWidth)) + position := c.hash(preimage[:]) + return pbinTreeKey(pbinCodeZone, position[:], byte(overflow%pbinStemSubtreeWidth)) +} + func (c *pbinDigestCache) storageKey(addr, slot []byte) []byte { addr32 := pbinAddr32(addr) slot32 := pbinSlot32(slot) diff --git a/execution/commitment/pbin_overflow_test.go b/execution/commitment/pbin_overflow_test.go new file mode 100644 index 00000000000..4cd9fb4ae6a --- /dev/null +++ b/execution/commitment/pbin_overflow_test.go @@ -0,0 +1,186 @@ +// Copyright 2026 The Erigon Authors +// This file is part of Erigon. +// +// Erigon is free software: you can redistribute it and/or modify +// it under the terms of the GNU Lesser General Public License as published by +// the Free Software Foundation, either version 3 of the License, or +// (at your option) any later version. +// +// Erigon is distributed in the hope that it will be useful, +// but WITHOUT ANY WARRANTY; without even the implied warranty of +// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +// GNU Lesser General Public License for more details. +// +// You should have received a copy of the GNU Lesser General Public License +// along with Erigon. If not, see . + +package commitment + +import ( + "fmt" + "testing" + + "github.com/stretchr/testify/require" + + "github.com/erigontech/erigon/common" + "github.com/erigontech/erigon/common/length" +) + +// pbinTestSpecCodeChunkKey is get_tree_key_for_code_chunk (eip:355-367) +// transcribed from the spec's Python, hashing with the independent Keccak the +// tests use. It is the ground truth the cache-backed derivation is diffed +// against. +func pbinTestSpecCodeChunkKey(t *testing.T, addr []byte, codeHash common.Hash, chunkID int) []byte { + t.Helper() + if chunkID < pbinStemSubtreeWidth-pbinCodeOffset { + stem := pbinTestKeccak(t, pbinTestAddress32(addr)) + return append(append([]byte{pbinAccountZone}, stem...), byte(pbinCodeOffset+chunkID)) + } + overflow := chunkID - (pbinStemSubtreeWidth - pbinCodeOffset) + position := pbinTestKeccak(t, codeHash[:], pbinTestBE32(uint64(overflow/pbinStemSubtreeWidth))) + key := append(append([]byte{pbinCodeZone}, position...), byte(overflow%pbinStemSubtreeWidth)) + require.Len(t, key, pbinCodeKeyLength) + return key +} + +// TestPBinCodeOverflowKeyMatchesSpec pins the second half of the code +// embedding: past the account header a chunk is content-addressed by code hash, +// with the overflow index split into a 32-byte tree index and a sub-index. +func TestPBinCodeOverflowKeyMatchesSpec(t *testing.T) { + t.Parallel() + + addr := pbinOracleAddr(60) + codeHash := common.Hash{0x82, 0x97} + + for _, chunkID := range []int{ + pbinHeaderCodeChunks, // the first overflow chunk + pbinHeaderCodeChunks + 1, // its neighbour on the same code stem + pbinHeaderCodeChunks + pbinStemSubtreeWidth - 1, // the last of the first code stem + pbinHeaderCodeChunks + pbinStemSubtreeWidth, // the first of the second + 792, // the last chunk MaxCodeSize produces + } { + t.Run(fmt.Sprintf("chunk %d", chunkID), func(t *testing.T) { + t.Parallel() + got := pbinTreeKeyCodeOverflow(codeHash, chunkID) + require.Equal(t, pbinTestSpecCodeChunkKey(t, addr, codeHash, chunkID), got) + require.Len(t, got, pbinCodeKeyLength) + require.EqualValues(t, pbinCodeZone, got[0]) + }) + } + + require.Panics(t, func() { pbinTreeKeyCodeOverflow(codeHash, pbinHeaderCodeChunks-1) }, + "a header chunk has no code-zone key") +} + +// TestPBinCodeKeyNeverRoutesToTheStorageZone guards H7. An overflow key is +// derived from code_hash ‖ tree_index, a 64-byte preimage that is not a plain +// key at all: the stream's key hasher only ever sees the two plain-key shapes +// and refuses anything else, so no length can carry a code key into the storage +// zone. +func TestPBinCodeKeyNeverRoutesToTheStorageZone(t *testing.T) { + t.Parallel() + + codeHash := common.Hash{0x11} + for chunkID := pbinHeaderCodeChunks; chunkID < pbinHeaderCodeChunks+600; chunkID += 37 { + key := pbinTreeKeyCodeOverflow(codeHash, chunkID) + require.EqualValues(t, pbinCodeZone, key[0], "chunk %d", chunkID) + require.Len(t, key, pbinCodeKeyLength, "chunk %d", chunkID) + } + + hasher := pbinKeyHasher() + for _, plainKey := range [][]byte{ + make([]byte, pbinCodeKeyLength), // a code key handed back as a plain key + make([]byte, pbinCodeKeyLength-1), // its stem + make([]byte, 2*length.Hash), // the overflow preimage itself + } { + require.Panics(t, func() { hasher(plainKey) }, + "a %d-byte plain key is neither an account nor a storage key", len(plainKey)) + } +} + +// TestPBinEngineCommitsOverflowCodeChunks is the code zone end to end: a +// contract whose code outgrows the account header keeps its first 128 chunks on +// the account stem and puts the rest in the code zone, and the whole leaf set +// has to match the reference tree. +func TestPBinEngineCommitsOverflowCodeChunks(t *testing.T) { + t.Parallel() + + for _, tc := range []struct { + name string + chunks int + }{ + {name: "one chunk past the header", chunks: pbinHeaderCodeChunks + 1}, + {name: "crosses a code stem", chunks: pbinHeaderCodeChunks + pbinStemSubtreeWidth + 1}, + } { + t.Run(tc.name, func(t *testing.T) { + t.Parallel() + + addr := pbinOracleAddr(61) + code := pbinTestCode((tc.chunks-1)*pbinChunkDataLen + 1) + corpus := new(pbinTestCorpus).accountWithCodeBytes(addr, 3, 7, code) + require.Equal(t, 2+tc.chunks, corpus.leafCount(t)) + + pph, ms := pbinTestEngine(t) + corpus.applyTo(t, ms) + root := pbinTestProcess(t, pph, corpus.plainKeys, corpus.updates) + + require.Equal(t, corpus.oracleRoot(t), root) + pbinTestVerifyRecords(t, ms, root, corpus.leafCount(t)) + }) + } +} + +// TestPBinOverflowChunksAreSharedByIdenticalCode pins the point of +// content-addressing (eip:352-354): two accounts running the same bytecode name +// the same code-zone leaves, so the zone holds one copy however many accounts +// reach it. +func TestPBinOverflowChunksAreSharedByIdenticalCode(t *testing.T) { + t.Parallel() + + code := pbinTestCode((pbinHeaderCodeChunks+2)*pbinChunkDataLen - 3) + a, b := pbinOracleAddr(62), pbinOracleAddr(63) + corpus := new(pbinTestCorpus). + accountWithCodeBytes(a, 1, 10, code). + accountWithCodeBytes(b, 2, 20, code) + + chunks := len(pbinChunkifyCode(code)) + overflow := chunks - pbinHeaderCodeChunks + require.Equal(t, 2, overflow) + // Two accounts: four header leaves, two full sets of header chunks, one + // shared set in the code zone. + require.Equal(t, 2*(2+pbinHeaderCodeChunks)+overflow, corpus.leafCount(t)) + + pph, ms := pbinTestEngine(t) + corpus.applyTo(t, ms) + root := pbinTestProcess(t, pph, corpus.plainKeys, corpus.updates) + + require.Equal(t, corpus.oracleRoot(t), root) + pbinTestVerifyRecords(t, ms, root, corpus.leafCount(t)) +} + +// TestPBinOverflowChunksFollowEveryAccountZoneKey pins where the code-zone block +// sits in the visit order: the zone byte puts it after every account-header key +// and before every storage-zone one, so the chunks of an account visited early +// have to wait for the last account of the run. The grid only walks forward, so +// a block emitted at the wrong point fails loudly rather than rewriting a folded +// row. +func TestPBinOverflowChunksFollowEveryAccountZoneKey(t *testing.T) { + t.Parallel() + + code := pbinTestCode((pbinHeaderCodeChunks + 1) * pbinChunkDataLen) + early := pbinOracleAddr(64) + corpus := new(pbinTestCorpus).accountWithCodeBytes(early, 1, 10, code) + for i := uint64(65); i < 70; i++ { + addr := pbinOracleAddr(i) + corpus.account(addr, i, i*2, common.Hash{byte(i)}). + storage(addr, pbinOracleSlot(7), 0x01). + storage(addr, pbinOracleSlot(4096), 0x02) + } + + pph, ms := pbinTestEngine(t) + corpus.applyTo(t, ms) + root := pbinTestProcess(t, pph, corpus.plainKeys, corpus.updates) + + require.Equal(t, corpus.oracleRoot(t), root) + pbinTestVerifyRecords(t, ms, root, corpus.leafCount(t)) +} diff --git a/execution/commitment/pbin_patricia_hashed.go b/execution/commitment/pbin_patricia_hashed.go index bacff827438..5b421f4aaa7 100644 --- a/execution/commitment/pbin_patricia_hashed.go +++ b/execution/commitment/pbin_patricia_hashed.go @@ -20,9 +20,9 @@ // this engine uses Keccak-256 both for node hashing and for tree-key // derivation, behind pbinHasher so the suite can be swapped. // -// Scope: Process over the account and storage zones, ModeDirect only. Code is -// chunked into the account header's own chunk leaves; a contract needing more -// than the header holds is refused until the code zone lands. Parallel mounting +// Scope: Process over all three zones, ModeDirect only. Code is chunked into the +// account header's chunk leaves, overflowing into the code zone where chunks are +// content-addressed by code hash and shared between accounts. Parallel mounting // is out. EIP-8297 has no removal: a zeroed storage slot keeps its leaf at 32 // zero bytes, an account removal is refused rather than guessed at, and code // chunks above a shortened redeploy's length stay in the tree — the tree is a @@ -37,6 +37,7 @@ import ( "fmt" "io" "math/bits" + "slices" "sync" "github.com/erigontech/erigon/common" @@ -57,6 +58,12 @@ type PBinPatriciaHashed struct { siblingKey [pbinAccountKeyLength]byte // scratch for the CODE_HASH key of the account being visited pendingCode pbinPendingCode + // overflowCode holds the run's code-zone chunks. They are content-addressed + // rather than keyed by address, so they neither follow the stream's order nor + // belong to the account that produced them, and are emitted as one sorted + // block once the stream leaves the code zone. + overflowCode []pbinOverflowChunk + keyDigest pbinDigestCache lastKey [pbinStorageKeyLength]byte // the deepest key visited so far, which the next one must exceed lastKeyLen int16 @@ -111,6 +118,7 @@ func (pph *PBinPatriciaHashed) Reset() { pph.rootChecked, pph.rootTouched, pph.rootPresent = false, false, false pph.rootPrev = nil pph.pendingCode = pbinPendingCode{} + pph.overflowCode = pph.overflowCode[:0] pph.lastKeyLen = 0 } @@ -119,6 +127,7 @@ func (pph *PBinPatriciaHashed) Reset() { // other. Production never calls it: the nil default is Keccak-256 on both. func (pph *PBinPatriciaHashed) setHashSuite(sum pbinHashFn) keyHasher { pph.hasher.sum = sum + pph.keyDigest = pbinDigestCache{sum: sum} return pbinKeyHasherWith(sum) } @@ -128,6 +137,7 @@ func (pph *PBinPatriciaHashed) Release() { pph.ctx = nil pph.traceW = nil pph.hasher.sum = nil + pph.keyDigest = pbinDigestCache{} pph.counters = pbinCounters{} pph.branchEncoder.buf = pph.branchEncoder.buf[:0] pbinPool.Put(pph) @@ -182,6 +192,9 @@ func (pph *PBinPatriciaHashed) Process(ctx context.Context, updates *Updates, lo if err = pph.flushPendingCode(); err != nil { return nil, fmt.Errorf("pbin: process %s: %w", logPrefix, err) } + if err = pph.flushOverflowCode(); err != nil { + return nil, fmt.Errorf("pbin: process %s: %w", logPrefix, err) + } for pph.grid.activeRows > 0 { if err = pph.fold(); err != nil { return nil, fmt.Errorf("pbin: final fold: %w", err) @@ -212,6 +225,9 @@ func (pph *PBinPatriciaHashed) processKey(treeKey, plainKey []byte, stateUpdate if err := pph.flushPendingCodeBefore(treeKey); err != nil { return err } + if err := pph.flushOverflowCodeBefore(treeKey); err != nil { + return err + } update := stateUpdate if update == nil { var err error @@ -267,8 +283,13 @@ func (pph *PBinPatriciaHashed) queueCode(basicDataKey, plainKey []byte, update * } chunks := pbinChunkifyCode(code) if len(chunks) > pbinHeaderCodeChunks { - return fmt.Errorf("%w: %x needs %d code chunks, the account header holds %d", - ErrPBinUnsupported, plainKey, len(chunks), pbinHeaderCodeChunks) + for i := pbinHeaderCodeChunks; i < len(chunks); i++ { + var oc pbinOverflowChunk + copy(oc.key[:], pph.keyDigest.codeOverflowKey(update.CodeHash, i)) + oc.value = chunks[i] + pph.overflowCode = append(pph.overflowCode, oc) + } + chunks = chunks[:pbinHeaderCodeChunks] } pph.pendingCode.chunks = chunks copy(pph.pendingCode.stem[:], basicDataKey) @@ -276,6 +297,50 @@ func (pph *PBinPatriciaHashed) queueCode(basicDataKey, plainKey []byte, update * return nil } +// pbinOverflowChunk is one code-zone chunk waiting for its block to be emitted. +type pbinOverflowChunk struct { + key [pbinCodeKeyLength]byte + value [pbinValueLength]byte +} + +// flushOverflowCodeBefore emits the code-zone block once the stream reaches a +// zone above it. Nothing the stream carries is a code-zone key — the zone is +// content-addressed and no plain key derives into it — so the block is written +// whole, between the last account-header key and the first storage one. +func (pph *PBinPatriciaHashed) flushOverflowCodeBefore(treeKey []byte) error { + if len(pph.overflowCode) == 0 || treeKey[0] <= pbinCodeZone { + return nil + } + return pph.flushOverflowCode() +} + +func (pph *PBinPatriciaHashed) flushOverflowCode() error { + if len(pph.overflowCode) == 0 { + return nil + } + slices.SortFunc(pph.overflowCode, func(a, b pbinOverflowChunk) int { return bytes.Compare(a.key[:], b.key[:]) }) + + var prev *pbinOverflowChunk + for i := range pph.overflowCode { + oc := &pph.overflowCode[i] + // Accounts running the same bytecode share leaves (eip:352-354), so the same + // key twice is one chunk two accounts asked for, not a conflict. + if prev != nil && oc.key == prev.key { + if oc.value != prev.value { + return fmt.Errorf("pbin: code chunk %x carries two values", oc.key[:]) + } + continue + } + update := Update{Flags: StorageUpdate, StorageLen: pbinValueLength, Storage: oc.value} + if err := pph.followAndUpdate(oc.key[:], nil, &update); err != nil { + return err + } + prev = oc + } + pph.overflowCode = pph.overflowCode[:0] + return nil +} + func (pph *PBinPatriciaHashed) codeOf(plainKey []byte) ([]byte, error) { ctx, ok := pph.ctx.(pbinCodeContext) if !ok { @@ -422,7 +487,7 @@ func (pph *PBinPatriciaHashed) updateCell(plainKey []byte, probe *pbinBitpath, u case 0: // A code chunk has no plain key: no state domain holds one, so the leaf // carries its own value and the branch record persists it. - if _, err := pbinCodeChunkValue(update); err != nil { + if _, err := pbinRecordLeafValue(update); err != nil { return err } case length.Addr: diff --git a/execution/commitment/pbin_process_test.go b/execution/commitment/pbin_process_test.go index 1d5f2bde6b4..7bed830ab87 100644 --- a/execution/commitment/pbin_process_test.go +++ b/execution/commitment/pbin_process_test.go @@ -88,7 +88,7 @@ func (c *pbinTestCorpus) entries(t *testing.T) []pbinOracleEntry { pbinOracleEntry{key: pbinTreeKeyAccount(plainKey, pbinCodeHashLeafKey), value: code[:]}) for j, chunk := range pbinChunkifyCode(c.codes[string(plainKey)]) { entries = append(entries, pbinOracleEntry{ - key: pbinTreeKeyCodeChunk(plainKey, j), + key: pbinTestChunkKey(plainKey, u.CodeHash, j), value: chunk[:], }) } @@ -105,6 +105,15 @@ func (c *pbinTestCorpus) entries(t *testing.T) []pbinOracleEntry { return entries } +// pbinTestChunkKey is where chunk chunkID of addr's code lives: the account's +// own stem while the header holds it, the content-addressed code zone after. +func pbinTestChunkKey(addr []byte, codeHash common.Hash, chunkID int) []byte { + if chunkID < pbinHeaderCodeChunks { + return pbinTreeKeyCodeChunk(addr, chunkID) + } + return pbinTreeKeyCodeOverflow(codeHash, chunkID) +} + func (c *pbinTestCorpus) oracleRoot(t *testing.T) []byte { t.Helper() root := pbinOracleRoot(c.entries(t)) diff --git a/execution/commitment/pbin_specengine_test.go b/execution/commitment/pbin_specengine_test.go index 6f5547aa9a1..c552bb469d7 100644 --- a/execution/commitment/pbin_specengine_test.go +++ b/execution/commitment/pbin_specengine_test.go @@ -18,9 +18,9 @@ import ( // engine rebuilds a leaf's value from an Update according to where the key sits, // so each value has to be mapped back onto the field the engine will read. // -// Not every leaf can be expressed that way: a code-chunk sub-index has no Update -// field at all. Those vectors are excluded by name below with an asserted count, -// so gaining code support breaks this test rather than silently widening it. +// Every position the embedding defines is expressible, so the exclusion list is +// asserted empty: a vector the mapping cannot express fails this test rather +// than being skipped. type pbinEngineLeaf struct { treeKey []byte @@ -47,11 +47,22 @@ func pbinLeafFromVector(key, value []byte, seq int) (pbinEngineLeaf, bool) { l.update.Flags = StorageUpdate l.update.StorageLen = int8(copy(l.update.Storage[:], value)) } + // A leaf carrying its own 32 bytes has no plain key: a code chunk, or a + // sub-index the embedding has reserved and defined no packing for. + recordLeaf := func() { + l.plainKey = nil + l.update.Flags = StorageUpdate + l.update.StorageLen = int8(copy(l.update.Storage[:], value)) + } if key[0] == pbinStorageZone { storageLeaf() return l, true } + if key[0] == pbinCodeZone { + recordLeaf() + return l, true + } switch sub := key[len(key)-1]; { case sub == pbinBasicDataLeafKey: l.plainKey = account @@ -69,7 +80,8 @@ func pbinLeafFromVector(key, value []byte, seq int) (pbinEngineLeaf, bool) { storageLeaf() return l, true default: - return l, false // code chunk: no Update field carries it + recordLeaf() + return l, true } } @@ -125,9 +137,8 @@ func TestPBinEngineMatchesSpecTrieRoots(t *testing.T) { } t.Logf("engine ran %d/%d reference root vectors: %v", len(ran), len(v.Trie), ran) - t.Logf("excluded (no Update field for a code-chunk leaf): %v", excluded) - require.Equal(t, []string{"full_header_stem"}, excluded, - "exclusions must stay pinned: gaining code support should widen this list, not hide it") + require.Empty(t, excluded, "every reference root vector must reproduce through the engine") + require.Len(t, ran, len(v.Trie)) } // TestPBinReleaseClearsHashSuite pins pooling hygiene: a released engine must diff --git a/execution/commitment/pbin_verify_test.go b/execution/commitment/pbin_verify_test.go index c57cf7b1d74..7cc773ac72e 100644 --- a/execution/commitment/pbin_verify_test.go +++ b/execution/commitment/pbin_verify_test.go @@ -190,7 +190,9 @@ func (v *pbinVerifier) plainState(c *pbinCell) (*Update, error) { case c.storageAddrLen > 0: return v.ms.Storage(c.storageAddr[:c.storageAddrLen]) default: - return nil, errors.New("pbin verify: leaf carries no plain key") + // A code chunk has no plain key and no state behind it: the record is the + // only place its value exists, so the check is that it round-tripped. + return &c.Update, nil } } @@ -289,7 +291,17 @@ func pbinVerifyDerivedKey(c *pbinCell, key []byte) ([]byte, error) { addr, slot := c.storageAddr[:length.Addr], c.storageAddr[length.Addr:c.storageAddrLen] return pbinTreeKeyStorage(addr, slot), nil default: - return nil, errors.New("pbin verify: leaf carries no plain key") + // A record-resident leaf holds no plain key to re-derive from, so what is + // checked is where it may sit: only a code chunk carries its own value, and + // a chunk is either at the top of an account stem or in the code zone — + // never in the storage zone (guards H7). + switch { + case len(key) == pbinCodeKeyLength && key[0] == pbinCodeZone: + case len(key) == pbinAccountKeyLength && key[0] == pbinAccountZone && key[pbinAccountKeyLength-1] >= pbinCodeOffset: + default: + return nil, fmt.Errorf("%w: value-carrying leaf at %x is no code chunk", errPBinVerifyPosition, key) + } + return key, nil } } From a6243065e2f98e8e6fd34cc003dd0505a46df262 Mon Sep 17 00:00:00 2001 From: awskii Date: Fri, 31 Jul 2026 00:10:33 +0700 Subject: [PATCH 37/56] =?UTF-8?q?feat:=20M1b=20gate=20=E2=80=94=20pbin=20d?= =?UTF-8?q?ev=20chain=20from=20genesis?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Boots --chain=dev on the EIP-8297 binary trie, produces blocks, deploys and calls contracts, and resumes across a restart with the header state-root check on. Observed roots and command lines in docs/pbin-m1b-smoke.md. Three defects the gate surfaced: - A fresh bin datadir was refused on its own second resolve. The snapshots stage commits an empty preverified.toml for a chain with no published hashes, which reads as a legacy datadir; with the variant not yet persisted the bin run was refused on the datadir it had just created. A bin datadir now persists its variant at first start whatever the downloader does. - Dev mode computed the EL genesis hash for the beacon Eth1Data before the backend copied the flag into statecfg, so the CL pinned the hex genesis. The flag now reaches statecfg where the CLI is read. - The parallel executor's normalized write set roots differently than the same block executed serially under bin (hex agrees on both executors), so bin stays on the serial executor instead of leaving a wrong-root path reachable. The divergence itself is unresolved. --- cmd/utils/flags.go | 5 + db/state/erigondb_settings.go | 6 +- db/state/pbin_variant_persist_test.go | 39 ++++++- docs/pbin-m1b-smoke.md | 103 ++++++++++++++++++ docs/plans/20260730-pbin-m1-local-el.md | 22 ++-- execution/stagedsync/exec3.go | 10 ++ .../stagedsync/pbin_parallel_exec_test.go | 55 ++++++++++ execution/stagedsync/stage_execute.go | 3 +- 8 files changed, 229 insertions(+), 14 deletions(-) create mode 100644 docs/pbin-m1b-smoke.md create mode 100644 execution/stagedsync/pbin_parallel_exec_test.go diff --git a/cmd/utils/flags.go b/cmd/utils/flags.go index 4fa4d9d8943..37023e123b7 100644 --- a/cmd/utils/flags.go +++ b/cmd/utils/flags.go @@ -54,6 +54,7 @@ import ( "github.com/erigontech/erigon/db/datadir" "github.com/erigontech/erigon/db/downloader/downloadercfg" "github.com/erigontech/erigon/db/snapcfg" + "github.com/erigontech/erigon/db/state/statecfg" "github.com/erigontech/erigon/db/version" "github.com/erigontech/erigon/diagnostics/metrics" "github.com/erigontech/erigon/execution/builder/buildercfg" @@ -2017,6 +2018,10 @@ func SetEthConfig(nodeCtx context.Context, ctx *cli.Command, nodeConfig *nodecfg if ctx.Bool(ExperimentalBinCommitmentFlag.Name) { cfg.ExperimentalBinCommitment = true + // The variant has to be process-wide before any genesis is computed here: + // dev mode derives the beacon Eth1Data from the EL genesis hash while still + // setting up the config, long before the backend applies the flag. + statecfg.ExperimentalBinCommitment = true } cfg.FcuTimeout = ctx.Duration(FcuTimeoutFlag.Name) diff --git a/db/state/erigondb_settings.go b/db/state/erigondb_settings.go index 823fe583a5b..bbb018e4cc9 100644 --- a/db/state/erigondb_settings.go +++ b/db/state/erigondb_settings.go @@ -183,7 +183,11 @@ func ResolveErigonDBSettingsWithRefsDefault(dirs datadir.Dirs, logger log.Logger if err := reconcileTrieVariant(settings, logger); err != nil { return nil, err } - if noDownloader { + // A bin datadir persists its variant right away even with a downloader running: + // no published snapshot set carries a bin erigondb.toml, and leaving the variant + // unpersisted lets the empty preverified.toml that the snapshots stage commits for + // a chain without published hashes read as a legacy datadir at the next resolve. + if noDownloader || trieVariant != nil { // No downloader to provide the real file — write defaults to disk now. logger.Info("Initializing erigondb.toml with DEFAULT settings (nodownloader)", "step_size", settings.StepSize, "steps_in_frozen_file", settings.StepsInFrozenFile, diff --git a/db/state/pbin_variant_persist_test.go b/db/state/pbin_variant_persist_test.go index e2680b151c5..7b523cb0450 100644 --- a/db/state/pbin_variant_persist_test.go +++ b/db/state/pbin_variant_persist_test.go @@ -24,6 +24,7 @@ import ( "github.com/stretchr/testify/require" "github.com/erigontech/erigon/common/log/v3" + "github.com/erigontech/erigon/db/config3" "github.com/erigontech/erigon/db/datadir" "github.com/erigontech/erigon/db/state/execctx" "github.com/erigontech/erigon/db/state/statecfg" @@ -167,7 +168,7 @@ func TestPBinVariantUnknownVariantRefused(t *testing.T) { require.Error(t, err) } -func TestPBinVariantFreshWithDownloaderCarriesBinWithoutWrite(t *testing.T) { +func TestPBinVariantFreshWithDownloaderPersistsBin(t *testing.T) { withVariantFlags(t, true, false, false) dirs := datadir.New(t.TempDir()) @@ -175,11 +176,39 @@ func TestPBinVariantFreshWithDownloaderCarriesBinWithoutWrite(t *testing.T) { require.NoError(t, err) require.Equal(t, TrieVariantBin, settings.TrieVariantName()) - _, err = os.Stat(filepath.Join(dirs.Snap, ERIGONDB_SETTINGS_FILE)) - require.True(t, os.IsNotExist(err), "fresh+downloader must leave erigondb.toml for the downloader") + written, err := readErigonDBSettings(filepath.Join(dirs.Snap, ERIGONDB_SETTINGS_FILE)) + require.NoError(t, err, "a bin datadir must persist its variant at first start, downloader or not") + require.Equal(t, TrieVariantBin, written.TrieVariantName()) + require.Equal(t, uint64(config3.DefaultStepSize), written.StepSize) +} + +// A chain with no published snapshot hashes gets an empty preverified.toml +// committed by the snapshots stage. Without a persisted variant that reads as a +// legacy datadir at the next resolve, and the bin run is refused on its own +// fresh datadir. +func TestPBinVariantSurvivesEmptyPreverifiedFromSnapshotsStage(t *testing.T) { + withVariantFlags(t, true, false, false) + dirs := datadir.New(t.TempDir()) + + _, err := ResolveErigonDBSettings(dirs, log.New(), false) + require.NoError(t, err) + + require.NoError(t, os.WriteFile(filepath.Join(dirs.Snap, datadir.PreverifiedFileName), []byte(""), 0644)) + + settings, err := ResolveErigonDBSettings(dirs, log.New(), false) + require.NoError(t, err) + require.Equal(t, TrieVariantBin, settings.TrieVariantName()) +} + +func TestPBinVariantFreshWithDownloaderRefusesDeliveredHexToml(t *testing.T) { + withVariantFlags(t, true, false, false) + dirs := datadir.New(t.TempDir()) + + _, err := ResolveErigonDBSettings(dirs, log.New(), false) + require.NoError(t, err) - // A later downloader-delivered hex toml must be refused under the bin - // process, not silently adopted. + // A downloader-delivered hex toml overwrites the persisted bin one; the next + // resolve must refuse rather than silently adopt hex. writeToml(t, dirs, "step_size = 100\nsteps_in_frozen_file = 8\n") _, err = ResolveErigonDBSettings(dirs, log.New(), false) require.Error(t, err) diff --git a/docs/pbin-m1b-smoke.md b/docs/pbin-m1b-smoke.md new file mode 100644 index 00000000000..31b4cd63b27 --- /dev/null +++ b/docs/pbin-m1b-smoke.md @@ -0,0 +1,103 @@ +# PBin M1b smoke run — `--chain=dev` on the binary commitment trie + +Record of the M1b gate: a local dev chain booted from genesis on the EIP-8297 binary +trie, produced blocks, deployed and called contracts, and resumed after a restart. + +Binary: `awskii/pbin-patricia`, erigon `v3.7.0-dev`, darwin/arm64. +Hash: Keccak-256 (BLAKE3 is test-only). Roots below agree with no other client. + +## Command line + +```bash +make erigon +./build/bin/erigon \ + --chain=dev \ + --datadir=/tmp/pbin-m1b/gate \ + --experimental.bin-commitment \ + --beacon.api=beacon,validator,node,config \ + --dev.slot-time=2 \ + --http.api=eth,erigon,web3,net,debug,trace,txpool \ + --http.port=8545 --beacon.api.port=5555 --private.api.addr=127.0.0.1:9090 +``` + +The header state-root check is on (its default), so every block below had its executed +root compared against the header the builder produced. + +Restart uses the same line **without** `--experimental.bin-commitment`: the variant is +persisted in `snapshots/erigondb.toml` (`trie_variant = 'bin'`) and re-adopted, logged as +`datadir uses the bin commitment trie; enabling it for this process`. + +## Genesis + +| | root | block hash | +|---|---|---| +| bin | `0xa314dd2e35d820afa60105d356faeae5beb379796fe3bf691a39df6e7bc9a331` | `0xa6d15d434deb7f19f5ac9655b7bf4918c056ecaacc25769bf5f6b3c242a9f538` | +| hex | `0xeed1da9777066ae75039e23f5d0ccc4ae5efae81b9314afcc87af0e714179b4c` | `0x3aa9a433bdbbf19493a237861e62e6c4a66ad676da6d1978dd8039228f64e2c0` | + +The dev beacon takes `Eth1Data` from the EL genesis hash, accepted it, and produced from +slot 1 on. The alloc's deposit contract is 6358 bytes = 206 code chunks (128 header + 78 +CODE_ZONE overflow), so block 0 already exercises Task 13. + +## Blocks, contracts + +Deployed from the dev signer `0x78eF752367584ee389aCB8824Ceec734456402b6` +(key = `sha256("signer:devnet")`). + +- **A** `0x55d8f9693a57f932cde89739f93d4a271d56a156` — init `0x600680600b6000396000f3600035600055`, + runtime stores calldata word 0 into slot 0. +- **B** `0x02dcc6fdd01d75a5bda67e4e7c074cfddc204111` — 4983-byte runtime (151 × `PUSH32`), + 161 chunks, so 33 land in CODE_ZONE overflow at runtime rather than at genesis. + +| block | event | root | +|---|---|---| +| 0 | genesis | `0xa314dd2e35d820afa60105d356faeae5beb379796fe3bf691a39df6e7bc9a331` | +| 15 | deploy A | `0x4cf9eb8a276c1dc5f7debf3d70f50228de7d5b28bf21e8d3b34d88df47426aef` | +| 16 | call A, slot 0 := `0x2a` | `0xa399537a22b085b5df15ffb5fc855870d67225817e0d18c74583db1009b2182a` | +| 17 | deploy B | `0xd513691489314ab5c754b18e3e51db092918725decbc87d475d1151174a3f773` | + +`eth_getStorageAt(A, 0)` = `0x…2a`, `eth_getCode(B)` = 4983 bytes. + +## Restart + +Stopped at head 21 (SIGTERM), restarted flagless on the same datadir. Roots at blocks +0/15/16/17 identical, head preserved, zero `Wrong trie root`. + +A longer run reached head 241 and repeated the restart over a datadir that had already +collated and merged state files (`v2.2-commitment.0-4.kv`, `4-6`, `6-7`): roots at blocks +0/15/16/17/100/200 identical across the restart. Before it, an earlier run resumed and +produced ~90 further blocks (79 → 171) with the root check on. + +Block *production* does not always resume after a restart: Caplin's forward sync stalls +("could not find sync committee for epoch"). Reproduced identically on hex, so it is a +dev-mode CL limitation, not a trie one. The EL side always resumed. + +## Collation and merge + +With `step_size = 64` and `MAX_REORG_DEPTH=8` (defaults never freeze on a chain this +short) the chain built and merged pbin commitment files while running, with no root +mismatch. + +## `integration commitment rebuild` + +Runs to completion under bin: adopts the persisted variant, rebuilds all three shards from +the pbin state files (497 / 256 / 160 keys, blocks 126 / 190 / 222). + +It does **not** confirm the chain's roots here. The per-shard roots the tool prints are +partial — each shard folds only its own key range — and the documented follow-up +(`integration stage_exec --reset`) cannot run on this datadir at all: `readGenesis` has no +`dev` entry and panics with `unknown chain spec with name dev`. Without it, DB remnants +past the rebuilt range make the first post-rebuild block report a wrong root — **on hex +exactly as on bin**, so the check as available is not variant-discriminating. + +The real forward-run-vs-rebuild oracle for pbin is the M1a gate +(`execution/commitment/backtester/pbin_m1a_test.go`), which does that comparison over a +real MDBX datadir with real `.kv` files. + +## Limitations hit during the run + +- **Parallel execution is off under bin.** The parallel executor's normalized write set + produces a different bin root than the same block executed serially (block 0: + `e557bca8…` vs the genesis root `a314dd2e…`; hex agrees on both executors). Rather than + leave a wrong-root path reachable, `executeInParallel` keeps bin on the serial executor. + Unresolved — the divergence itself still needs a root cause. +- Dev-mode CL cannot reliably resume block production after a restart (above). diff --git a/docs/plans/20260730-pbin-m1-local-el.md b/docs/plans/20260730-pbin-m1-local-el.md index 413e7776f5b..7e13b8d8c03 100644 --- a/docs/plans/20260730-pbin-m1-local-el.md +++ b/docs/plans/20260730-pbin-m1-local-el.md @@ -351,13 +351,21 @@ Production keeps Keccak-256. This task only makes the **test** path run the whol **Files:** - Create: `docs/pbin-m1b-smoke.md` - -- [ ] verify genesis block 0 computes a binary root and the dev beacon accepts it -- [ ] run a local `--chain=dev` node to a few blocks, deploying and calling a contract -- [ ] verify a restart resumes at the same root -- [ ] record the observed genesis root and block roots in `docs/pbin-m1b-smoke.md` with the exact command line -- [ ] verify `integration commitment rebuild` on the resulting datadir reproduces the same roots -- [ ] run the package suite — must pass before task 15 +- Modify: `db/state/erigondb_settings.go` (➕ persist the variant at first start even with a downloader) +- Modify: `db/state/pbin_variant_persist_test.go` (➕) +- Modify: `cmd/utils/flags.go` (➕ the variant must reach statecfg before dev computes genesis) +- Modify: `execution/stagedsync/exec3.go`, `execution/stagedsync/stage_execute.go` (➕ `executeInParallel`) +- Create: `execution/stagedsync/pbin_parallel_exec_test.go` (➕) + +- [x] verify genesis block 0 computes a binary root and the dev beacon accepts it — bin genesis root `a314dd2e…` / block hash `a6d15d43…` vs hex `eed1da97…` / `3aa9a433…`; the beacon takes `Eth1Data` from the EL genesis hash and produced from slot 1. Block 0 already carries the deposit contract's 206 code chunks (128 header + 78 overflow), so Task 13 is exercised at genesis +- [x] run a local `--chain=dev` node to a few blocks, deploying and calling a contract — reached head 241; deployed a storage setter (call → slot 0 = `0x2a`) and a 4983-byte contract (161 chunks, 33 in CODE_ZONE overflow at runtime), both verified through RPC. Zero `Wrong trie root` with the header check at its default ON +- [x] verify a restart resumes at the same root — flagless restart re-adopts the persisted `trie_variant = 'bin'`; roots identical across the restart, including over a datadir with collated **and merged** commitment files. ⚠️ block *production* does not always resume: Caplin's forward sync stalls after a restart — reproduced identically on hex, so it is a dev-mode CL limitation, not a trie one +- [x] record the observed genesis root and block roots in `docs/pbin-m1b-smoke.md` with the exact command line +- [x] verify `integration commitment rebuild` on the resulting datadir reproduces the same roots — ⚠️ **it cannot be verified on a dev datadir, on either variant.** The rebuild itself runs to completion under bin (adopts the persisted variant, rebuilds all 3 shards from the pbin state files), but the per-shard roots it prints are partial, and its documented follow-up `integration stage_exec --reset` panics on `--chain=dev` (`readGenesis`: unknown chain spec). Without it, the first post-rebuild block reports a wrong root **on hex exactly as on bin**, so the check is not variant-discriminating. The forward-vs-rebuild oracle for pbin stays the M1a gate +- [x] ➕ **bug found and fixed: a fresh bin datadir was refused on its own second resolve.** The snapshots stage commits an empty `preverified.toml` for a chain with no published hashes, which `ResolveErigonDBSettings` reads as a legacy datadir; with the variant not yet persisted (first start + downloader deferred the write) the bin run was refused on the datadir it had just created. A bin datadir now persists its variant at first start whatever the downloader does — nothing publishes a bin `erigondb.toml` for it to pre-empt. `TestPBinVariantFreshWithDownloaderPersistsBin`, `TestPBinVariantSurvivesEmptyPreverifiedFromSnapshotsStage`; `…RefusesDeliveredHexToml` keeps the delivered-hex refusal +- [x] ➕ **bug found and fixed: the dev beacon pinned the hex genesis.** Dev mode computes the EL genesis hash for `Eth1Data` while still assembling the config, long before the backend copies the flag into `statecfg`, so the CL asked for a genesis hash the EL never wrote. The flag now reaches `statecfg` where the CLI is read +- [x] ➕ ⚠️ **parallel execution gated off under bin.** The parallel executor's normalized write set roots differently than the same block executed serially — block 0 gave `e557bca8…` against the genesis root `a314dd2e…`, while hex agrees on both executors, so the difference is something only the bin trie hashes (code chunks / `code_size` are the candidates; not root-caused). `executeInParallel` keeps bin on the serial executor rather than leaving a wrong-root path reachable, matching Task 8's refuse-don't-degrade rule. `TestPBinExecuteInParallelExcludesBin`. **Open for M2** +- [x] run the package suite — must pass before task 15 ### Task 15: Verify acceptance criteria diff --git a/execution/stagedsync/exec3.go b/execution/stagedsync/exec3.go index 83c1591a6fc..3a511981058 100644 --- a/execution/stagedsync/exec3.go +++ b/execution/stagedsync/exec3.go @@ -113,6 +113,16 @@ func restoreTxNum(ctx context.Context, cfg *ExecuteBlockCfg, applyTx kv.Tx, curr // Deferring cuts re-org validation overhead; the parallel apply path also needs // Flush() to carry the pending update across sync cycles. The bin trie has no // deferred-update path and refuses the request, so it stays on the inline path. +// executeInParallel picks the executor. The parallel executor's normalized write +// set produces a different bin-trie root than the serial one for the same block, +// so the bin variant stays on the serial executor until that is resolved. +func executeInParallel(variant commitment.TrieVariant, exec3Parallel, experimentalBAL bool) bool { + if variant == commitment.VariantBinPatriciaTrie { + return false + } + return exec3Parallel || experimentalBAL +} + func deferCommitmentUpdates(variant commitment.TrieVariant, isForkValidation, parallel, isApplyingBlocks bool) bool { if variant == commitment.VariantBinPatriciaTrie { return false diff --git a/execution/stagedsync/pbin_parallel_exec_test.go b/execution/stagedsync/pbin_parallel_exec_test.go new file mode 100644 index 00000000000..f027e43214b --- /dev/null +++ b/execution/stagedsync/pbin_parallel_exec_test.go @@ -0,0 +1,55 @@ +// Copyright 2026 The Erigon Authors +// This file is part of Erigon. +// +// Erigon is free software: you can redistribute it and/or modify +// it under the terms of the GNU Lesser General Public License as published by +// the Free Software Foundation, either version 3 of the License, or +// (at your option) any later version. +// +// Erigon is distributed in the hope that it will be useful, +// but WITHOUT ANY WARRANTY; without even the implied warranty of +// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +// GNU Lesser General Public License for more details. +// +// You should have received a copy of the GNU Lesser General Public License +// along with Erigon. If not, see . + +package stagedsync + +import ( + "testing" + + "github.com/stretchr/testify/require" + + "github.com/erigontech/erigon/execution/commitment" +) + +// TestPBinExecuteInParallelExcludesBin pins the executor choice. The parallel +// executor's normalized write set roots differently under the bin trie than the +// state the same block produces serially, so bin runs the serial executor +// whatever the parallel toggles say; every other variant is left alone. +func TestPBinExecuteInParallelExcludesBin(t *testing.T) { + t.Parallel() + + for _, tc := range []struct { + name string + variant commitment.TrieVariant + exec3Parallel bool + experimentalBAL bool + want bool + }{ + {name: "hex parallel", variant: commitment.VariantHexPatriciaTrie, exec3Parallel: true, want: true}, + {name: "hex bal", variant: commitment.VariantHexPatriciaTrie, experimentalBAL: true, want: true}, + {name: "hex serial", variant: commitment.VariantHexPatriciaTrie}, + {name: "parallel trie parallel", variant: commitment.VariantParallelHexPatricia, exec3Parallel: true, want: true}, + {name: "bin parallel", variant: commitment.VariantBinPatriciaTrie, exec3Parallel: true}, + {name: "bin bal", variant: commitment.VariantBinPatriciaTrie, experimentalBAL: true}, + {name: "bin serial", variant: commitment.VariantBinPatriciaTrie}, + } { + t.Run(tc.name, func(t *testing.T) { + t.Parallel() + got := executeInParallel(tc.variant, tc.exec3Parallel, tc.experimentalBAL) + require.Equal(t, tc.want, got) + }) + } +} diff --git a/execution/stagedsync/stage_execute.go b/execution/stagedsync/stage_execute.go index 8f22a1fb2a6..39dc81a9c16 100644 --- a/execution/stagedsync/stage_execute.go +++ b/execution/stagedsync/stage_execute.go @@ -379,7 +379,8 @@ func SpawnExecuteBlocksStage(s *StageState, u Unwinder, doms *execctx.SharedDoma return nil } - if err := ExecV3(ctx, s, u, cfg, doms, rwTx, dbg.Exec3Parallel || cfg.experimentalBAL, to, logger); err != nil { + parallel := executeInParallel(doms.GetCommitmentCtx().Trie().Variant(), dbg.Exec3Parallel, cfg.experimentalBAL) + if err := ExecV3(ctx, s, u, cfg, doms, rwTx, parallel, to, logger); err != nil { return err } return nil From 77dc829925d0d5066aa496b67d9405851a728f18 Mon Sep 17 00:00:00 2001 From: awskii Date: Fri, 31 Jul 2026 00:17:45 +0700 Subject: [PATCH 38/56] =?UTF-8?q?feat:=20M1=20acceptance=20audit=20?= =?UTF-8?q?=E2=80=94=20hazards,=20API=20breaks,=20pbin=20naming=20rule?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Verify Task 15's acceptance criteria for the pbin M1 milestone. All 13 hazards (H1-H13) have a named passing test or a structural assert; the mapping is recorded in the plan. Q2/Q4/Q5 are answered in place, Q1 and Q3 deferred with a reason — Q3 gained the missing marker and a statement of what it does not block. Exactly one exported declaration changed across the whole milestone (WithSequentialCommitment -> WithoutParallelCommitment), so no fourth API break was taken. An AST diff of package commitment at the plan's base against HEAD found 11 new package-level test helpers without the pbin prefix the naming rule requires; renamed. Two generic helpers in db/state prefixed for the same reason. --- db/state/pbin_variant_persist_test.go | 42 +++++++++---------- docs/plans/20260730-pbin-m1-local-el.md | 34 +++++++++++---- execution/commitment/pbin_code_test.go | 6 +-- execution/commitment/pbin_codesize_test.go | 4 +- execution/commitment/pbin_specengine_test.go | 2 +- execution/commitment/pbin_specroots_test.go | 12 +++--- execution/commitment/pbin_specvectors_test.go | 18 ++++---- .../commitment/pbin_vs_hex_compare_test.go | 40 +++++++++--------- 8 files changed, 88 insertions(+), 70 deletions(-) diff --git a/db/state/pbin_variant_persist_test.go b/db/state/pbin_variant_persist_test.go index 7b523cb0450..62b20b3602a 100644 --- a/db/state/pbin_variant_persist_test.go +++ b/db/state/pbin_variant_persist_test.go @@ -33,7 +33,7 @@ import ( // The tests below mutate process-wide statecfg flags, so none of them may run // in parallel; save/restore keeps the rest of the package unaffected. -func withVariantFlags(t *testing.T, bin, streaming, parallel bool) { +func pbinWithVariantFlags(t *testing.T, bin, streaming, parallel bool) { t.Helper() origBin := statecfg.ExperimentalBinCommitment origStream := statecfg.ExperimentalStreamingCommitment @@ -48,7 +48,7 @@ func withVariantFlags(t *testing.T, bin, streaming, parallel bool) { statecfg.ExperimentalParallelCommitment = parallel } -func writeToml(t *testing.T, dirs datadir.Dirs, content string) string { +func pbinWriteToml(t *testing.T, dirs datadir.Dirs, content string) string { t.Helper() path := filepath.Join(dirs.Snap, ERIGONDB_SETTINGS_FILE) require.NoError(t, os.WriteFile(path, []byte(content), 0644)) @@ -56,7 +56,7 @@ func writeToml(t *testing.T, dirs datadir.Dirs, content string) string { } func TestPBinVariantFirstStartPersistsBin(t *testing.T) { - withVariantFlags(t, true, false, false) + pbinWithVariantFlags(t, true, false, false) dirs := datadir.New(t.TempDir()) settings, err := ResolveErigonDBSettings(dirs, log.New(), true) @@ -70,7 +70,7 @@ func TestPBinVariantFirstStartPersistsBin(t *testing.T) { } func TestPBinVariantHexFirstStartWritesNoVariantKey(t *testing.T) { - withVariantFlags(t, false, false, false) + pbinWithVariantFlags(t, false, false, false) dirs := datadir.New(t.TempDir()) settings, err := ResolveErigonDBSettings(dirs, log.New(), true) @@ -83,7 +83,7 @@ func TestPBinVariantHexFirstStartWritesNoVariantKey(t *testing.T) { } func TestPBinVariantFlaglessRestartStaysBin(t *testing.T) { - withVariantFlags(t, true, false, false) + pbinWithVariantFlags(t, true, false, false) dirs := datadir.New(t.TempDir()) _, err := ResolveErigonDBSettings(dirs, log.New(), true) require.NoError(t, err) @@ -99,7 +99,7 @@ func TestPBinVariantFlaglessRestartStaysBin(t *testing.T) { } func TestPBinVariantHexDatadirRefusesBinFlag(t *testing.T) { - withVariantFlags(t, true, false, false) + pbinWithVariantFlags(t, true, false, false) for name, content := range map[string]string{ "absent_field": "step_size = 100\nsteps_in_frozen_file = 8\n", @@ -107,7 +107,7 @@ func TestPBinVariantHexDatadirRefusesBinFlag(t *testing.T) { } { t.Run(name, func(t *testing.T) { dirs := datadir.New(t.TempDir()) - writeToml(t, dirs, content) + pbinWriteToml(t, dirs, content) _, err := ResolveErigonDBSettings(dirs, log.New(), false) require.Error(t, err) }) @@ -118,16 +118,16 @@ func TestPBinVariantBinDatadirRefusesStreamingAndParallel(t *testing.T) { const binToml = "step_size = 100\nsteps_in_frozen_file = 8\ntrie_variant = \"bin\"\n" t.Run("streaming", func(t *testing.T) { - withVariantFlags(t, false, true, false) + pbinWithVariantFlags(t, false, true, false) dirs := datadir.New(t.TempDir()) - writeToml(t, dirs, binToml) + pbinWriteToml(t, dirs, binToml) _, err := ResolveErigonDBSettings(dirs, log.New(), false) require.Error(t, err) }) t.Run("parallel", func(t *testing.T) { - withVariantFlags(t, false, false, true) + pbinWithVariantFlags(t, false, false, true) dirs := datadir.New(t.TempDir()) - writeToml(t, dirs, binToml) + pbinWriteToml(t, dirs, binToml) _, err := ResolveErigonDBSettings(dirs, log.New(), false) require.Error(t, err) }) @@ -135,14 +135,14 @@ func TestPBinVariantBinDatadirRefusesStreamingAndParallel(t *testing.T) { func TestPBinVariantRefusesReferences(t *testing.T) { t.Run("persisted", func(t *testing.T) { - withVariantFlags(t, false, false, false) + pbinWithVariantFlags(t, false, false, false) dirs := datadir.New(t.TempDir()) - writeToml(t, dirs, "step_size = 100\nsteps_in_frozen_file = 8\nreferences_in_commitment_branches = true\ntrie_variant = \"bin\"\n") + pbinWriteToml(t, dirs, "step_size = 100\nsteps_in_frozen_file = 8\nreferences_in_commitment_branches = true\ntrie_variant = \"bin\"\n") _, err := ResolveErigonDBSettings(dirs, log.New(), false) require.Error(t, err) }) t.Run("first_start", func(t *testing.T) { - withVariantFlags(t, true, false, false) + pbinWithVariantFlags(t, true, false, false) dirs := datadir.New(t.TempDir()) refs := true _, err := ResolveErigonDBSettingsWithRefsDefault(dirs, log.New(), true, &refs) @@ -151,7 +151,7 @@ func TestPBinVariantRefusesReferences(t *testing.T) { } func TestPBinVariantLegacyDatadirRefusesBin(t *testing.T) { - withVariantFlags(t, true, false, false) + pbinWithVariantFlags(t, true, false, false) dirs := datadir.New(t.TempDir()) require.NoError(t, os.WriteFile(filepath.Join(dirs.Snap, datadir.PreverifiedFileName), []byte(""), 0644)) @@ -160,16 +160,16 @@ func TestPBinVariantLegacyDatadirRefusesBin(t *testing.T) { } func TestPBinVariantUnknownVariantRefused(t *testing.T) { - withVariantFlags(t, false, false, false) + pbinWithVariantFlags(t, false, false, false) dirs := datadir.New(t.TempDir()) - writeToml(t, dirs, "step_size = 100\nsteps_in_frozen_file = 8\ntrie_variant = \"verkle\"\n") + pbinWriteToml(t, dirs, "step_size = 100\nsteps_in_frozen_file = 8\ntrie_variant = \"verkle\"\n") _, err := ResolveErigonDBSettings(dirs, log.New(), false) require.Error(t, err) } func TestPBinVariantFreshWithDownloaderPersistsBin(t *testing.T) { - withVariantFlags(t, true, false, false) + pbinWithVariantFlags(t, true, false, false) dirs := datadir.New(t.TempDir()) settings, err := ResolveErigonDBSettings(dirs, log.New(), false) @@ -187,7 +187,7 @@ func TestPBinVariantFreshWithDownloaderPersistsBin(t *testing.T) { // legacy datadir at the next resolve, and the bin run is refused on its own // fresh datadir. func TestPBinVariantSurvivesEmptyPreverifiedFromSnapshotsStage(t *testing.T) { - withVariantFlags(t, true, false, false) + pbinWithVariantFlags(t, true, false, false) dirs := datadir.New(t.TempDir()) _, err := ResolveErigonDBSettings(dirs, log.New(), false) @@ -201,7 +201,7 @@ func TestPBinVariantSurvivesEmptyPreverifiedFromSnapshotsStage(t *testing.T) { } func TestPBinVariantFreshWithDownloaderRefusesDeliveredHexToml(t *testing.T) { - withVariantFlags(t, true, false, false) + pbinWithVariantFlags(t, true, false, false) dirs := datadir.New(t.TempDir()) _, err := ResolveErigonDBSettings(dirs, log.New(), false) @@ -209,7 +209,7 @@ func TestPBinVariantFreshWithDownloaderRefusesDeliveredHexToml(t *testing.T) { // A downloader-delivered hex toml overwrites the persisted bin one; the next // resolve must refuse rather than silently adopt hex. - writeToml(t, dirs, "step_size = 100\nsteps_in_frozen_file = 8\n") + pbinWriteToml(t, dirs, "step_size = 100\nsteps_in_frozen_file = 8\n") _, err = ResolveErigonDBSettings(dirs, log.New(), false) require.Error(t, err) } diff --git a/docs/plans/20260730-pbin-m1-local-el.md b/docs/plans/20260730-pbin-m1-local-el.md index 7e13b8d8c03..04d3e9af526 100644 --- a/docs/plans/20260730-pbin-m1-local-el.md +++ b/docs/plans/20260730-pbin-m1-local-el.md @@ -113,7 +113,7 @@ Blocking items needing a human or upstream answer. Do not proceed past the task - **Q1 (highest value) — what does the reference produce for a removed account?** EIP-161 empty-removal and EIP-6780 destruct both hand pbin a `DeleteUpdate` on a committed leaf. Task 9 would turn that into BASIC_DATA `{version 0, code_size 0, nonce 0, balance 0}` + CODE_HASH `keccak(empty)`. Consistent with eip:345-347 but **not verified against the reference**, and `testdata/eip8297_vectors.json` does not cover it. It silently changes the root. ⚠️ **Deferred at Task 9, still unanswered.** Account removal keeps erroring at both sites (`updateCell`, the `loadCellState` account arm); only storage was reinterpreted. Note the `zero_value_present` vector *is* an account-zone BASIC_DATA leaf of 32 zero bytes, so the reference at least admits that leaf shape — it does not say a removal produces it. Unblocks nothing in M1: a dev chain reaches neither removal path. - **Q2 — is the tree a pure function of current state, or of history?** (H8.) eip-8297 is silent. Determines whether recompute-from-domains is a valid oracle at all, hence whether the M1a gate means anything. Possibly an upstream spec question. **Answered (Task 12): of history, for code chunks only.** Accounts and storage are a pure function of current state — every leaf's value comes from a domain read. Code chunks are not: eip:439-443 says EVM execution never removes entries, so a redeploy to shorter code leaves the chunks above the new length in the tree holding the old code's bytes, and nothing in the current state records that they exist. A forward run commits them; a recompute from the domains emits only `ceil(code_size/31)`. Both roots are internally consistent and different — `TestPBinShorteningRedeployKeepsStaleChunks` pins both, including the exact residue. Consequences: **recompute-from-domains is not an oracle for a code-bearing account** (the M1a gate stays valid only because its datadir is code-free), and `integration commitment rebuild` over a chain that has seen a shortening redeploy will not reproduce the chain's roots. Reachable in practice by an EIP-7702 delegation clear and by a metamorphic CREATE2 redeploy; not by the M1b dev chain. Zeroing the tail instead would need the previous code length, which no read on the commitment path has. Still worth raising upstream — the same divergence exists for any client that rebuilds state from a snapshot. -- **Q3 — does the reference create a present-zero leaf on `SSTORE 0` to a virgin slot?** Erigon drops it at `execution/state/state_object.go:245-246` before any writer sees it. If yes, that guard must be variant-gated and every zero-SSTORE becomes a state write. Still open after Task 9, which deliberately kept the virgin case a no-op: a delete only zeroes a cell that already holds a leaf. +- **Q3 — does the reference create a present-zero leaf on `SSTORE 0` to a virgin slot?** Erigon drops it at `execution/state/state_object.go:245-246` before any writer sees it. If yes, that guard must be variant-gated and every zero-SSTORE becomes a state write. ⚠️ **Deferred at Task 9, still unanswered.** Task 9 deliberately kept the virgin case a no-op: a delete only zeroes a cell that already holds a leaf. Unblocks nothing in M1 — the drop happens above the commitment layer, so pbin never sees the write on either answer, and M1 compares roots only against itself (M1a) and against the header the same node produced (M1b), never against the reference. It becomes blocking the moment a pbin chain is compared with another client's. - **Q4 — confirm `github.com/zeebo/blake3`** as a **test-only** dependency (lower stakes now that production stays Keccak; the in-graph `lukechampine.com/blake3` may simply be enough). Its measured advantage was 13–18% arm64 / 58–78% amd64, not re-verified. **Answered (Task 1):** `zeebo` not added — production stays Keccak so BLAKE3 speed is irrelevant, and `lukechampine.com/blake3` is already a direct dependency used by `pbin_specroots_test.go`; it now backs `pbinBlake3Hash` for all three seam tests. - **Q5 — forward/backward compatibility of a new `erigondb.toml` key** across erigon binaries, given the file is downloader-delivered and **wins over the CLI** (`erigondb_settings.go:74-89`). **Answered (Task 6):** `readErigonDBSettings` uses `go-toml/v2` `Unmarshal`, which ignores unknown keys — older binaries parse a `trie_variant` toml fine. The key is written only when bin, so published/downloader tomls stay byte-identical, and a downloader-delivered hex toml under a bin process is refused at resolve. Residual risk: a binary **predating the key** opens a bin datadir as hex with no guard — inherent to any new key; acceptable while bin is experimental and fresh-datadir-only. @@ -369,17 +369,35 @@ Production keeps Keccak-256. This task only makes the **test** path run the whol ### Task 15: Verify acceptance criteria -- [ ] verify every hazard H1–H12 has a named passing test or a structural assert -- [ ] verify all five open questions are answered and recorded, or explicitly deferred with ⚠️ and a reason -- [ ] verify only the three sanctioned API breaks were taken; `git diff --stat` shows no fourth -- [ ] verify every new package-level identifier carries the `pbin` prefix -- [ ] run `go test ./execution/commitment/... ./db/state/... -count=1` -- [ ] run `go build ./...` and `make lint` until clean -- [ ] verify the three `pbin_spec*_test.go` oracles pass under BLAKE3 with 7/7 engine vectors +- [x] verify every hazard H1–H12 has a named passing test or a structural assert — all thirteen (H13 included) are guarded and every named test passes: + + | ID | Guard | Where | + |----|-------|-------| + | H1 | `TestPBinCtorRefusesSharedBranchCache`, `TestPBinSharedDomainsHasNoSharedBranchCache`, `TestPBinBranchCacheTrunkSlotCollision` | `commitmentdb` — the first is the structural ctor assert, the last pins the collision itself | + | H2 | `TestPBinRootRecordRealTableIteration`, `TestPBinLoadRootNoRecordVersusStoredTree` | `commitment` | + | H3 | `TestPBinSpecKeyRouting` (full 32-byte keys under BLAKE3), `TestPBinReleaseClearsHashSuite` | `commitment` | + | H4 | the 11 `TestPBinVariant*` | `db/state` | + | H5 | `TestPBinVisitOrderIsMonotonic`, `TestPBinCodeChunksFollowHeaderSlots` over the `errPBinVisitOrder` assert in `followAndUpdate` | `commitment` | + | H6 | `TestPBinRestartRoundTripDeepPath` (527-bit prefix) | `commitment` | + | H7 | `TestPBinCodeKeyNeverRoutesToTheStorageZone` | `commitment` | + | H8 | `TestPBinShorteningRedeployKeepsStaleChunks` — confirms the divergence, does not fix it (Q2) | `commitment` | + | H9 | `TestPBinTrieContextIgnoresClearedDelegationResidue`, `TestPBinTrieContextRefusesCodeBearingAccountWithoutCode` | `commitmentdb` | + | H10 | `TestPBinVariantRefusesReferences` | `db/state` | + | H11 | `TestPBinCodeChunksSurviveAsRecordSiblings` | `commitment` | + | H12 | `TestPBinFoldDeleteUnreachableFromProcess` | `commitment` | + | H13 | `TestHeaderRootCheckDefaultOnAndTogglable`; all five comparisons go through `headerRootMismatch` (`exec3.go:835`, `exec3_serial.go:202`, `committer.go:554/:656/:763`) and `backend.go:334` warns when the check is off | `execution/stagedsync` | + +- [x] verify all five open questions are answered and recorded, or explicitly deferred with ⚠️ and a reason — Q2, Q4, Q5 answered in place; Q1 and Q3 deferred with ⚠️ and a stated reason each. ➕ Q3 carried a reason but no ⚠️ marker and no statement of what it does not block; both added here +- [x] verify only the three sanctioned API breaks were taken; `git diff --stat` shows no fourth — over every non-test file in `1e078ffb04..HEAD`, exactly one exported declaration is removed or changed: `WithSequentialCommitment` → `WithoutParallelCommitment` (Task 7). Task 6's `trie_variant` is a new toml key, additive to any reader and refused rather than degraded on disagreement. Task 13 turned out additive, so its namespace break was never taken. `Update.Encode/Decode`'s wire change stays inside the debug-trace path the plan's own analysis bounds it to, and the two bin-variant panics (`SetDeferCommitmentUpdates`, `SetCollapseTracer`) change no signature and are unreachable under hex +- [x] verify every new package-level identifier carries the `pbin` prefix — checked by AST, not by grep: package-level declarations of `package commitment` at `1e078ffb04` diffed against HEAD give 394 new identifiers. 11 were unprefixed test helpers (`mustHex`, `runHex`, `blake3Sum`, the `pbin_vs_hex_compare_test.go` corpus builders and `engineShape`, both spec-vector loaders) — all generic enough to collide with a future test file in the same package, all renamed here. Five stay unprefixed deliberately: `NewPBinPatriciaHashed` (Go constructor form, still carries `PBin`), `StatefulTrie` (Task 5 promoted it as a variant-neutral interface that hex and parallel also implement), `VariantBinPatriciaTrie` (member of the `Variant*` enum family), and the two `Test…` names that follow the production symbol they exercise (`TestInitializeTrieAndUpdates_BinVariant`, `TestParseTrieVariantBin`). The rule's rationale is collision inside `package commitment`, so identifiers added to `db/state`, `execctx`, `stagedsync`, `genesiswrite`, `db/integrity` and `statecfg` follow their own packages' conventions instead (`WithHexCommitmentOnly` beside `WithoutSharedBranchCache`, `ExperimentalBinCommitment` beside `ExperimentalParallelCommitment`); the two generic test helpers among them (`writeToml`, `withVariantFlags` in `db/state`) were prefixed anyway +- [x] run `go test ./execution/commitment/... ./db/state/... -count=1` — green +- [x] run `go build ./...` and `make lint` until clean +- [x] verify the three `pbin_spec*_test.go` oracles pass under BLAKE3 with 7/7 engine vectors — `pbin_specroots_test.go:60` hard-asserts the vectors' `hasher` is `blake3` before replaying them, and `pbin_specengine_test.go:140` asserts the exclusion list is empty. All 7 engine vectors run and pass: `empty`, `single_account_leaf`, `one_header_stem_two_leaves`, `two_accounts`, `cross_zone_small`, `zero_value_present`, `full_header_stem` ### Task 16: [Final] Update documentation - [ ] update the package doc comment on `pbin_patricia_hashed.go` to state BLAKE3, the M1 scope, and the stated limitations (no witness, no getProof, no parallel) +- [ ] ➕ fix the stale comment on `VariantBinPatriciaTrie` (`commitment.go:154-155`), which still says the variant is not wired to the domain layer and has no state save/restore — both untrue since Task 5 (found in the Task 15 audit) - [ ] update `CLAUDE.md` if new patterns were discovered - [ ] move this plan to `docs/plans/completed/` diff --git a/execution/commitment/pbin_code_test.go b/execution/commitment/pbin_code_test.go index 51f24e04e26..8f006fc7793 100644 --- a/execution/commitment/pbin_code_test.go +++ b/execution/commitment/pbin_code_test.go @@ -30,16 +30,16 @@ import ( // layout. func TestPBinChunkifyCodeVectors(t *testing.T) { t.Parallel() - v := loadPBinSpecVectors(t) + v := pbinLoadSpecVectors(t) require.NotEmpty(t, v.Chunkify) for _, tc := range v.Chunkify { t.Run(tc.Name, func(t *testing.T) { t.Parallel() - got := pbinChunkifyCode(mustHex(t, tc.Code)) + got := pbinChunkifyCode(pbinMustHex(t, tc.Code)) require.Len(t, got, len(tc.Chunks)) for i, want := range tc.Chunks { - require.Equal(t, mustHex(t, want), got[i][:], "chunk %d", i) + require.Equal(t, pbinMustHex(t, want), got[i][:], "chunk %d", i) } }) } diff --git a/execution/commitment/pbin_codesize_test.go b/execution/commitment/pbin_codesize_test.go index db1ec78b472..85376af550c 100644 --- a/execution/commitment/pbin_codesize_test.go +++ b/execution/commitment/pbin_codesize_test.go @@ -32,7 +32,7 @@ import ( // code size has to reach the leaf value, not be forced to zero. func TestPBinBasicDataLeafCarriesCodeSize(t *testing.T) { t.Parallel() - v := loadPBinSpecVectors(t) + v := pbinLoadSpecVectors(t) require.NotEmpty(t, v.BasicData) addr := pbinOracleAddr(1) @@ -45,7 +45,7 @@ func TestPBinBasicDataLeafCarriesCodeSize(t *testing.T) { u := Update{Flags: NonceUpdate | BalanceUpdate, Nonce: tc.Nonce, Balance: *bal, CodeSize: tc.CodeSize} got, err := pbinLeafValue(key, &u) require.NoError(t, err) - require.Equal(t, mustHex(t, tc.Value), got[:], + require.Equal(t, pbinMustHex(t, tc.Value), got[:], "BASIC_DATA leaf mismatch for code_size=%d nonce=%d balance=%s", tc.CodeSize, tc.Nonce, tc.Balance) } } diff --git a/execution/commitment/pbin_specengine_test.go b/execution/commitment/pbin_specengine_test.go index c552bb469d7..4809c1fd60d 100644 --- a/execution/commitment/pbin_specengine_test.go +++ b/execution/commitment/pbin_specengine_test.go @@ -87,7 +87,7 @@ func pbinLeafFromVector(key, value []byte, seq int) (pbinEngineLeaf, bool) { func TestPBinEngineMatchesSpecTrieRoots(t *testing.T) { t.Parallel() - v := loadPBinRootVectors(t) + v := pbinLoadRootVectors(t) var ran, excluded []string for _, tc := range v.Trie { diff --git a/execution/commitment/pbin_specroots_test.go b/execution/commitment/pbin_specroots_test.go index b71cf10c7c9..81eb33f071a 100644 --- a/execution/commitment/pbin_specroots_test.go +++ b/execution/commitment/pbin_specroots_test.go @@ -46,12 +46,12 @@ type pbinRootVectors struct { } `json:"sequence_vectors"` } -func blake3Sum(b []byte) [32]byte { return blake3.Sum256(b) } +func pbinBlake3Sum(b []byte) [32]byte { return blake3.Sum256(b) } -// pbinBlake3Hash adapts blake3Sum to the engine's injectable hash seam. +// pbinBlake3Hash adapts pbinBlake3Sum to the engine's injectable hash seam. var pbinBlake3Hash pbinHashFn = func(b []byte) common.Hash { return common.Hash(blake3.Sum256(b)) } -func loadPBinRootVectors(t *testing.T) pbinRootVectors { +func pbinLoadRootVectors(t *testing.T) pbinRootVectors { t.Helper() raw, err := os.ReadFile("testdata/eip8297_vectors.json") require.NoError(t, err) @@ -77,12 +77,12 @@ func pbinOracleRootOf(t *testing.T, entries map[string][]byte) [32]byte { for _, k := range keys { tree.insert([]byte(k), entries[k]) } - return pbinOracleMerkelizeWith(tree.root, blake3Sum) + return pbinOracleMerkelizeWith(tree.root, pbinBlake3Sum) } func TestPBinOracleMatchesSpecTrieRoots(t *testing.T) { t.Parallel() - v := loadPBinRootVectors(t) + v := pbinLoadRootVectors(t) require.NotEmpty(t, v.Trie) for _, tc := range v.Trie { @@ -106,7 +106,7 @@ func TestPBinOracleMatchesSpecTrieRoots(t *testing.T) { // divergence is pinned to the op that caused it. func TestPBinOracleMatchesSpecSequenceRoots(t *testing.T) { t.Parallel() - v := loadPBinRootVectors(t) + v := pbinLoadRootVectors(t) require.NotEmpty(t, v.Sequences) checked := 0 diff --git a/execution/commitment/pbin_specvectors_test.go b/execution/commitment/pbin_specvectors_test.go index 5ad831b0f1a..33a1c11ef5d 100644 --- a/execution/commitment/pbin_specvectors_test.go +++ b/execution/commitment/pbin_specvectors_test.go @@ -42,7 +42,7 @@ type pbinSpecChunkifyVector struct { Chunks []string `json:"chunks"` } -func loadPBinSpecVectors(t *testing.T) pbinSpecVectors { +func pbinLoadSpecVectors(t *testing.T) pbinSpecVectors { t.Helper() raw, err := os.ReadFile("testdata/eip8297_vectors.json") require.NoError(t, err) @@ -51,7 +51,7 @@ func loadPBinSpecVectors(t *testing.T) pbinSpecVectors { return v } -func mustHex(t *testing.T, s string) []byte { +func pbinMustHex(t *testing.T, s string) []byte { t.Helper() b, err := hex.DecodeString(s[2:]) require.NoError(t, err) @@ -62,7 +62,7 @@ func mustHex(t *testing.T, s string) []byte { // different hash: BASIC_DATA packing is pure byte layout. func TestPBinSpecBasicDataVectors(t *testing.T) { t.Parallel() - v := loadPBinSpecVectors(t) + v := pbinLoadSpecVectors(t) require.NotEmpty(t, v.BasicData) for _, tc := range v.BasicData { @@ -71,7 +71,7 @@ func TestPBinSpecBasicDataVectors(t *testing.T) { got, err := pbinEncodeBasicData(tc.Nonce, bal, tc.CodeSize) require.NoError(t, err) - require.Equal(t, mustHex(t, tc.Value), got[:], + require.Equal(t, pbinMustHex(t, tc.Value), got[:], "BASIC_DATA mismatch for code_size=%d nonce=%d balance=%s", tc.CodeSize, tc.Nonce, tc.Balance) } } @@ -83,15 +83,15 @@ func TestPBinSpecBasicDataVectors(t *testing.T) { // diverge here (guards H3). func TestPBinSpecKeyRouting(t *testing.T) { t.Parallel() - v := loadPBinSpecVectors(t) - addr := mustHex(t, v.Embedding.Address) + v := pbinLoadSpecVectors(t) + addr := pbinMustHex(t, v.Embedding.Address) require.Len(t, addr, 20) hasher := pbinKeyHasherWith(pbinBlake3Hash) - require.Equal(t, mustHex(t, v.Embedding.BasicDataKey), hasher(addr), "BASIC_DATA key") + require.Equal(t, pbinMustHex(t, v.Embedding.BasicDataKey), hasher(addr), "BASIC_DATA key") c := pbinDigestCache{sum: pbinBlake3Hash} - require.Equal(t, mustHex(t, v.Embedding.CodeHashKey), c.accountKey(addr, pbinCodeHashLeafKey), "CODE_HASH key") + require.Equal(t, pbinMustHex(t, v.Embedding.CodeHashKey), c.accountKey(addr, pbinCodeHashLeafKey), "CODE_HASH key") for _, s := range v.Embedding.Slots { slot, err := uint256.FromDecimal(s.Slot.String()) @@ -99,6 +99,6 @@ func TestPBinSpecKeyRouting(t *testing.T) { slotBytes := slot.Bytes32() plainKey := append(append(make([]byte, 0, len(addr)+len(slotBytes)), addr...), slotBytes[:]...) - require.Equal(t, mustHex(t, s.Key), hasher(plainKey), "slot %s key", s.Slot) + require.Equal(t, pbinMustHex(t, s.Key), hasher(plainKey), "slot %s key", s.Slot) } } diff --git a/execution/commitment/pbin_vs_hex_compare_test.go b/execution/commitment/pbin_vs_hex_compare_test.go index 43a068403c8..afe981dc044 100644 --- a/execution/commitment/pbin_vs_hex_compare_test.go +++ b/execution/commitment/pbin_vs_hex_compare_test.go @@ -16,7 +16,7 @@ import ( // corpus. Roots differ by construction — the trees, keys and node preimages all // differ — so this measures shape and footprint, not equality. -type engineShape struct { +type pbinEngineShape struct { name string root []byte records int @@ -24,7 +24,7 @@ type engineShape struct { depthBits []int // path length to each stored branch, in key bits } -func (s engineShape) depthStats() (maxD, p50, mean int) { +func (s pbinEngineShape) depthStats() (maxD, p50, mean int) { if len(s.depthBits) == 0 { return 0, 0, 0 } @@ -37,9 +37,9 @@ func (s engineShape) depthStats() (maxD, p50, mean int) { return d[len(d)-1], d[len(d)/2], sum / len(d) } -// hexPathBits converts a HexToCompact-encoded branch key to a path length in +// pbinHexPathBits converts a HexToCompact-encoded branch key to a path length in // key bits so the two radices are comparable: one nibble is four bits. -func hexPathBits(compact string) int { +func pbinHexPathBits(compact string) int { if len(compact) == 0 { return 0 } @@ -58,7 +58,7 @@ func pbinPathBits(key string) int { return int(p.bitLen) } -func runHex(t *testing.T, plainKeys [][]byte, updates []Update) engineShape { +func pbinRunHex(t *testing.T, plainKeys [][]byte, updates []Update) pbinEngineShape { t.Helper() ms := NewMockState(t) // PBin derives its zone from the plain-key length, so a comparison corpus @@ -71,16 +71,16 @@ func runHex(t *testing.T, plainKeys [][]byte, updates []Update) engineShape { root, err := hph.Process(context.Background(), upds, "", nil, WarmupConfig{}) require.NoError(t, err) - s := engineShape{name: "hex", root: root} + s := pbinEngineShape{name: "hex", root: root} for k, v := range ms.cm { s.records++ s.recordByte += len(v) - s.depthBits = append(s.depthBits, hexPathBits(k)) + s.depthBits = append(s.depthBits, pbinHexPathBits(k)) } return s } -func runPBin(t *testing.T, plainKeys [][]byte, updates []Update) (engineShape, pbinCounters) { +func pbinRunBin(t *testing.T, plainKeys [][]byte, updates []Update) (pbinEngineShape, pbinCounters) { t.Helper() ms := NewMockState(t) pph := NewPBinPatriciaHashed(ms) @@ -91,7 +91,7 @@ func runPBin(t *testing.T, plainKeys [][]byte, updates []Update) (engineShape, p root, err := pph.Process(context.Background(), upds, "", nil, WarmupConfig{}) require.NoError(t, err) - s := engineShape{name: "bin", root: root} + s := pbinEngineShape{name: "bin", root: root} for k, v := range ms.cm { s.records++ s.recordByte += len(v) @@ -100,10 +100,10 @@ func runPBin(t *testing.T, plainKeys [][]byte, updates []Update) (engineShape, p return s, pph.counters } -// clusteredCorpus gives every contract slots that share a storage group, which -// is what EIP-8297's raw sub-index co-locates. scatteredCorpus spreads slots so +// pbinClusteredCorpus gives every contract slots that share a storage group, which +// is what EIP-8297's raw sub-index co-locates. pbinScatteredCorpus spreads slots so // no two share a group — the mapping-style access random corpora produce. -func clusteredCorpus(contracts, slotsPer int) ([][]byte, []Update) { +func pbinClusteredCorpus(contracts, slotsPer int) ([][]byte, []Update) { ub := NewUpdateBuilder() for c := range contracts { addr := fmt.Sprintf("%040x", c+1) @@ -115,7 +115,7 @@ func clusteredCorpus(contracts, slotsPer int) ([][]byte, []Update) { return ub.Build() } -func scatteredCorpus(contracts, slotsPer int) ([][]byte, []Update) { +func pbinScatteredCorpus(contracts, slotsPer int) ([][]byte, []Update) { ub := NewUpdateBuilder() for c := range contracts { addr := fmt.Sprintf("%040x", c+1) @@ -135,14 +135,14 @@ func TestPBinVsHexStructure(t *testing.T) { name string build func(int, int) ([][]byte, []Update) }{ - {"clustered", clusteredCorpus}, - {"scattered", scatteredCorpus}, + {"clustered", pbinClusteredCorpus}, + {"scattered", pbinScatteredCorpus}, } { t.Run(tc.name, func(t *testing.T) { plainKeys, updates := tc.build(16, 16) - hex := runHex(t, plainKeys, updates) - bin, counters := runPBin(t, plainKeys, updates) + hex := pbinRunHex(t, plainKeys, updates) + bin, counters := pbinRunBin(t, plainKeys, updates) require.NotEqual(t, hex.root, bin.root, "hex and binary trees must not agree on a root; equality means one of them is not the tree it claims to be") @@ -198,14 +198,14 @@ func TestPBinStemCoLocation(t *testing.T) { require.NotEqual(t, base[33:65], next[33:65], "a new tree_index must move the group digest") // a co-located pair shares a long prefix; a cross-group pair does not - sharedBits := commonPrefixBitsOfKeys(base, c.storageKey(addr, slotOf(257))) - crossBits := commonPrefixBitsOfKeys(base, next) + sharedBits := pbinCommonPrefixBitsOfKeys(base, c.storageKey(addr, slotOf(257))) + crossBits := pbinCommonPrefixBitsOfKeys(base, next) require.Greater(t, sharedBits, crossBits, "co-located slots must share a longer key prefix than cross-group slots") t.Logf("co-located slots share %d bits; cross-group share %d bits", sharedBits, crossBits) } -func commonPrefixBitsOfKeys(a, b []byte) int { +func pbinCommonPrefixBitsOfKeys(a, b []byte) int { n := min(len(a), len(b)) for i := range n { if a[i] != b[i] { From e0507afbbb23e7b38453f459956319a4f51b2e5a Mon Sep 17 00:00:00 2001 From: awskii Date: Fri, 31 Jul 2026 00:22:21 +0700 Subject: [PATCH 39/56] =?UTF-8?q?feat:=20M1=20documentation=20=E2=80=94=20?= =?UTF-8?q?pbin=20engine=20doc,=20variant=20comment,=20pbin=20naming=20con?= =?UTF-8?q?vention?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- AGENTS.md | 2 ++ .../20260730-pbin-m1-local-el.md | 8 +++---- execution/commitment/commitment.go | 5 ++-- execution/commitment/pbin_patricia_hashed.go | 23 +++++++++++++------ 4 files changed, 25 insertions(+), 13 deletions(-) rename docs/plans/{ => completed}/20260730-pbin-m1-local-el.md (97%) diff --git a/AGENTS.md b/AGENTS.md index 03cdd307294..ee2035d1434 100644 --- a/AGENTS.md +++ b/AGENTS.md @@ -118,6 +118,8 @@ Commit messages: prefix with package(s) modified, e.g., `eth, rpc: make trace co Don't sign commits, pr's, issues, comments. +`package commitment` holds two engines in one namespace. Every package-level identifier belonging to the EIP-8297 binary trie carries a `pbin` prefix (`PBin` for exported ones) — the hex engine already owns the generic names (`cell`, `fold`, `unfold`, `computeCellHash`), so an unprefixed addition is a collision waiting to happen. Test helpers included. + Run `make lint` before every push. The linter is non-deterministic — run it repeatedly until clean. **Important**: Always run `make lint` after making code changes and before committing. Fix any linter errors before proceeding. PRs must pass `make lint` before being opened or updated. diff --git a/docs/plans/20260730-pbin-m1-local-el.md b/docs/plans/completed/20260730-pbin-m1-local-el.md similarity index 97% rename from docs/plans/20260730-pbin-m1-local-el.md rename to docs/plans/completed/20260730-pbin-m1-local-el.md index 04d3e9af526..24fb8237829 100644 --- a/docs/plans/20260730-pbin-m1-local-el.md +++ b/docs/plans/completed/20260730-pbin-m1-local-el.md @@ -396,10 +396,10 @@ Production keeps Keccak-256. This task only makes the **test** path run the whol ### Task 16: [Final] Update documentation -- [ ] update the package doc comment on `pbin_patricia_hashed.go` to state BLAKE3, the M1 scope, and the stated limitations (no witness, no getProof, no parallel) -- [ ] ➕ fix the stale comment on `VariantBinPatriciaTrie` (`commitment.go:154-155`), which still says the variant is not wired to the domain layer and has no state save/restore — both untrue since Task 5 (found in the Task 15 audit) -- [ ] update `CLAUDE.md` if new patterns were discovered -- [ ] move this plan to `docs/plans/completed/` +- [x] update the package doc comment on `pbin_patricia_hashed.go` to state BLAKE3, the M1 scope, and the stated limitations (no witness, no getProof, no parallel) — ⚠️ wording correction against the checklist item: the doc states BLAKE3 as the **test-only** override set through `setHashSuite`, since production is Keccak (Solution Overview point 1); writing "the engine uses BLAKE3" would have made the doc claim the cross-client compatibility the plan forbids. Limitations named: sequential `ModeDirect` only, parallel/streaming structurally out, and the four record-reinterpreting paths (witness, `eth_getProof`, `eth_simulateV1`, receipt regeneration) refusing rather than reading bit-path records as hex +- [x] ➕ fix the stale comment on `VariantBinPatriciaTrie` (`commitment.go:154-155`), which still says the variant is not wired to the domain layer and has no state save/restore — both untrue since Task 5 (found in the Task 15 audit) — now names what is actually true of the variant (experimental, whole-datadir, sequential) and points at the engine doc for the unsupported paths instead of restating them +- [x] update `CLAUDE.md` if new patterns were discovered — one addition, the `pbin` prefix rule under Conventions. It is the only M1 convention that outlives this plan: `package commitment` carries two engines in one namespace and the hex one owns the generic names, so the rule binds every future binary-trie change, not just M1. Everything else discovered here is either recorded in the code or specific to this milestone. (`CLAUDE.md` is a symlink to `AGENTS.md`; the edit lands in the target) +- [x] move this plan to `docs/plans/completed/` ## Post-Completion diff --git a/execution/commitment/commitment.go b/execution/commitment/commitment.go index 59e6949bc8b..2c06a1aa02b 100644 --- a/execution/commitment/commitment.go +++ b/execution/commitment/commitment.go @@ -151,8 +151,9 @@ const ( VariantHexPatriciaTrie TrieVariant = "hex-patricia-hashed" VariantParallelHexPatricia TrieVariant = "hex-parallel-patricia-hashed" VariantStreamingHexPatricia TrieVariant = "hex-streaming-patricia-hashed" - // VariantBinPatriciaTrie is EIP-8297's binary tree. It is not wired to the - // domain layer: commitment state save/restore has no binary implementation. + // VariantBinPatriciaTrie is EIP-8297's binary tree. Experimental: a + // whole-datadir property resolved at first start, sequential only, and + // unsupported on the paths listed in PBinPatriciaHashed's doc. VariantBinPatriciaTrie TrieVariant = "bin-patricia-hashed" ) diff --git a/execution/commitment/pbin_patricia_hashed.go b/execution/commitment/pbin_patricia_hashed.go index 5b421f4aaa7..f604f93937c 100644 --- a/execution/commitment/pbin_patricia_hashed.go +++ b/execution/commitment/pbin_patricia_hashed.go @@ -17,16 +17,25 @@ // PBinPatriciaHashed — commitment over EIP-8297's partitioned binary tree. // // The EIP leaves its hash function open and names Keccak-256 as a candidate; -// this engine uses Keccak-256 both for node hashing and for tree-key -// derivation, behind pbinHasher so the suite can be swapped. +// this engine uses Keccak-256 both for node hashing and for tree-key derivation. +// BLAKE3 is a test-only override, set on both seams at once by setHashSuite so +// the reference vectors can be replayed. It buys comparability with the +// reference implementation, never agreement with another client — no client +// would agree with a Keccak-keyed binary tree. // // Scope: Process over all three zones, ModeDirect only. Code is chunked into the // account header's chunk leaves, overflowing into the code zone where chunks are -// content-addressed by code hash and shared between accounts. Parallel mounting -// is out. EIP-8297 has no removal: a zeroed storage slot keeps its leaf at 32 -// zero bytes, an account removal is refused rather than guessed at, and code -// chunks above a shortened redeploy's length stay in the tree — the tree is a -// function of history there, not of current state. +// content-addressed by code hash and shared between accounts. Parallel and +// streaming mounting are structurally out — their prefix trie is nibble-shaped +// and the binary key space has no nibbles. The paths that reinterpret commitment +// records outside block execution — witness, eth_getProof, eth_simulateV1, +// receipt regeneration — refuse this variant rather than read bit-path records +// as hex ones. +// +// EIP-8297 has no removal: a zeroed storage slot keeps its leaf at 32 zero +// bytes, an account removal is refused rather than guessed at, and code chunks +// above a shortened redeploy's length stay in the tree — the tree is a function +// of history there, not of current state. package commitment From 058483d13f5b00923b9c6d6734ddd4c0ecd404f5 Mon Sep 17 00:00:00 2001 From: awskii Date: Fri, 31 Jul 2026 16:14:55 +0700 Subject: [PATCH 40/56] =?UTF-8?q?fix:=20pbin=20review=20findings=20?= =?UTF-8?q?=E2=80=94=20stale=20run=20state,=20shadow=20cross-check?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Process cleared lastKeyLen but not the code buffers, so a run that failed mid-stream left chunks queued for the next run to emit against its own keys. fold left the folded row's prevRecord pointing at the record it just consumed. shadowCrossCheck compared two roots this node computed itself, so routing it through headerRootMismatch let the header-root toggle disable the only check validating the BAL-driven path. --- db/state/pbin_variant_persist_test.go | 16 +++--- .../commitmentdb/commitment_context.go | 15 +++-- .../commitmentdb/pbin_nocache_test.go | 9 +-- execution/commitment/pbin_cell_test.go | 55 ++++++++++++++---- execution/commitment/pbin_code_test.go | 57 ++++++++++++++++++- execution/commitment/pbin_fuzz_test.go | 56 ++++++++++++------ execution/commitment/pbin_state_test.go | 6 +- execution/stagedsync/committer.go | 5 +- 8 files changed, 166 insertions(+), 53 deletions(-) diff --git a/db/state/pbin_variant_persist_test.go b/db/state/pbin_variant_persist_test.go index 62b20b3602a..978baaa0650 100644 --- a/db/state/pbin_variant_persist_test.go +++ b/db/state/pbin_variant_persist_test.go @@ -109,7 +109,7 @@ func TestPBinVariantHexDatadirRefusesBinFlag(t *testing.T) { dirs := datadir.New(t.TempDir()) pbinWriteToml(t, dirs, content) _, err := ResolveErigonDBSettings(dirs, log.New(), false) - require.Error(t, err) + require.ErrorContains(t, err, "datadir was created with the hex commitment trie") }) } } @@ -122,14 +122,14 @@ func TestPBinVariantBinDatadirRefusesStreamingAndParallel(t *testing.T) { dirs := datadir.New(t.TempDir()) pbinWriteToml(t, dirs, binToml) _, err := ResolveErigonDBSettings(dirs, log.New(), false) - require.Error(t, err) + require.ErrorContains(t, err, "sequential-only") }) t.Run("parallel", func(t *testing.T) { pbinWithVariantFlags(t, false, false, true) dirs := datadir.New(t.TempDir()) pbinWriteToml(t, dirs, binToml) _, err := ResolveErigonDBSettings(dirs, log.New(), false) - require.Error(t, err) + require.ErrorContains(t, err, "sequential-only") }) } @@ -139,14 +139,14 @@ func TestPBinVariantRefusesReferences(t *testing.T) { dirs := datadir.New(t.TempDir()) pbinWriteToml(t, dirs, "step_size = 100\nsteps_in_frozen_file = 8\nreferences_in_commitment_branches = true\ntrie_variant = \"bin\"\n") _, err := ResolveErigonDBSettings(dirs, log.New(), false) - require.Error(t, err) + require.ErrorContains(t, err, "references_in_commitment_branches") }) t.Run("first_start", func(t *testing.T) { pbinWithVariantFlags(t, true, false, false) dirs := datadir.New(t.TempDir()) refs := true _, err := ResolveErigonDBSettingsWithRefsDefault(dirs, log.New(), true, &refs) - require.Error(t, err) + require.ErrorContains(t, err, "references_in_commitment_branches") }) } @@ -156,7 +156,7 @@ func TestPBinVariantLegacyDatadirRefusesBin(t *testing.T) { require.NoError(t, os.WriteFile(filepath.Join(dirs.Snap, datadir.PreverifiedFileName), []byte(""), 0644)) _, err := ResolveErigonDBSettings(dirs, log.New(), false) - require.Error(t, err) + require.ErrorContains(t, err, "already has hex commitment state") } func TestPBinVariantUnknownVariantRefused(t *testing.T) { @@ -165,7 +165,7 @@ func TestPBinVariantUnknownVariantRefused(t *testing.T) { pbinWriteToml(t, dirs, "step_size = 100\nsteps_in_frozen_file = 8\ntrie_variant = \"verkle\"\n") _, err := ResolveErigonDBSettings(dirs, log.New(), false) - require.Error(t, err) + require.ErrorContains(t, err, "unknown trie_variant") } func TestPBinVariantFreshWithDownloaderPersistsBin(t *testing.T) { @@ -211,5 +211,5 @@ func TestPBinVariantFreshWithDownloaderRefusesDeliveredHexToml(t *testing.T) { // resolve must refuse rather than silently adopt hex. pbinWriteToml(t, dirs, "step_size = 100\nsteps_in_frozen_file = 8\n") _, err = ResolveErigonDBSettings(dirs, log.New(), false) - require.Error(t, err) + require.ErrorContains(t, err, "datadir was created with the hex commitment trie") } diff --git a/execution/commitment/commitmentdb/commitment_context.go b/execution/commitment/commitmentdb/commitment_context.go index d50d19255c2..e6a200aede6 100644 --- a/execution/commitment/commitmentdb/commitment_context.go +++ b/execution/commitment/commitmentdb/commitment_context.go @@ -508,6 +508,9 @@ func (sdc *SharedDomainsCommitmentContext) ComputeCommitment(ctx context.Context sdc.patriciaTrie.SetTraceWriter(sdc.traceW) if updateCount == 0 { + // The binary trie reads its stored root record here, so the trie has to be + // bound to this tx even on the path that touches nothing. + sdc.trieContext(tx, blockNum, txNum, ctx) rootHash, err = sdc.patriciaTrie.RootHash() return rootHash, err } @@ -727,11 +730,12 @@ func (sdc *SharedDomainsCommitmentContext) warmupTrieContextFactory(db kv.Tempor wm := kvmetrics.NewDomainMetrics() workerCtx := kvmetrics.ContextWithMetrics(ctx, wm) warmupCtx := &TrieContext{ - getter: sdc.sharedDomains.AsGetter(roTx), - putter: sdc.sharedDomains.AsPutDel(roTx), - stepSize: stepSize, - txNum: txNum, - traceW: sdc.traceW, + getter: sdc.sharedDomains.AsGetter(roTx), + putter: sdc.sharedDomains.AsPutDel(roTx), + stepSize: stepSize, + txNum: txNum, + traceW: sdc.traceW, + readCodeSize: sdc.variant == commitment.VariantBinPatriciaTrie, } if sdc.stateReader != nil { warmupCtx.stateReader = sdc.stateReader.CloneForWorker(workerCtx, roTx) @@ -779,6 +783,7 @@ func (sdc *SharedDomainsCommitmentContext) concurrentTrieContextFactory(db kv.Te txNum: txNum, localCollector: collector, traceW: sdc.traceW, + readCodeSize: sdc.variant == commitment.VariantBinPatriciaTrie, } if sdc.stateReader != nil { warmupCtx.stateReader = sdc.stateReader.CloneForWorker(workerCtx, roTx) diff --git a/execution/commitment/commitmentdb/pbin_nocache_test.go b/execution/commitment/commitmentdb/pbin_nocache_test.go index cb4f2982786..4d2b9c827a7 100644 --- a/execution/commitment/commitmentdb/pbin_nocache_test.go +++ b/execution/commitment/commitmentdb/pbin_nocache_test.go @@ -111,9 +111,7 @@ func pbinNewTestDb(tb testing.TB) kv.TemporalRwDB { // TestPBinSharedDomainsHasNoSharedBranchCache checks the execctx wiring: a // bin-variant SharedDomains over an aggregator whose AggTx provides the shared -// BranchCache must reach the commitment-context ctor without it. While the bin -// variant still lacks state save/restore the ctor refuses it outright — but it -// must refuse for that reason, never because a shared cache got through. +// BranchCache must reach the commitment-context ctor without it, and must open. func TestPBinSharedDomainsHasNoSharedBranchCache(t *testing.T) { t.Parallel() @@ -131,10 +129,7 @@ func TestPBinSharedDomainsHasNoSharedBranchCache(t *testing.T) { sd, sdErr = execctx.NewSharedDomains(t.Context(), tx, log.New(), execctx.WithTrieConfig(cfg)) require.NoError(t, sdErr) }) - if msg != "" { - require.NotContains(t, msg, "branch cache") - return - } + require.Empty(t, msg) defer sd.Close() require.False(t, sd.HasSharedBranchCache()) } diff --git a/execution/commitment/pbin_cell_test.go b/execution/commitment/pbin_cell_test.go index 143d35323d9..d52690f97f6 100644 --- a/execution/commitment/pbin_cell_test.go +++ b/execution/commitment/pbin_cell_test.go @@ -58,6 +58,18 @@ func pbinTestLeafCell(pattern byte, bitLen int16) pbinCell { return c } +// pbinTestChunkLeafCell builds the one leaf shape that carries its value in the +// record instead of a plain key: a code chunk. +func pbinTestChunkLeafCell(pattern byte, bitLen int16) pbinCell { + c := pbinTestBranchCell(pattern, bitLen) + c.kind = pbinNodeLeaf + for i := range c.Storage { + c.Storage[i] = pattern ^ byte(i+1) + } + c.Flags, c.StorageLen = StorageUpdate, pbinValueLength + return c +} + // A prefix of any admissible bit length must survive a record round-trip: the // 66-byte storage path does not fit the shared codec's fields, and a silent // truncation would commit a wrong root (guards H4). @@ -103,6 +115,8 @@ func TestPBinBranchCodecRoundTripCellShapes(t *testing.T) { {"hashless account leaf", 0b11, 0b11, [2]pbinCell{accountLeaf, pbinTestLeafCell(0x05, 64)}}, {"only the right cell present", 0b10, 0b10, [2]pbinCell{pbinTestEmptyCell(), pbinTestBranchCell(0x07, 9)}}, {"deleted left cell", 0b11, 0b10, [2]pbinCell{pbinTestEmptyCell(), pbinTestBranchCell(0x08, 9)}}, + {"record-resident chunk leaf", 0b11, 0b11, [2]pbinCell{pbinTestChunkLeafCell(0x09, 12), pbinTestBranchCell(0x0A, 21)}}, + {"two chunk leaves", 0b11, 0b11, [2]pbinCell{pbinTestChunkLeafCell(0x0B, 0), pbinTestChunkLeafCell(0x0C, 528)}}, } { t.Run(tc.name, func(t *testing.T) { t.Parallel() @@ -126,20 +140,30 @@ func TestPBinBranchCodecRoundTripCellShapes(t *testing.T) { func TestPBinBranchCodecIsCanonical(t *testing.T) { t.Parallel() - cells := [2]pbinCell{pbinTestLeafCell(0x7C, 33), pbinTestBranchCell(0x3E, 528)} + for _, tc := range []struct { + name string + cells [2]pbinCell + }{ + {"plain-key leaf and branch", [2]pbinCell{pbinTestLeafCell(0x7C, 33), pbinTestBranchCell(0x3E, 528)}}, + {"chunk leaf and branch", [2]pbinCell{pbinTestChunkLeafCell(0x6D, 33), pbinTestBranchCell(0x3E, 528)}}, + } { + t.Run(tc.name, func(t *testing.T) { + t.Parallel() - var enc pbinBranchEncoder - rec, err := enc.encode(0b11, 0b11, &cells) - require.NoError(t, err) - want := bytes.Clone(rec) + var enc pbinBranchEncoder + rec, err := enc.encode(0b11, 0b11, &tc.cells) + require.NoError(t, err) + want := bytes.Clone(rec) - var got [2]pbinCell - _, _, err = pbinDecodeBranch(want, &got) - require.NoError(t, err) + var got [2]pbinCell + _, _, err = pbinDecodeBranch(want, &got) + require.NoError(t, err) - again, err := enc.encode(0b11, 0b11, &got) - require.NoError(t, err) - require.Equal(t, want, again) + again, err := enc.encode(0b11, 0b11, &got) + require.NoError(t, err) + require.Equal(t, want, again) + }) + } } // pbinTestRecord assembles a record by hand so decode can be probed with bytes @@ -201,6 +225,15 @@ func TestPBinBranchDecodeRejects(t *testing.T) { {"leaf without a plain key", pbinTestRecord(0b01, 0b01, pbinTestCellBody(pbinFieldLeaf, 0, nil))}, {"leaf naming both plain keys", pbinTestRecord(0b01, 0b01, pbinTestCellBody(pbinFieldLeaf|pbinFieldAccountAddr|pbinFieldStorageAddr, 0, nil, append(pbinTestLenAndVal(bytes.Repeat([]byte{0xEE}, length.Addr)), pbinTestLenAndVal(bytes.Repeat([]byte{0xEE}, length.Addr+length.Hash))...)...))}, + // A record-resident value and a plain key are two answers to the same + // question; a branch has no value at all. + {"leaf naming a plain key and a record value", pbinTestRecord(0b01, 0b01, pbinTestCellBody(pbinFieldLeaf|pbinFieldAccountAddr|pbinFieldLeafValue, 0, nil, + append(pbinTestLenAndVal(bytes.Repeat([]byte{0xEE}, length.Addr)), pbinTestLenAndVal(bytes.Repeat([]byte{0xEE}, pbinValueLength))...)...))}, + {"branch carrying a record value", pbinTestRecord(0b01, 0b01, pbinTestCellBody(pbinFieldBranch|pbinFieldLeafValue, 0, nil, + pbinTestLenAndVal(bytes.Repeat([]byte{0xEE}, pbinValueLength))...))}, + {"record value shorter than a leaf value", pbinTestRecord(0b01, 0b01, pbinTestCellBody(pbinFieldLeaf|pbinFieldLeafValue, 0, nil, + pbinTestLenAndVal(bytes.Repeat([]byte{0xEE}, pbinValueLength-1))...))}, + {"truncated record value", pbinTestRecord(0b01, 0b01, pbinTestCellBody(pbinFieldLeaf|pbinFieldLeafValue, 0, nil, pbinValueLength, 0xEE))}, } { t.Run(tc.name, func(t *testing.T) { t.Parallel() diff --git a/execution/commitment/pbin_code_test.go b/execution/commitment/pbin_code_test.go index 8f006fc7793..7247f663ea2 100644 --- a/execution/commitment/pbin_code_test.go +++ b/execution/commitment/pbin_code_test.go @@ -23,6 +23,8 @@ import ( "testing" "github.com/stretchr/testify/require" + + "github.com/erigontech/erigon/common/empty" ) // TestPBinChunkifyCodeVectors is the external check on chunk_code (eip:374-397): @@ -229,6 +231,59 @@ func TestPBinShorteningRedeployKeepsStaleChunks(t *testing.T) { } } +// TestPBinClearedCodeKeepsChunks covers the other half of H8: clearing an +// account's code — an EIP-7702 delegation reset is the common case — is a +// shortening redeploy down to zero chunks. The header leaves follow the account, +// the chunks stay behind, and nothing in the state records that they exist. +func TestPBinClearedCodeKeepsChunks(t *testing.T) { + t.Parallel() + + addr := pbinOracleAddr(19) + designator := pbinTestCode(23) // the size a 7702 designator occupies + deploy := new(pbinTestCorpus).accountWithCodeBytes(addr, 1, 10, designator) + cleared := new(pbinTestCorpus).account(addr, 2, 10, empty.CodeHash) + + _, _, forward := pbinTestBatches(t, deploy, cleared) + + want := cleared.entries(t) + for i, chunk := range pbinChunkifyCode(designator) { + want = append(want, pbinOracleEntry{key: pbinTreeKeyCodeChunk(addr, i), value: chunk[:]}) + } + wantRoot := pbinOracleRoot(want) + require.Equal(t, wantRoot[:], forward, "clearing code leaves its chunks in the tree") + + _, rebuilt := new(pbinTestCorpus).account(addr, 2, 10, empty.CodeHash).process(t) + require.NotEqual(t, rebuilt, forward, "the state a rebuild reads no longer names the chunks") +} + +// TestPBinGrowingRedeployReplacesChunks is the case a rebuild does reproduce: +// code that only grows overwrites every chunk it had and adds the rest, so the +// forward tree and a rebuild from state agree. Both a redeploy inside the +// account header and one spilling into the code zone. +func TestPBinGrowingRedeployReplacesChunks(t *testing.T) { + t.Parallel() + + for _, tc := range []struct{ before, after int }{ + {before: 31, after: 62}, // 1 chunk to 2, both in the header + {before: 62, after: pbinHeaderCodeChunks*pbinChunkDataLen + 62}, // header-only to header plus code zone + } { + t.Run(fmt.Sprintf("%d bytes up to %d", tc.before, tc.after), func(t *testing.T) { + t.Parallel() + + addr := pbinOracleAddr(20) + short, long := pbinTestCode(tc.before), pbinTestCode(tc.after) + deploy := new(pbinTestCorpus).accountWithCodeBytes(addr, 1, 10, short) + redeploy := new(pbinTestCorpus).accountWithCodeBytes(addr, 2, 10, long) + + _, _, forward := pbinTestBatches(t, deploy, redeploy) + + _, rebuilt := new(pbinTestCorpus).accountWithCodeBytes(addr, 2, 10, long).process(t) + require.Equal(t, rebuilt, forward, "growth leaves no chunk of the old code behind") + require.Equal(t, redeploy.oracleRoot(t), forward) + }) + } +} + // TestPBinCodelessContextRefusesCodeBearingAccount pins that the code read is // not optional: a context that cannot serve code cannot commit an account whose // chunks the tree needs. @@ -266,7 +321,7 @@ func TestPBinCodeSizeMustMatchTheCodeBehindIt(t *testing.T) { upd := WrapKeyUpdates(t, ModeDirect, pbinKeyHasher(), corpus.plainKeys, corpus.updates) _, err := pph.Process(context.Background(), upd, "", nil, WarmupConfig{}) - require.Error(t, err) + require.ErrorContains(t, err, "the code domain holds") } // TestPBinZoneKeyLengthIsExplicit pins that the code zone is recognised rather diff --git a/execution/commitment/pbin_fuzz_test.go b/execution/commitment/pbin_fuzz_test.go index 951b43e67e2..f66d2a9c61a 100644 --- a/execution/commitment/pbin_fuzz_test.go +++ b/execution/commitment/pbin_fuzz_test.go @@ -34,16 +34,36 @@ var pbinFuzzSlots = []uint64{0, 1, 2, 63, 64, 65, 66, 127, 128, 255, 256, 257, 2 // pbinFuzzAccountBit is the selector bit choosing an account write over a slot. const pbinFuzzAccountBit = 0x04 +// pbinFuzzCodeSizes is the code pool. The last entry is the only one that spills +// past the account header into the code zone. +var pbinFuzzCodeSizes = []int{0, 23, 31, 62, pbinHeaderCodeChunks*pbinChunkDataLen + 62} + +// pbinFuzzCode is the code an address carries for a whole run. Keying it on the +// address is what keeps the oracle valid: a redeploy to shorter code leaves its +// high chunks in the tree (H8), and the oracle only knows the final state. +func pbinFuzzCode(addrSeed, salt byte) []byte { + n := pbinFuzzCodeSizes[int(addrSeed+salt)%len(pbinFuzzCodeSizes)] + if n == 0 { + return nil + } + return pbinTestCode(n) +} + // pbinFuzzCorpus reads the input three bytes at a time — what to write, where, -// and with what value — drawing addresses and slots from small pools so keys -// collide by construction. -func pbinFuzzCorpus(data []byte) *pbinTestCorpus { +// and with what value — drawing addresses, slots and code lengths from small +// pools so keys collide by construction. +func pbinFuzzCorpus(data []byte, codeSalt byte) *pbinTestCorpus { c := new(pbinTestCorpus) for i := 0; i+2 < len(data); i += 3 { where, slot, value := data[i], data[i+1], data[i+2] - addr := pbinOracleAddr(uint64(where & 0x03)) + addrSeed := where & 0x03 + addr := pbinOracleAddr(uint64(addrSeed)) if where&pbinFuzzAccountBit != 0 { - c.account(addr, uint64(value), uint64(value)*1_000_000_007, common.Hash{value, 0xC0}) + if code := pbinFuzzCode(addrSeed, codeSalt); code != nil { + c.accountWithCodeBytes(addr, uint64(value), uint64(value)*1_000_000_007, code) + } else { + c.account(addr, uint64(value), uint64(value)*1_000_000_007, common.Hash{value, 0xC0}) + } continue } c.storage(addr, pbinOracleSlot(pbinFuzzSlots[int(slot)%len(pbinFuzzSlots)]), value, value^0xFF) @@ -53,16 +73,16 @@ func pbinFuzzCorpus(data []byte) *pbinTestCorpus { // pbinFuzzBatches cuts the corpus in two, so a run also covers what one Process // call leaves for the next to read back. -func pbinFuzzBatches(data []byte, cut byte) []*pbinTestCorpus { - c := pbinFuzzCorpus(data) +func pbinFuzzBatches(data []byte, cut, codeSalt byte) []*pbinTestCorpus { + c := pbinFuzzCorpus(data, codeSalt) if len(c.plainKeys) == 0 { return nil } at := int(cut) % (len(c.plainKeys) + 1) batches := make([]*pbinTestCorpus, 0, 2) for _, b := range []*pbinTestCorpus{ - {plainKeys: c.plainKeys[:at], updates: c.updates[:at]}, - {plainKeys: c.plainKeys[at:], updates: c.updates[at:]}, + {plainKeys: c.plainKeys[:at], updates: c.updates[:at], codes: c.codes}, + {plainKeys: c.plainKeys[at:], updates: c.updates[at:], codes: c.codes}, } { if len(b.plainKeys) > 0 { batches = append(batches, b) @@ -80,14 +100,16 @@ func FuzzPBinProcessMatchesOracle(f *testing.F) { // Seeds spell the generator's (selector, slot, value) triples: bit 2 of the // selector asks for an account, its low bits pick the address, and the slot // byte indexes the pool. - f.Add([]byte{0x04, 0, 1, 0x05, 0, 2}, byte(0)) // two accounts - f.Add([]byte{0x00, 10, 1, 0x00, 11, 2, 0x00, 12, 3}, byte(2)) // three slots of one group - f.Add([]byte{0x00, 3, 1, 0x00, 4, 2, 0x04, 0, 3}, byte(1)) // the 63/64 zone boundary plus a header - f.Add([]byte{0x04, 0, 1, 0x00, 15, 2, 0x01, 15, 3, 0x02, 16, 4}, byte(3)) // one slot per address - f.Add([]byte{0x00, 10, 1, 0x00, 10, 2, 0x00, 10, 3}, byte(1)) // the same slot rewritten + f.Add([]byte{0x04, 0, 1, 0x05, 0, 2}, byte(0), byte(0)) // two accounts, no code + f.Add([]byte{0x00, 10, 1, 0x00, 11, 2, 0x00, 12, 3}, byte(2), byte(0)) // three slots of one group + f.Add([]byte{0x00, 3, 1, 0x00, 4, 2, 0x04, 0, 3}, byte(1), byte(0)) // the 63/64 zone boundary plus a header + f.Add([]byte{0x04, 0, 1, 0x00, 15, 2, 0x01, 15, 3, 0x02, 16, 4}, byte(3), byte(0)) // one slot per address + f.Add([]byte{0x00, 10, 1, 0x00, 10, 2, 0x00, 10, 3}, byte(1), byte(0)) // the same slot rewritten + f.Add([]byte{0x04, 0, 1, 0x00, 5, 2, 0x04, 0, 3}, byte(1), byte(1)) // code interleaved with a header slot + f.Add([]byte{0x04, 0, 1, 0x05, 0, 2, 0x00, 17, 3}, byte(2), byte(4)) // code spilling into the code zone - f.Fuzz(func(t *testing.T, data []byte, cut byte) { - batches := pbinFuzzBatches(data, cut) + f.Fuzz(func(t *testing.T, data []byte, cut, codeSalt byte) { + batches := pbinFuzzBatches(data, cut, codeSalt) if len(batches) == 0 { return } @@ -95,7 +117,7 @@ func FuzzPBinProcessMatchesOracle(f *testing.F) { pph, ms := pbinTestEngine(t) var root []byte for _, b := range batches { - require.NoError(t, ms.applyPlainUpdates(b.plainKeys, b.updates)) + b.applyTo(t, ms) root = pbinTestProcess(t, pph, b.plainKeys, b.updates) } require.Len(t, root, length.Hash) diff --git a/execution/commitment/pbin_state_test.go b/execution/commitment/pbin_state_test.go index cb0567f142f..d2baf0467b5 100644 --- a/execution/commitment/pbin_state_test.go +++ b/execution/commitment/pbin_state_test.go @@ -127,7 +127,7 @@ func TestPBinSetStateRejectsForeignBlob(t *testing.T) { t.Run(name, func(t *testing.T) { t.Parallel() fresh := NewPBinPatriciaHashed(ms) - require.Error(t, fresh.SetState(blob), "blob %x must be refused", blob) + require.ErrorIs(t, fresh.SetState(blob), errPBinStateBlob, "blob %x must be refused", blob) }) } } @@ -142,6 +142,6 @@ func TestPBinStateRefusesOpenRows(t *testing.T) { pph.grid.activeRows = 1 _, err := pph.EncodeCurrentState(nil) - require.Error(t, err) - require.Error(t, pph.SetState(nil)) + require.ErrorIs(t, err, errPBinStateOpen) + require.ErrorIs(t, pph.SetState(nil), errPBinStateOpen) } diff --git a/execution/stagedsync/committer.go b/execution/stagedsync/committer.go index b51dcb6ffd4..19ec6a2e95b 100644 --- a/execution/stagedsync/committer.go +++ b/execution/stagedsync/committer.go @@ -1,6 +1,7 @@ package stagedsync import ( + "bytes" "context" "errors" "fmt" @@ -653,7 +654,9 @@ func (cc *commitmentCalculator) shadowCrossCheck(ctx context.Context, r *blockRe cc.fail(ctx, r, fmt.Errorf("shadow incremental compute: %w", err)) return } - if headerRootMismatch(rh, balRoot) { + // Both operands are roots this node computed, so the header-root toggle does + // not apply: this is the only thing validating the BAL-driven path. + if !bytes.Equal(rh, balRoot) { cc.fail(ctx, r, fmt.Errorf("%w: shadow mismatch block %d incremental %x BAL-driven %x", ErrWrongTrieRoot, r.BlockNum, rh, balRoot)) return From 26cc0a83a9d38a7d82274542759e8935e9d97bd5 Mon Sep 17 00:00:00 2001 From: awskii Date: Fri, 31 Jul 2026 16:15:04 +0700 Subject: [PATCH 41/56] feat: select the pbin trie hash at runtime The engine hashed Keccak-256 with BLAKE3 reachable only from tests, so a build could not agree with the other clients on a binary-trie testnet, which follow the execution-specs reference and hash BLAKE3. --experimental.bin-commitment.hash picks between them. The choice goes through setHashSuite, which sets node hashing and key derivation together so the two cannot diverge, and is persisted to erigondb.toml as trie_hash next to trie_variant: roots do not survive a change, so a datadir keeps the hash it was built with and refuses a differing flag. --- cmd/integration/commands/flags.go | 1 + cmd/utils/flags.go | 20 +++ db/state/erigondb_settings.go | 38 ++++- db/state/statecfg/state_schema.go | 6 + docs/pbin-m1b-smoke.md | 4 +- execution/commitment/commitment.go | 2 +- execution/commitment/pbin_hash.go | 44 +++++- execution/commitment/pbin_hashsuite_test.go | 137 +++++++++++++++++++ execution/commitment/pbin_keys.go | 2 +- execution/commitment/pbin_patricia_hashed.go | 24 ++-- node/cli/default_flags.go | 1 + node/eth/backend.go | 7 + node/ethconfig/config.go | 1 + 13 files changed, 268 insertions(+), 19 deletions(-) create mode 100644 execution/commitment/pbin_hashsuite_test.go diff --git a/cmd/integration/commands/flags.go b/cmd/integration/commands/flags.go index 954ba1b28c0..df456391513 100644 --- a/cmd/integration/commands/flags.go +++ b/cmd/integration/commands/flags.go @@ -177,6 +177,7 @@ func withExperimentalCommitment(cmd *cobra.Command) { cmd.Flags().BoolVar(&statecfg.ExperimentalParallelCommitment, utils.ExperimentalParallelCommitmentFlag.Name, statecfg.ExperimentalParallelCommitment, utils.ExperimentalParallelCommitmentFlag.Usage) cmd.Flags().BoolVar(&statecfg.ExperimentalStreamingCommitment, utils.ExperimentalStreamingCommitmentFlag.Name, statecfg.ExperimentalStreamingCommitment, utils.ExperimentalStreamingCommitmentFlag.Usage) cmd.Flags().BoolVar(&statecfg.ExperimentalBinCommitment, utils.ExperimentalBinCommitmentFlag.Name, statecfg.ExperimentalBinCommitment, utils.ExperimentalBinCommitmentFlag.Usage) + cmd.Flags().StringVar(&statecfg.BinCommitmentHash, utils.ExperimentalBinCommitmentHashFlag.Name, statecfg.BinCommitmentHash, utils.ExperimentalBinCommitmentHashFlag.Usage) } func withBatchSize(cmd *cobra.Command) { diff --git a/cmd/utils/flags.go b/cmd/utils/flags.go index 37023e123b7..0b8bc001329 100644 --- a/cmd/utils/flags.go +++ b/cmd/utils/flags.go @@ -60,6 +60,7 @@ import ( "github.com/erigontech/erigon/execution/builder/buildercfg" "github.com/erigontech/erigon/execution/chain/networkname" chainspec "github.com/erigontech/erigon/execution/chain/spec" + "github.com/erigontech/erigon/execution/commitment" "github.com/erigontech/erigon/execution/protocol/params" "github.com/erigontech/erigon/execution/protocol/rules/ethash/ethashcfg" "github.com/erigontech/erigon/execution/state/genesiswrite" @@ -1123,6 +1124,15 @@ var ( Usage: "EXPERIMENTAL: enables the EIP-8297 binary commitment trie. Takes effect on a fresh datadir only and is persisted there.", Value: false, } + // ExperimentalBinCommitmentHashFlag picks H for the binary trie. Persisted and + // adopted like the variant itself: roots do not survive a change. + ExperimentalBinCommitmentHashFlag = cli.StringFlag{ + Name: "experimental.bin-commitment.hash", + Usage: "EXPERIMENTAL: hash for the EIP-8297 binary commitment trie: \"keccak\" (default) or \"blake3\". blake3 matches the execution-specs reference and the other clients on the binary-trie testnets. Takes effect on a fresh datadir only and is persisted there.", + // Empty, not "keccak": an unset flag must stay distinguishable from an + // explicit one, which a hex datadir refuses. + Value: "", + } GDBMeFlag = cli.BoolFlag{ Name: "gdbme", Usage: "restart erigon under gdb for debug purposes", @@ -2024,6 +2034,16 @@ func SetEthConfig(nodeCtx context.Context, ctx *cli.Command, nodeConfig *nodecfg statecfg.ExperimentalBinCommitment = true } + if h := ctx.String(ExperimentalBinCommitmentHashFlag.Name); h != "" { + if err := commitment.SetPBinHashSuite(h); err != nil { + Fatalf("%v", err) + } + // Genesis is computed here too, so the suite has to be live before the + // datadir reconciles it. + cfg.BinCommitmentHash = h + statecfg.BinCommitmentHash = h + } + cfg.FcuTimeout = ctx.Duration(FcuTimeoutFlag.Name) cfg.FcuBackgroundPrune = ctx.Bool(FcuBackgroundPruneFlag.Name) cfg.FcuBackgroundCommit = ctx.Bool(FcuBackgroundCommitFlag.Name) diff --git a/db/state/erigondb_settings.go b/db/state/erigondb_settings.go index bbb018e4cc9..28c3dee779d 100644 --- a/db/state/erigondb_settings.go +++ b/db/state/erigondb_settings.go @@ -13,6 +13,7 @@ import ( "github.com/erigontech/erigon/db/config3" "github.com/erigontech/erigon/db/datadir" "github.com/erigontech/erigon/db/state/statecfg" + "github.com/erigontech/erigon/execution/commitment" ) const ERIGONDB_SETTINGS_FILE = "erigondb.toml" @@ -29,6 +30,9 @@ type ErigonDBSettings struct { // TrieVariant is the commitment trie the datadir was created with ("hex" or // "bin"); absent means hex. Like every erigondb.toml key it wins over the CLI. TrieVariant *string `toml:"trie_variant,omitempty"` + // TrieHash is H for a "bin" datadir ("keccak" or "blake3"); absent means + // keccak. Meaningless under "hex", which has no choice of hash. + TrieHash *string `toml:"trie_hash,omitempty"` } // RefsInCommitmentBranches resolves the commitment "references in branches" regime, @@ -49,6 +53,14 @@ func (s *ErigonDBSettings) TrieVariantName() string { return *s.TrieVariant } +// TrieHashName resolves H for a bin datadir, treating an absent field as Keccak. +func (s *ErigonDBSettings) TrieHashName() string { + if s.TrieHash == nil || *s.TrieHash == "" { + return commitment.PBinHashKeccak + } + return *s.TrieHash +} + // reconcileTrieVariant applies the datadir's trie variant to the process: a bin // datadir turns the bin flag on process-wide, and a combination the bin engine // cannot honour is refused rather than degraded to a wrong-root run. @@ -65,7 +77,23 @@ func reconcileTrieVariant(s *ErigonDBSettings, logger log.Logger) error { logger.Info("datadir uses the bin commitment trie; enabling it for this process") statecfg.ExperimentalBinCommitment = true } + // The stored hash wins over the flag: every root on disk was built with it, + // so honouring a differing flag would silently produce a second tree. + stored := s.TrieHashName() + if statecfg.BinCommitmentHash != "" && statecfg.BinCommitmentHash != stored { + return fmt.Errorf("--experimental.bin-commitment.hash=%s: datadir was built with %q; the bin trie needs a fresh datadir to change hash", + statecfg.BinCommitmentHash, stored) + } + if err := commitment.SetPBinHashSuite(stored); err != nil { + return fmt.Errorf("erigondb.toml: %w", err) + } case TrieVariantHex: + if s.TrieHash != nil { + return errors.New("erigondb.toml: trie_hash is meaningless under trie_variant \"hex\"") + } + if statecfg.BinCommitmentHash != "" { + return errors.New("--experimental.bin-commitment.hash needs --experimental.bin-commitment") + } if statecfg.ExperimentalBinCommitment { return errors.New("--experimental.bin-commitment: datadir was created with the hex commitment trie; the bin trie needs a fresh datadir") } @@ -134,7 +162,7 @@ func ResolveErigonDBSettingsWithRefsDefault(dirs datadir.Dirs, logger log.Logger // snapshot metadata and must not be rewritten. logger.Info("erigondb settings", "step_size", settings.StepSize, "steps_in_frozen_file", settings.StepsInFrozenFile, "references_in_commitment_branches", settings.RefsInCommitmentBranches(), - "trie_variant", settings.TrieVariantName()) + "trie_variant", settings.TrieVariantName(), "trie_hash", settings.TrieHashName()) return settings, nil } @@ -143,10 +171,15 @@ func ResolveErigonDBSettingsWithRefsDefault(dirs datadir.Dirs, logger log.Logger refs = *refsFirstStart } - var trieVariant *string + var trieVariant, trieHash *string if statecfg.ExperimentalBinCommitment { v := TrieVariantBin trieVariant = &v + h := statecfg.BinCommitmentHash + if h == "" { + h = commitment.PBinHashKeccak + } + trieHash = &h } preverifiedExists, err := dir.FileExist(filepath.Join(dirs.Snap, datadir.PreverifiedFileName)) @@ -179,6 +212,7 @@ func ResolveErigonDBSettingsWithRefsDefault(dirs datadir.Dirs, logger log.Logger StepsInFrozenFile: config3.DefaultStepsInFrozenFile, ReferencesInCommitmentBranches: &refs, TrieVariant: trieVariant, + TrieHash: trieHash, } if err := reconcileTrieVariant(settings, logger); err != nil { return nil, err diff --git a/db/state/statecfg/state_schema.go b/db/state/statecfg/state_schema.go index 814a01d149a..63155456798 100644 --- a/db/state/statecfg/state_schema.go +++ b/db/state/statecfg/state_schema.go @@ -213,6 +213,12 @@ var ExperimentalStreamingCommitment = false // starts, so a flagless restart of a bin datadir stays bin. var ExperimentalBinCommitment = dbg.EnvBool("COMMITMENT_BIN", false) +// BinCommitmentHash names H for the binary trie ("keccak" or "blake3", empty +// meaning keccak). Persisted and adopted exactly like ExperimentalBinCommitment: +// roots are incomparable across a change, so a datadir keeps the hash it was +// built with. +var BinCommitmentHash = dbg.EnvString("COMMITMENT_BIN_HASH", "") + var Schema = SchemaGen{ AccountsDomain: DomainCfg{ Name: kv.AccountsDomain, ValuesTable: kv.TblAccountVals, diff --git a/docs/pbin-m1b-smoke.md b/docs/pbin-m1b-smoke.md index 31b4cd63b27..8a6eb572ca4 100644 --- a/docs/pbin-m1b-smoke.md +++ b/docs/pbin-m1b-smoke.md @@ -4,7 +4,9 @@ Record of the M1b gate: a local dev chain booted from genesis on the EIP-8297 bi trie, produced blocks, deployed and called contracts, and resumed after a restart. Binary: `awskii/pbin-patricia`, erigon `v3.7.0-dev`, darwin/arm64. -Hash: Keccak-256 (BLAKE3 is test-only). Roots below agree with no other client. +Hash: Keccak-256, the default. Roots below agree with no other client; reproducing +them elsewhere needs the same flags. `--experimental.bin-commitment.hash=blake3` +selects the hash the execution-specs reference and the other binary-trie clients use. ## Command line diff --git a/execution/commitment/commitment.go b/execution/commitment/commitment.go index 2c06a1aa02b..8d90cb40dcb 100644 --- a/execution/commitment/commitment.go +++ b/execution/commitment/commitment.go @@ -176,7 +176,7 @@ func InitializeTrieAndUpdates(mode Mode, tmpdir string, cfg TrieConfig) (Trie, * // ModeDirect regardless of the argument: the parallel prefix trie is a // hex-nibble structure and the binary key space has no nibbles. trie := NewPBinPatriciaHashed(nil) - tree := NewUpdates(ModeDirect, tmpdir, pbinKeyHasher()) + tree := NewUpdates(ModeDirect, tmpdir, trie.setHashSuite(pbinSelectedSum)) return trie, tree case VariantHexPatriciaTrie: fallthrough diff --git a/execution/commitment/pbin_hash.go b/execution/commitment/pbin_hash.go index 122a98d17f7..d5a90194810 100644 --- a/execution/commitment/pbin_hash.go +++ b/execution/commitment/pbin_hash.go @@ -22,6 +22,7 @@ import ( "fmt" keccak "github.com/erigontech/fastkeccak" + "lukechampine.com/blake3" "github.com/erigontech/erigon/common" "github.com/erigontech/erigon/common/length" @@ -46,12 +47,47 @@ var pbinEmptyTreeHash common.Hash var errPBinCellHash = errors.New("pbin: cell cannot be hashed") // pbinHashFn is H. EIP-8297 leaves the hash open and names Keccak-256 among the -// candidates (eip:511-513); the execution-specs reference hashes with BLAKE3, so -// tests substitute it to compare roots against that reference. Key derivation -// hashes too, so a suite is only fully swapped when pbinDigestCache is swapped -// with it. +// candidates (eip:511-513); the execution-specs reference hashes with BLAKE3. +// Key derivation hashes too, so a suite is only fully swapped when +// pbinDigestCache is swapped with it. type pbinHashFn func([]byte) common.Hash +// Names for H, as the --experimental.bin-commitment.hash flag spells them. +const ( + PBinHashKeccak = "keccak" + PBinHashBlake3 = "blake3" +) + +// pbinSelectedSum is H for every binary-trie engine this process builds; nil is +// Keccak-256. Roots are not comparable across a change, so the datadir persists +// the choice and refuses to reopen under a different one. +var pbinSelectedSum pbinHashFn + +// SetPBinHashSuite selects H by name. Keccak-256 is the default because the EIP +// names it first, but the clients sharing a binary-trie testnet follow the +// execution-specs reference, which hashes BLAKE3 — interoperating means asking +// for it. Call before the first engine is built. +func SetPBinHashSuite(name string) error { + switch name { + case "", PBinHashKeccak: + pbinSelectedSum = nil + case PBinHashBlake3: + pbinSelectedSum = func(b []byte) common.Hash { return common.Hash(blake3.Sum256(b)) } + default: + return fmt.Errorf("unknown bin commitment hash %q, want %q or %q", name, PBinHashKeccak, PBinHashBlake3) + } + return nil +} + +// PBinHashSuiteName reports the selected suite, for logging and for the value +// the datadir persists. +func PBinHashSuiteName() string { + if pbinSelectedSum == nil { + return PBinHashKeccak + } + return PBinHashBlake3 +} + // pbinHasher applies H to node preimages. Every preimage fits its single scratch // buffer, so each node costs one hash call and no allocation. Its zero value is // ready and hashes with Keccak-256. diff --git a/execution/commitment/pbin_hashsuite_test.go b/execution/commitment/pbin_hashsuite_test.go new file mode 100644 index 00000000000..0a2a4aeb46b --- /dev/null +++ b/execution/commitment/pbin_hashsuite_test.go @@ -0,0 +1,137 @@ +// Copyright 2026 The Erigon Authors +// This file is part of Erigon. +// +// Erigon is free software: you can redistribute it and/or modify +// it under the terms of the GNU Lesser General Public License as published by +// the Free Software Foundation, either version 3 of the License, or +// (at your option) any later version. +// +// Erigon is distributed in the hope that it will be useful, +// but WITHOUT ANY WARRANTY; without even the implied warranty of +// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +// GNU Lesser General Public License for more details. +// +// You should have received a copy of the GNU Lesser General Public License +// along with Erigon. If not, see . + +package commitment + +import ( + "encoding/hex" + "sort" + "testing" + + "github.com/stretchr/testify/require" +) + +// These tests move the process-wide hash selection, so none of them is parallel. + +func pbinRestoreHashSuite(t *testing.T) { + t.Helper() + prev := PBinHashSuiteName() + t.Cleanup(func() { require.NoError(t, SetPBinHashSuite(prev)) }) +} + +func TestPBinSetHashSuite(t *testing.T) { + pbinRestoreHashSuite(t) + + require.NoError(t, SetPBinHashSuite(PBinHashBlake3)) + require.Equal(t, PBinHashBlake3, PBinHashSuiteName()) + + require.NoError(t, SetPBinHashSuite(PBinHashKeccak)) + require.Equal(t, PBinHashKeccak, PBinHashSuiteName()) + + // An absent setting is the Keccak default, not an error. + require.NoError(t, SetPBinHashSuite("")) + require.Equal(t, PBinHashKeccak, PBinHashSuiteName()) + + require.Error(t, SetPBinHashSuite("sha256")) + require.Equal(t, PBinHashKeccak, PBinHashSuiteName(), "a rejected name must not change the suite") +} + +// TestPBinInitializeTrieAppliesHashSuite pins that the selection reaches both +// seams through the production constructor: an engine whose node hashing and +// key derivation disagreed would build a tree no one can reproduce. +func TestPBinInitializeTrieAppliesHashSuite(t *testing.T) { + pbinRestoreHashSuite(t) + + for _, tc := range []struct { + name string + wantSame bool + }{ + {PBinHashKeccak, false}, + {PBinHashBlake3, true}, + } { + t.Run(tc.name, func(t *testing.T) { + require.NoError(t, SetPBinHashSuite(tc.name)) + trie, tree := InitializeTrieAndUpdates(ModeDirect, t.TempDir(), TrieConfig{Variant: VariantBinPatriciaTrie}) + pph, ok := trie.(*PBinPatriciaHashed) + require.True(t, ok) + defer pph.Release() + + require.Equal(t, tc.wantSame, pph.hasher.sum != nil, "node hashing seam") + require.Equal(t, tc.wantSame, pph.keyDigest.sum != nil, "key derivation seam") + + // The buffer's hasher has to derive the same key the engine will look for. + addr := make([]byte, 20) + addr[19] = 1 + require.Equal(t, pbinKeyHasherWith(pph.hasher.sum)(addr), tree.hasher(addr)) + }) + } +} + +// TestPBinBlake3SuiteMatchesSpecRoots is the interop check: with BLAKE3 selected +// the way a node selects it, the engine reproduces the reference implementation's +// roots. Under the Keccak default the same vectors must NOT match — otherwise the +// selection is not reaching the engine and the test proves nothing. +func TestPBinBlake3SuiteMatchesSpecRoots(t *testing.T) { + pbinRestoreHashSuite(t) + v := pbinLoadRootVectors(t) + require.NotEmpty(t, v.Trie) + + rootOf := func(t *testing.T, tc int) string { + t.Helper() + leaves := make([]pbinEngineLeaf, 0, len(v.Trie[tc].Entries)) + for i, e := range v.Trie[tc].Entries { + key, err := hex.DecodeString(e.Key[2:]) + require.NoError(t, err) + val, err := hex.DecodeString(e.Value[2:]) + require.NoError(t, err) + l, ok := pbinLeafFromVector(key, val, i+1) + require.True(t, ok) + leaves = append(leaves, l) + } + sort.Slice(leaves, func(i, j int) bool { + return string(leaves[i].treeKey) < string(leaves[j].treeKey) + }) + + trie, _ := InitializeTrieAndUpdates(ModeDirect, t.TempDir(), TrieConfig{Variant: VariantBinPatriciaTrie}) + pph := trie.(*PBinPatriciaHashed) + defer pph.Release() + pph.ResetContext(NewMockState(t)) + + for i := range leaves { + require.NoError(t, pph.followAndUpdate(leaves[i].treeKey, leaves[i].plainKey, &leaves[i].update)) + } + for pph.grid.activeRows > 0 { + require.NoError(t, pph.fold()) + } + require.NoError(t, pph.storeRoot()) + got, err := pph.RootHash() + require.NoError(t, err) + return hex.EncodeToString(got) + } + + for i, tc := range v.Trie { + t.Run(tc.Name, func(t *testing.T) { + require.NoError(t, SetPBinHashSuite(PBinHashBlake3)) + require.Equal(t, tc.Root[2:], rootOf(t, i)) + + if len(tc.Entries) == 0 { + return // the empty tree is 32 zero bytes under any hash (eip:208) + } + require.NoError(t, SetPBinHashSuite(PBinHashKeccak)) + require.NotEqual(t, tc.Root[2:], rootOf(t, i), "keccak must not reproduce a blake3 reference root") + }) + } +} diff --git a/execution/commitment/pbin_keys.go b/execution/commitment/pbin_keys.go index c0b593d5d49..88a71109e06 100644 --- a/execution/commitment/pbin_keys.go +++ b/execution/commitment/pbin_keys.go @@ -132,7 +132,7 @@ func pbinTreeKeyCodeOverflow(codeHash common.Hash, chunkID int) []byte { // from, so borrowing another goroutine's cache stays correct. func pbinKeyHasher() keyHasher { return pbinKeyHasherWith(nil) } -// pbinKeyHasherWith derives keys under sum, nil meaning Keccak-256. Tests swap +// pbinKeyHasherWith derives keys under sum, nil meaning Keccak-256. Callers swap // the hash here and on node hashing together through setHashSuite. func pbinKeyHasherWith(sum pbinHashFn) keyHasher { var pool sync.Pool diff --git a/execution/commitment/pbin_patricia_hashed.go b/execution/commitment/pbin_patricia_hashed.go index f604f93937c..beab6d4516c 100644 --- a/execution/commitment/pbin_patricia_hashed.go +++ b/execution/commitment/pbin_patricia_hashed.go @@ -16,12 +16,12 @@ // PBinPatriciaHashed — commitment over EIP-8297's partitioned binary tree. // -// The EIP leaves its hash function open and names Keccak-256 as a candidate; -// this engine uses Keccak-256 both for node hashing and for tree-key derivation. -// BLAKE3 is a test-only override, set on both seams at once by setHashSuite so -// the reference vectors can be replayed. It buys comparability with the -// reference implementation, never agreement with another client — no client -// would agree with a Keccak-keyed binary tree. +// The EIP leaves its hash function open and names Keccak-256 as a candidate, +// which is this engine's default for both node hashing and tree-key derivation. +// BLAKE3 is the alternative, selected by SetPBinHashSuite and applied to both +// seams at once. It is what the execution-specs reference and the other clients +// on the shared binary-trie testnets hash with, so a node that has to agree with +// them runs on BLAKE3; a Keccak-keyed tree agrees with no other client. // // Scope: Process over all three zones, ModeDirect only. Code is chunked into the // account header's chunk leaves, overflowing into the code zone where chunks are @@ -133,7 +133,7 @@ func (pph *PBinPatriciaHashed) Reset() { // setHashSuite swaps H on both seams at once — node hashing on this engine and // the returned key-derivation hasher — so neither can be configured without the -// other. Production never calls it: the nil default is Keccak-256 on both. +// other. nil is Keccak-256 on both. func (pph *PBinPatriciaHashed) setHashSuite(sum pbinHashFn) keyHasher { pph.hasher.sum = sum pph.keyDigest = pbinDigestCache{sum: sum} @@ -181,13 +181,16 @@ var pbinRootKey = []byte{0x08} // HashSort hands keys over in tree-key order, which is descent order, so the // grid only ever walks the path between two consecutive keys. // -// M0 ignores warmup: the engine runs against an in-memory context, so there is -// no page cache to pre-warm. +// warmup is ignored: the engine has no parallel read path to pre-warm for. func (pph *PBinPatriciaHashed) Process(ctx context.Context, updates *Updates, logPrefix string, onProgress func(*CommitProgress), warmup WarmupConfig) ([]byte, error) { var processed uint64 // Each run is its own ascending stream: the grid is back at the root, so the - // key the previous run ended on bounds nothing. + // key the previous run ended on bounds nothing. The code buffers are cleared + // for the same reason — a run that failed mid-stream leaves chunks queued, + // and the next run must not emit them against its own keys. pph.lastKeyLen = 0 + pph.pendingCode = pbinPendingCode{} + pph.overflowCode = pph.overflowCode[:0] err := updates.HashSort(ctx, nil, func(treeKey, plainKey []byte, stateUpdate *Update) error { if err := pph.processKey(treeKey, plainKey, stateUpdate); err != nil { return err @@ -880,6 +883,7 @@ func (pph *PBinPatriciaHashed) fold() error { return err } g.activeRows-- + g.prevRecord[row] = nil pph.currentKey.truncate(max(upDepth-1, 0)) return nil } diff --git a/node/cli/default_flags.go b/node/cli/default_flags.go index 0a8371efcbf..1ca0e966b43 100644 --- a/node/cli/default_flags.go +++ b/node/cli/default_flags.go @@ -269,6 +269,7 @@ var DefaultFlags = []cli.Flag{ &utils.ExperimentalParallelCommitmentFlag, &utils.ExperimentalStreamingCommitmentFlag, &utils.ExperimentalBinCommitmentFlag, + &utils.ExperimentalBinCommitmentHashFlag, &utils.MCPDisableFlag, &utils.MCPAddrFlag, diff --git a/node/eth/backend.go b/node/eth/backend.go index 8335b6df433..ef23d86a0ad 100644 --- a/node/eth/backend.go +++ b/node/eth/backend.go @@ -76,6 +76,7 @@ import ( "github.com/erigontech/erigon/execution/cache" "github.com/erigontech/erigon/execution/chain" chainspec "github.com/erigontech/erigon/execution/chain/spec" + "github.com/erigontech/erigon/execution/commitment" "github.com/erigontech/erigon/execution/engineapi" "github.com/erigontech/erigon/execution/engineapi/engine_block_downloader" "github.com/erigontech/erigon/execution/exec" @@ -316,6 +317,12 @@ func New(ctx context.Context, stack *node.Node, config *ethconfig.Config, logger if config.ExperimentalBinCommitment { statecfg.ExperimentalBinCommitment = true } + if config.BinCommitmentHash != "" { + if err = commitment.SetPBinHashSuite(config.BinCommitmentHash); err != nil { + return err + } + statecfg.BinCommitmentHash = config.BinCommitmentHash + } if err = stages.UpdateMetrics(tx); err != nil { return err diff --git a/node/ethconfig/config.go b/node/ethconfig/config.go index d68e60c2de1..2988f6d6d24 100644 --- a/node/ethconfig/config.go +++ b/node/ethconfig/config.go @@ -326,6 +326,7 @@ type Sync struct { ExperimentalParallelCommitment bool ExperimentalStreamingCommitment bool ExperimentalBinCommitment bool + BinCommitmentHash string PersistReceiptsCacheV2 bool SnapshotDownloadToBlock uint64 // exclusive [0,toBlock) } From e835693e40d6dec98bd7ede4dc4ed10f5131b635 Mon Sep 17 00:00:00 2001 From: awskii Date: Fri, 31 Jul 2026 20:27:07 +0700 Subject: [PATCH 42/56] execution/commitment: isolate pbin update stream --- execution/commitment/pbin_hashsuite_test.go | 2 +- execution/commitment/pbin_patricia_hashed.go | 239 +----------------- execution/commitment/pbin_update_stream.go | 247 +++++++++++++++++++ 3 files changed, 253 insertions(+), 235 deletions(-) create mode 100644 execution/commitment/pbin_update_stream.go diff --git a/execution/commitment/pbin_hashsuite_test.go b/execution/commitment/pbin_hashsuite_test.go index 0a2a4aeb46b..0e03de15fcb 100644 --- a/execution/commitment/pbin_hashsuite_test.go +++ b/execution/commitment/pbin_hashsuite_test.go @@ -70,7 +70,7 @@ func TestPBinInitializeTrieAppliesHashSuite(t *testing.T) { defer pph.Release() require.Equal(t, tc.wantSame, pph.hasher.sum != nil, "node hashing seam") - require.Equal(t, tc.wantSame, pph.keyDigest.sum != nil, "key derivation seam") + require.Equal(t, tc.wantSame, pph.updateStream.keyDigest.sum != nil, "key derivation seam") // The buffer's hasher has to derive the same key the engine will look for. addr := make([]byte, 20) diff --git a/execution/commitment/pbin_patricia_hashed.go b/execution/commitment/pbin_patricia_hashed.go index beab6d4516c..d98172816b0 100644 --- a/execution/commitment/pbin_patricia_hashed.go +++ b/execution/commitment/pbin_patricia_hashed.go @@ -46,7 +46,6 @@ import ( "fmt" "io" "math/bits" - "slices" "sync" "github.com/erigontech/erigon/common" @@ -64,15 +63,7 @@ type PBinPatriciaHashed struct { hasher pbinHasher branchEncoder pbinBranchEncoder counters pbinCounters - - siblingKey [pbinAccountKeyLength]byte // scratch for the CODE_HASH key of the account being visited - pendingCode pbinPendingCode - // overflowCode holds the run's code-zone chunks. They are content-addressed - // rather than keyed by address, so they neither follow the stream's order nor - // belong to the account that produced them, and are emitted as one sorted - // block once the stream leaves the code zone. - overflowCode []pbinOverflowChunk - keyDigest pbinDigestCache + updateStream pbinUpdateStream lastKey [pbinStorageKeyLength]byte // the deepest key visited so far, which the next one must exceed lastKeyLen int16 @@ -126,8 +117,7 @@ func (pph *PBinPatriciaHashed) Reset() { pph.currentKey = pbinBitpath{} pph.rootChecked, pph.rootTouched, pph.rootPresent = false, false, false pph.rootPrev = nil - pph.pendingCode = pbinPendingCode{} - pph.overflowCode = pph.overflowCode[:0] + pph.updateStream.reset() pph.lastKeyLen = 0 } @@ -136,7 +126,7 @@ func (pph *PBinPatriciaHashed) Reset() { // other. nil is Keccak-256 on both. func (pph *PBinPatriciaHashed) setHashSuite(sum pbinHashFn) keyHasher { pph.hasher.sum = sum - pph.keyDigest = pbinDigestCache{sum: sum} + pph.updateStream.keyDigest = pbinDigestCache{sum: sum} return pbinKeyHasherWith(sum) } @@ -146,7 +136,7 @@ func (pph *PBinPatriciaHashed) Release() { pph.ctx = nil pph.traceW = nil pph.hasher.sum = nil - pph.keyDigest = pbinDigestCache{} + pph.updateStream.release() pph.counters = pbinCounters{} pph.branchEncoder.buf = pph.branchEncoder.buf[:0] pbinPool.Put(pph) @@ -158,13 +148,6 @@ var ( errPBinVisitOrder = errors.New("pbin: visit order is not ascending") ) -// pbinCodeContext is the read code chunking needs. PatriciaContext hands out -// account state, not the bytecode the chunk leaves hold, so a context that -// cannot serve code cannot commit a code-bearing account. -type pbinCodeContext interface { - Code(plainKey []byte) ([]byte, error) -} - // ErrPBinUnsupported marks a code path only the hex trie implements. Callers // wrap it with the path name so the bin variant refuses instead of no-opping. var ErrPBinUnsupported = errors.New("pbin: unsupported under the bin commitment variant") @@ -183,30 +166,11 @@ var pbinRootKey = []byte{0x08} // // warmup is ignored: the engine has no parallel read path to pre-warm for. func (pph *PBinPatriciaHashed) Process(ctx context.Context, updates *Updates, logPrefix string, onProgress func(*CommitProgress), warmup WarmupConfig) ([]byte, error) { - var processed uint64 - // Each run is its own ascending stream: the grid is back at the root, so the - // key the previous run ended on bounds nothing. The code buffers are cleared - // for the same reason — a run that failed mid-stream leaves chunks queued, - // and the next run must not emit them against its own keys. pph.lastKeyLen = 0 - pph.pendingCode = pbinPendingCode{} - pph.overflowCode = pph.overflowCode[:0] - err := updates.HashSort(ctx, nil, func(treeKey, plainKey []byte, stateUpdate *Update) error { - if err := pph.processKey(treeKey, plainKey, stateUpdate); err != nil { - return err - } - processed++ - return nil - }) + processed, err := pph.updateStream.process(ctx, updates, pph.ctx, pph.followAndUpdate) if err != nil { return nil, fmt.Errorf("pbin: process %s: %w", logPrefix, err) } - if err = pph.flushPendingCode(); err != nil { - return nil, fmt.Errorf("pbin: process %s: %w", logPrefix, err) - } - if err = pph.flushOverflowCode(); err != nil { - return nil, fmt.Errorf("pbin: process %s: %w", logPrefix, err) - } for pph.grid.activeRows > 0 { if err = pph.fold(); err != nil { return nil, fmt.Errorf("pbin: final fold: %w", err) @@ -225,199 +189,6 @@ func (pph *PBinPatriciaHashed) Process(ctx context.Context, updates *Updates, lo return pph.RootHash() } -// processKey routes one update into the tree. An account fans out to two leaves -// visited back to back — BASIC_DATA and the CODE_HASH sibling at the next -// sub-index — which is what lets the shared keyHasher stay a one-key function. -// Its code chunks sit at the top of the same stem and are held back until the -// stream leaves it. -func (pph *PBinPatriciaHashed) processKey(treeKey, plainKey []byte, stateUpdate *Update) error { - if stateUpdate != nil && stateUpdate.Deleted() { - return fmt.Errorf("%w: update for %x", errPBinDeleteUnsupported, plainKey) - } - if err := pph.flushPendingCodeBefore(treeKey); err != nil { - return err - } - if err := pph.flushOverflowCodeBefore(treeKey); err != nil { - return err - } - update := stateUpdate - if update == nil { - var err error - if update, err = pph.stateOf(plainKey); err != nil { - return err - } - } - if err := pph.followAndUpdate(treeKey, plainKey, update); err != nil { - return err - } - if len(plainKey) != length.Addr { - return nil - } - codeKey, err := pph.codeHashKey(treeKey) - if err != nil { - return err - } - if err = pph.followAndUpdate(codeKey, plainKey, update); err != nil { - return err - } - return pph.queueCode(treeKey, plainKey, update) -} - -// pbinPendingCode is one account's code fan-out, waiting for the stream to leave -// its stem. Chunks occupy the header's top sub-indices, so emitting them at the -// account's own visit would descend past a header storage slot the stream has not -// delivered yet, and coming back for it would rewrite a record the fold had -// already written. -type pbinPendingCode struct { - stem [pbinAccountKeyLength - 1]byte - plainKey [length.Addr]byte - chunks [][pbinValueLength]byte -} - -// queueCode reads the account's code and holds its chunks until the stem is done. -// The size the BASIC_DATA leaf hashes and the code the chunks come from are two -// reads, so they are checked against each other rather than trusted apart. -func (pph *PBinPatriciaHashed) queueCode(basicDataKey, plainKey []byte, update *Update) error { - if update.CodeSize == 0 { - return nil - } - if len(pph.pendingCode.chunks) != 0 { - return fmt.Errorf("pbin: code for %x queued while %x is still pending: the stem exit was missed", - plainKey, pph.pendingCode.plainKey[:]) - } - code, err := pph.codeOf(plainKey) - if err != nil { - return err - } - if uint64(len(code)) != update.CodeSize { - return fmt.Errorf("pbin: account %x says %d code bytes, the code domain holds %d", - plainKey, update.CodeSize, len(code)) - } - chunks := pbinChunkifyCode(code) - if len(chunks) > pbinHeaderCodeChunks { - for i := pbinHeaderCodeChunks; i < len(chunks); i++ { - var oc pbinOverflowChunk - copy(oc.key[:], pph.keyDigest.codeOverflowKey(update.CodeHash, i)) - oc.value = chunks[i] - pph.overflowCode = append(pph.overflowCode, oc) - } - chunks = chunks[:pbinHeaderCodeChunks] - } - pph.pendingCode.chunks = chunks - copy(pph.pendingCode.stem[:], basicDataKey) - copy(pph.pendingCode.plainKey[:], plainKey) - return nil -} - -// pbinOverflowChunk is one code-zone chunk waiting for its block to be emitted. -type pbinOverflowChunk struct { - key [pbinCodeKeyLength]byte - value [pbinValueLength]byte -} - -// flushOverflowCodeBefore emits the code-zone block once the stream reaches a -// zone above it. Nothing the stream carries is a code-zone key — the zone is -// content-addressed and no plain key derives into it — so the block is written -// whole, between the last account-header key and the first storage one. -func (pph *PBinPatriciaHashed) flushOverflowCodeBefore(treeKey []byte) error { - if len(pph.overflowCode) == 0 || treeKey[0] <= pbinCodeZone { - return nil - } - return pph.flushOverflowCode() -} - -func (pph *PBinPatriciaHashed) flushOverflowCode() error { - if len(pph.overflowCode) == 0 { - return nil - } - slices.SortFunc(pph.overflowCode, func(a, b pbinOverflowChunk) int { return bytes.Compare(a.key[:], b.key[:]) }) - - var prev *pbinOverflowChunk - for i := range pph.overflowCode { - oc := &pph.overflowCode[i] - // Accounts running the same bytecode share leaves (eip:352-354), so the same - // key twice is one chunk two accounts asked for, not a conflict. - if prev != nil && oc.key == prev.key { - if oc.value != prev.value { - return fmt.Errorf("pbin: code chunk %x carries two values", oc.key[:]) - } - continue - } - update := Update{Flags: StorageUpdate, StorageLen: pbinValueLength, Storage: oc.value} - if err := pph.followAndUpdate(oc.key[:], nil, &update); err != nil { - return err - } - prev = oc - } - pph.overflowCode = pph.overflowCode[:0] - return nil -} - -func (pph *PBinPatriciaHashed) codeOf(plainKey []byte) ([]byte, error) { - ctx, ok := pph.ctx.(pbinCodeContext) - if !ok { - return nil, fmt.Errorf("%w: %T serves no code, needed to chunk account %x", - ErrPBinUnsupported, pph.ctx, plainKey) - } - code, err := ctx.Code(plainKey) - if err != nil { - return nil, fmt.Errorf("pbin: read code %x: %w", plainKey, err) - } - return code, nil -} - -// flushPendingCodeBefore emits the held-back chunks when treeKey leaves their -// stem. Chunk sub-indices are the highest in a stem, so a stem the stream has -// left is a stem no key can return to. -func (pph *PBinPatriciaHashed) flushPendingCodeBefore(treeKey []byte) error { - if len(pph.pendingCode.chunks) == 0 || bytes.HasPrefix(treeKey, pph.pendingCode.stem[:]) { - return nil - } - return pph.flushPendingCode() -} - -func (pph *PBinPatriciaHashed) flushPendingCode() error { - p := &pph.pendingCode - var key [pbinAccountKeyLength]byte - copy(key[:], p.stem[:]) - for i := range p.chunks { - key[pbinAccountKeyLength-1] = byte(pbinCodeOffset + i) - update := Update{Flags: StorageUpdate, StorageLen: pbinValueLength, Storage: p.chunks[i]} - if err := pph.followAndUpdate(key[:], nil, &update); err != nil { - return err - } - } - p.chunks = nil - return nil -} - -func (pph *PBinPatriciaHashed) stateOf(plainKey []byte) (*Update, error) { - if len(plainKey) == length.Addr { - update, err := pph.ctx.Account(plainKey) - if err != nil { - return nil, fmt.Errorf("pbin: read account %x: %w", plainKey, err) - } - return update, nil - } - update, err := pph.ctx.Storage(plainKey) - if err != nil { - return nil, fmt.Errorf("pbin: read storage %x: %w", plainKey, err) - } - return update, nil -} - -// codeHashKey is the CODE_HASH leaf beside a BASIC_DATA key: same stem, next -// sub-index (eip:311-320). The two sit adjacent in key order, so visiting them -// together never walks the descent backwards. -func (pph *PBinPatriciaHashed) codeHashKey(basicDataKey []byte) ([]byte, error) { - if len(basicDataKey) != pbinAccountKeyLength || basicDataKey[pbinAccountKeyLength-1] != pbinBasicDataLeafKey { - return nil, fmt.Errorf("pbin: %x is not a BASIC_DATA key", basicDataKey) - } - copy(pph.siblingKey[:], basicDataKey) - pph.siblingKey[pbinAccountKeyLength-1] = pbinCodeHashLeafKey - return pph.siblingKey[:], nil -} - // followAndUpdate moves the grid onto treeKey and writes the update into the // cell that lands there. // diff --git a/execution/commitment/pbin_update_stream.go b/execution/commitment/pbin_update_stream.go new file mode 100644 index 00000000000..2ce3416800e --- /dev/null +++ b/execution/commitment/pbin_update_stream.go @@ -0,0 +1,247 @@ +// Copyright 2026 The Erigon Authors +// This file is part of Erigon. +// +// Erigon is free software: you can redistribute it and/or modify +// it under the terms of the GNU Lesser General Public License as published by +// the Free Software Foundation, either version 3 of the License, or +// (at your option) any later version. +// +// Erigon is distributed in the hope that it will be useful, +// but WITHOUT ANY WARRANTY; without even the implied warranty of +// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +// GNU Lesser General Public License for more details. +// +// You should have received a copy of the GNU Lesser General Public License +// along with Erigon. If not, see . + +package commitment + +import ( + "bytes" + "context" + "fmt" + "slices" + + "github.com/erigontech/erigon/common/length" +) + +type pbinUpdateSink func(treeKey, plainKey []byte, update *Update) error + +type pbinUpdateStream struct { + state PatriciaContext + emit pbinUpdateSink + + siblingKey [pbinAccountKeyLength]byte + pendingCode pbinPendingCode + overflowCode []pbinOverflowChunk + keyDigest pbinDigestCache +} + +type pbinPendingCode struct { + stem [pbinAccountKeyLength - 1]byte + plainKey [length.Addr]byte + chunks [][pbinValueLength]byte +} + +type pbinOverflowChunk struct { + key [pbinCodeKeyLength]byte + value [pbinValueLength]byte +} + +type pbinCodeContext interface { + Code(plainKey []byte) ([]byte, error) +} + +func (s *pbinUpdateStream) process(ctx context.Context, updates *Updates, state PatriciaContext, emit pbinUpdateSink) (uint64, error) { + s.reset() + s.state, s.emit = state, emit + defer func() { s.state, s.emit = nil, nil }() + + var processed uint64 + err := updates.HashSort(ctx, nil, func(treeKey, plainKey []byte, stateUpdate *Update) error { + if err := s.processKey(treeKey, plainKey, stateUpdate); err != nil { + return err + } + processed++ + return nil + }) + if err != nil { + return processed, err + } + if err = s.flushPendingCode(); err != nil { + return processed, err + } + if err = s.flushOverflowCode(); err != nil { + return processed, err + } + return processed, nil +} + +func (s *pbinUpdateStream) reset() { + s.state, s.emit = nil, nil + s.pendingCode = pbinPendingCode{} + s.overflowCode = s.overflowCode[:0] +} + +func (s *pbinUpdateStream) release() { + s.reset() + s.keyDigest = pbinDigestCache{} +} + +// processKey expands an account into its basic-data and code-hash leaves. Code +// chunks are delayed until emitting them cannot move the ordered trie walk back. +func (s *pbinUpdateStream) processKey(treeKey, plainKey []byte, stateUpdate *Update) error { + if stateUpdate != nil && stateUpdate.Deleted() { + return fmt.Errorf("%w: update for %x", errPBinDeleteUnsupported, plainKey) + } + if err := s.flushPendingCodeBefore(treeKey); err != nil { + return err + } + if err := s.flushOverflowCodeBefore(treeKey); err != nil { + return err + } + update := stateUpdate + if update == nil { + var err error + if update, err = s.stateOf(plainKey); err != nil { + return err + } + } + if err := s.emit(treeKey, plainKey, update); err != nil { + return err + } + if len(plainKey) != length.Addr { + return nil + } + codeKey, err := s.codeHashKey(treeKey) + if err != nil { + return err + } + if err = s.emit(codeKey, plainKey, update); err != nil { + return err + } + return s.queueCode(treeKey, plainKey, update) +} + +func (s *pbinUpdateStream) queueCode(basicDataKey, plainKey []byte, update *Update) error { + if update.CodeSize == 0 { + return nil + } + if len(s.pendingCode.chunks) != 0 { + return fmt.Errorf("pbin: code for %x queued while %x is still pending: the stem exit was missed", + plainKey, s.pendingCode.plainKey[:]) + } + code, err := s.codeOf(plainKey) + if err != nil { + return err + } + if uint64(len(code)) != update.CodeSize { + return fmt.Errorf("pbin: account %x says %d code bytes, the code domain holds %d", + plainKey, update.CodeSize, len(code)) + } + chunks := pbinChunkifyCode(code) + if len(chunks) > pbinHeaderCodeChunks { + for i := pbinHeaderCodeChunks; i < len(chunks); i++ { + var oc pbinOverflowChunk + copy(oc.key[:], s.keyDigest.codeOverflowKey(update.CodeHash, i)) + oc.value = chunks[i] + s.overflowCode = append(s.overflowCode, oc) + } + chunks = chunks[:pbinHeaderCodeChunks] + } + s.pendingCode.chunks = chunks + copy(s.pendingCode.stem[:], basicDataKey) + copy(s.pendingCode.plainKey[:], plainKey) + return nil +} + +func (s *pbinUpdateStream) flushOverflowCodeBefore(treeKey []byte) error { + if len(s.overflowCode) == 0 || treeKey[0] <= pbinCodeZone { + return nil + } + return s.flushOverflowCode() +} + +func (s *pbinUpdateStream) flushOverflowCode() error { + if len(s.overflowCode) == 0 { + return nil + } + slices.SortFunc(s.overflowCode, func(a, b pbinOverflowChunk) int { return bytes.Compare(a.key[:], b.key[:]) }) + + var prev *pbinOverflowChunk + for i := range s.overflowCode { + oc := &s.overflowCode[i] + if prev != nil && oc.key == prev.key { + if oc.value != prev.value { + return fmt.Errorf("pbin: code chunk %x carries two values", oc.key[:]) + } + continue + } + update := Update{Flags: StorageUpdate, StorageLen: pbinValueLength, Storage: oc.value} + if err := s.emit(oc.key[:], nil, &update); err != nil { + return err + } + prev = oc + } + s.overflowCode = s.overflowCode[:0] + return nil +} + +func (s *pbinUpdateStream) codeOf(plainKey []byte) ([]byte, error) { + ctx, ok := s.state.(pbinCodeContext) + if !ok { + return nil, fmt.Errorf("%w: %T serves no code, needed to chunk account %x", + ErrPBinUnsupported, s.state, plainKey) + } + code, err := ctx.Code(plainKey) + if err != nil { + return nil, fmt.Errorf("pbin: read code %x: %w", plainKey, err) + } + return code, nil +} + +func (s *pbinUpdateStream) flushPendingCodeBefore(treeKey []byte) error { + if len(s.pendingCode.chunks) == 0 || bytes.HasPrefix(treeKey, s.pendingCode.stem[:]) { + return nil + } + return s.flushPendingCode() +} + +func (s *pbinUpdateStream) flushPendingCode() error { + p := &s.pendingCode + var key [pbinAccountKeyLength]byte + copy(key[:], p.stem[:]) + for i := range p.chunks { + key[pbinAccountKeyLength-1] = byte(pbinCodeOffset + i) + update := Update{Flags: StorageUpdate, StorageLen: pbinValueLength, Storage: p.chunks[i]} + if err := s.emit(key[:], nil, &update); err != nil { + return err + } + } + p.chunks = nil + return nil +} + +func (s *pbinUpdateStream) stateOf(plainKey []byte) (*Update, error) { + if len(plainKey) == length.Addr { + update, err := s.state.Account(plainKey) + if err != nil { + return nil, fmt.Errorf("pbin: read account %x: %w", plainKey, err) + } + return update, nil + } + update, err := s.state.Storage(plainKey) + if err != nil { + return nil, fmt.Errorf("pbin: read storage %x: %w", plainKey, err) + } + return update, nil +} + +func (s *pbinUpdateStream) codeHashKey(basicDataKey []byte) ([]byte, error) { + if len(basicDataKey) != pbinAccountKeyLength || basicDataKey[pbinAccountKeyLength-1] != pbinBasicDataLeafKey { + return nil, fmt.Errorf("pbin: %x is not a BASIC_DATA key", basicDataKey) + } + copy(s.siblingKey[:], basicDataKey) + s.siblingKey[pbinAccountKeyLength-1] = pbinCodeHashLeafKey + return s.siblingKey[:], nil +} From ddfb55364263f9d1ee28ff46f4250aee9b90fb93 Mon Sep 17 00:00:00 2001 From: awskii Date: Fri, 31 Jul 2026 20:51:36 +0700 Subject: [PATCH 43/56] execution/commitment: consolidate pbin spec vector tests --- execution/commitment/pbin_hashsuite_test.go | 38 ++------ execution/commitment/pbin_specengine_test.go | 88 ++++++------------- execution/commitment/pbin_specroots_test.go | 38 +------- execution/commitment/pbin_specvectors_test.go | 22 ++++- 4 files changed, 56 insertions(+), 130 deletions(-) diff --git a/execution/commitment/pbin_hashsuite_test.go b/execution/commitment/pbin_hashsuite_test.go index 0e03de15fcb..e1fbd85da85 100644 --- a/execution/commitment/pbin_hashsuite_test.go +++ b/execution/commitment/pbin_hashsuite_test.go @@ -17,8 +17,6 @@ package commitment import ( - "encoding/hex" - "sort" "testing" "github.com/stretchr/testify/require" @@ -86,52 +84,28 @@ func TestPBinInitializeTrieAppliesHashSuite(t *testing.T) { // selection is not reaching the engine and the test proves nothing. func TestPBinBlake3SuiteMatchesSpecRoots(t *testing.T) { pbinRestoreHashSuite(t) - v := pbinLoadRootVectors(t) + v := pbinLoadSpecVectors(t) require.NotEmpty(t, v.Trie) - rootOf := func(t *testing.T, tc int) string { + rootOf := func(t *testing.T, tc pbinSpecTrieVector) string { t.Helper() - leaves := make([]pbinEngineLeaf, 0, len(v.Trie[tc].Entries)) - for i, e := range v.Trie[tc].Entries { - key, err := hex.DecodeString(e.Key[2:]) - require.NoError(t, err) - val, err := hex.DecodeString(e.Value[2:]) - require.NoError(t, err) - l, ok := pbinLeafFromVector(key, val, i+1) - require.True(t, ok) - leaves = append(leaves, l) - } - sort.Slice(leaves, func(i, j int) bool { - return string(leaves[i].treeKey) < string(leaves[j].treeKey) - }) - trie, _ := InitializeTrieAndUpdates(ModeDirect, t.TempDir(), TrieConfig{Variant: VariantBinPatriciaTrie}) pph := trie.(*PBinPatriciaHashed) defer pph.Release() pph.ResetContext(NewMockState(t)) - - for i := range leaves { - require.NoError(t, pph.followAndUpdate(leaves[i].treeKey, leaves[i].plainKey, &leaves[i].update)) - } - for pph.grid.activeRows > 0 { - require.NoError(t, pph.fold()) - } - require.NoError(t, pph.storeRoot()) - got, err := pph.RootHash() - require.NoError(t, err) - return hex.EncodeToString(got) + return pbinSpecEngineRoot(t, pph, tc) } - for i, tc := range v.Trie { + for _, tc := range v.Trie { t.Run(tc.Name, func(t *testing.T) { require.NoError(t, SetPBinHashSuite(PBinHashBlake3)) - require.Equal(t, tc.Root[2:], rootOf(t, i)) + require.Equal(t, tc.Root[2:], rootOf(t, tc)) if len(tc.Entries) == 0 { return // the empty tree is 32 zero bytes under any hash (eip:208) } require.NoError(t, SetPBinHashSuite(PBinHashKeccak)) - require.NotEqual(t, tc.Root[2:], rootOf(t, i), "keccak must not reproduce a blake3 reference root") + require.NotEqual(t, tc.Root[2:], rootOf(t, tc), "keccak must not reproduce a blake3 reference root") }) } } diff --git a/execution/commitment/pbin_specengine_test.go b/execution/commitment/pbin_specengine_test.go index 4809c1fd60d..d3df88fd346 100644 --- a/execution/commitment/pbin_specengine_test.go +++ b/execution/commitment/pbin_specengine_test.go @@ -29,9 +29,8 @@ type pbinEngineLeaf struct { } // pbinLeafFromVector maps a raw (key, value) pair onto the Update the engine -// reads for that key's position. ok is false when the position has no Update -// field to carry the value. -func pbinLeafFromVector(key, value []byte, seq int) (pbinEngineLeaf, bool) { +// reads for that key's position. +func pbinLeafFromVector(key, value []byte, seq int) pbinEngineLeaf { var l pbinEngineLeaf l.treeKey = key @@ -57,11 +56,11 @@ func pbinLeafFromVector(key, value []byte, seq int) (pbinEngineLeaf, bool) { if key[0] == pbinStorageZone { storageLeaf() - return l, true + return l } if key[0] == pbinCodeZone { recordLeaf() - return l, true + return l } switch sub := key[len(key)-1]; { case sub == pbinBasicDataLeafKey: @@ -70,75 +69,42 @@ func pbinLeafFromVector(key, value []byte, seq int) (pbinEngineLeaf, bool) { l.update.CodeSize = uint64(binary.BigEndian.Uint32(value[pbinBasicDataCodeSizeOffset:])) l.update.Nonce = binary.BigEndian.Uint64(value[pbinBasicDataNonceOffset:]) l.update.Balance = *new(uint256.Int).SetBytes(value[pbinBasicDataBalanceOffset:]) - return l, true + return l case sub == pbinCodeHashLeafKey: l.plainKey = account l.update.Flags = CodeUpdate l.update.CodeHash = common.BytesToHash(value) - return l, true + return l case sub >= pbinHeaderStorageOffset && sub < pbinCodeOffset: storageLeaf() - return l, true + return l default: recordLeaf() - return l, true + return l } } -func TestPBinEngineMatchesSpecTrieRoots(t *testing.T) { - t.Parallel() - v := pbinLoadRootVectors(t) - - var ran, excluded []string - for _, tc := range v.Trie { - leaves := make([]pbinEngineLeaf, 0, len(tc.Entries)) - representable := true - for i, e := range tc.Entries { - key, err := hex.DecodeString(e.Key[2:]) - require.NoError(t, err) - val, err := hex.DecodeString(e.Value[2:]) - require.NoError(t, err) - l, ok := pbinLeafFromVector(key, val, i+1) - if !ok { - representable = false - break - } - leaves = append(leaves, l) - } - if !representable { - excluded = append(excluded, tc.Name) - continue - } - ran = append(ran, tc.Name) - - t.Run(tc.Name, func(t *testing.T) { - // tree-key order is the engine's visit invariant - sort.Slice(leaves, func(i, j int) bool { - return string(leaves[i].treeKey) < string(leaves[j].treeKey) - }) - - ms := NewMockState(t) - pph := NewPBinPatriciaHashed(ms) - pph.setHashSuite(pbinBlake3Hash) - - for i := range leaves { - require.NoError(t, pph.followAndUpdate(leaves[i].treeKey, leaves[i].plainKey, &leaves[i].update), - "insert %x", leaves[i].treeKey) - } - for pph.grid.activeRows > 0 { - require.NoError(t, pph.fold()) - } - require.NoError(t, pph.storeRoot()) - - got, err := pph.RootHash() - require.NoError(t, err) - require.Equal(t, tc.Root[2:], hex.EncodeToString(got)) - }) +func pbinSpecEngineRoot(t *testing.T, pph *PBinPatriciaHashed, tc pbinSpecTrieVector) string { + t.Helper() + leaves := make([]pbinEngineLeaf, len(tc.Entries)) + for i, e := range tc.Entries { + leaves[i] = pbinLeafFromVector(pbinMustHex(t, e.Key), pbinMustHex(t, e.Value), i+1) } + sort.Slice(leaves, func(i, j int) bool { + return string(leaves[i].treeKey) < string(leaves[j].treeKey) + }) - t.Logf("engine ran %d/%d reference root vectors: %v", len(ran), len(v.Trie), ran) - require.Empty(t, excluded, "every reference root vector must reproduce through the engine") - require.Len(t, ran, len(v.Trie)) + for i := range leaves { + require.NoError(t, pph.followAndUpdate(leaves[i].treeKey, leaves[i].plainKey, &leaves[i].update), + "insert %x", leaves[i].treeKey) + } + for pph.grid.activeRows > 0 { + require.NoError(t, pph.fold()) + } + require.NoError(t, pph.storeRoot()) + got, err := pph.RootHash() + require.NoError(t, err) + return hex.EncodeToString(got) } // TestPBinReleaseClearsHashSuite pins pooling hygiene: a released engine must diff --git a/execution/commitment/pbin_specroots_test.go b/execution/commitment/pbin_specroots_test.go index 81eb33f071a..744ea014ca5 100644 --- a/execution/commitment/pbin_specroots_test.go +++ b/execution/commitment/pbin_specroots_test.go @@ -2,8 +2,6 @@ package commitment import ( "encoding/hex" - "encoding/json" - "os" "sort" "testing" @@ -24,43 +22,11 @@ import ( // The engine itself is tied to this oracle by the differential tests, so the // chain reaches the engine even though the engine hashes with Keccak-256. -type pbinRootVectors struct { - Meta map[string]string `json:"meta"` - EmptyRoot string `json:"empty_root"` - Trie []struct { - Name string `json:"name"` - Entries []struct { - Key string `json:"key"` - Value string `json:"value"` - } `json:"entries"` - Root string `json:"root"` - } `json:"trie_vectors"` - Sequences []struct { - Seed int `json:"seed"` - Ops []struct { - Op string `json:"op"` - Key string `json:"key"` - Value string `json:"value"` - } `json:"ops"` - RootsAfter []string `json:"roots_after"` - } `json:"sequence_vectors"` -} - func pbinBlake3Sum(b []byte) [32]byte { return blake3.Sum256(b) } // pbinBlake3Hash adapts pbinBlake3Sum to the engine's injectable hash seam. var pbinBlake3Hash pbinHashFn = func(b []byte) common.Hash { return common.Hash(blake3.Sum256(b)) } -func pbinLoadRootVectors(t *testing.T) pbinRootVectors { - t.Helper() - raw, err := os.ReadFile("testdata/eip8297_vectors.json") - require.NoError(t, err) - var v pbinRootVectors - require.NoError(t, json.Unmarshal(raw, &v)) - require.Equal(t, "blake3", v.Meta["hasher"], "vectors are only replayable under the hash they were generated with") - return v -} - // pbinOracleRootOf builds the oracle trie from a whole key set and merkelizes it // under BLAKE3. Building from the surviving set is also how a delete is applied: // the EIP's insert has no removal, and the reference's removal semantics are @@ -82,7 +48,7 @@ func pbinOracleRootOf(t *testing.T, entries map[string][]byte) [32]byte { func TestPBinOracleMatchesSpecTrieRoots(t *testing.T) { t.Parallel() - v := pbinLoadRootVectors(t) + v := pbinLoadSpecVectors(t) require.NotEmpty(t, v.Trie) for _, tc := range v.Trie { @@ -106,7 +72,7 @@ func TestPBinOracleMatchesSpecTrieRoots(t *testing.T) { // divergence is pinned to the op that caused it. func TestPBinOracleMatchesSpecSequenceRoots(t *testing.T) { t.Parallel() - v := pbinLoadRootVectors(t) + v := pbinLoadSpecVectors(t) require.NotEmpty(t, v.Sequences) checked := 0 diff --git a/execution/commitment/pbin_specvectors_test.go b/execution/commitment/pbin_specvectors_test.go index 33a1c11ef5d..eaf01b7fce6 100644 --- a/execution/commitment/pbin_specvectors_test.go +++ b/execution/commitment/pbin_specvectors_test.go @@ -33,7 +33,17 @@ type pbinSpecVectors struct { Key string `json:"key"` } `json:"slots"` } `json:"embedding_vectors"` - Chunkify []pbinSpecChunkifyVector `json:"chunkify_vectors"` + Chunkify []pbinSpecChunkifyVector `json:"chunkify_vectors"` + Trie []pbinSpecTrieVector `json:"trie_vectors"` + Sequences []struct { + Seed int `json:"seed"` + Ops []struct { + Op string `json:"op"` + Key string `json:"key"` + Value string `json:"value"` + } `json:"ops"` + RootsAfter []string `json:"roots_after"` + } `json:"sequence_vectors"` } type pbinSpecChunkifyVector struct { @@ -42,12 +52,22 @@ type pbinSpecChunkifyVector struct { Chunks []string `json:"chunks"` } +type pbinSpecTrieVector struct { + Name string `json:"name"` + Entries []struct { + Key string `json:"key"` + Value string `json:"value"` + } `json:"entries"` + Root string `json:"root"` +} + func pbinLoadSpecVectors(t *testing.T) pbinSpecVectors { t.Helper() raw, err := os.ReadFile("testdata/eip8297_vectors.json") require.NoError(t, err) var v pbinSpecVectors require.NoError(t, json.Unmarshal(raw, &v)) + require.Equal(t, "blake3", v.Meta["hasher"], "vectors require their generation hash") return v } From bd98ebbcb16b0d35de97548a2f87cec48c420071 Mon Sep 17 00:00:00 2001 From: awskii Date: Fri, 31 Jul 2026 21:09:14 +0700 Subject: [PATCH 44/56] execution/commitment: drop redundant pbin verifier corpus --- execution/commitment/pbin_verify_test.go | 48 ------------------------ 1 file changed, 48 deletions(-) diff --git a/execution/commitment/pbin_verify_test.go b/execution/commitment/pbin_verify_test.go index 7cc773ac72e..64275214b78 100644 --- a/execution/commitment/pbin_verify_test.go +++ b/execution/commitment/pbin_verify_test.go @@ -24,7 +24,6 @@ import ( "github.com/stretchr/testify/require" - "github.com/erigontech/erigon/common" "github.com/erigontech/erigon/common/length" ) @@ -339,53 +338,6 @@ func pbinTestVerifyRecords(t *testing.T, ms *MockState, root []byte, wantLeaves require.Equal(t, wantLeaves, leaves) } -func pbinTestVerifyCorpora(t *testing.T) []struct { - name string - corpus *pbinTestCorpus -} { - t.Helper() - return []struct { - name string - corpus *pbinTestCorpus - }{ - { - name: "two accounts", - corpus: new(pbinTestCorpus). - account(pbinOracleAddr(51), 1, 2, common.Hash{0x01}). - account(pbinOracleAddr(52), 3, 4, common.Hash{0x02}), - }, - { - name: "zone boundary slots", - corpus: new(pbinTestCorpus). - storage(pbinOracleAddr(53), pbinOracleSlot(63), 0x01). - storage(pbinOracleAddr(53), pbinOracleSlot(64), 0x02). - storage(pbinOracleAddr(53), pbinOracleSlot(255), 0x03). - storage(pbinOracleAddr(53), pbinOracleSlot(256), 0x04), - }, - {name: "mixed accounts and storage", corpus: pbinTestMixedCorpus()}, - {name: "deep shared prefix", corpus: pbinTestDeepSharedPrefixCorpus()}, - } -} - -// TestPBinVerifyRecordsRebuildRoot is the independent recompute: what the engine -// wrote must hash back to what it returned, with no cell of the live grid -// involved. -func TestPBinVerifyRecordsRebuildRoot(t *testing.T) { - t.Parallel() - - for _, tc := range pbinTestVerifyCorpora(t) { - t.Run(tc.name, func(t *testing.T) { - t.Parallel() - pph, ms := pbinTestEngine(t) - require.NoError(t, ms.applyPlainUpdates(tc.corpus.plainKeys, tc.corpus.updates)) - root := pbinTestProcess(t, pph, tc.corpus.plainKeys, tc.corpus.updates) - - require.Equal(t, tc.corpus.oracleRoot(t), root) - pbinTestVerifyRecords(t, ms, root, len(tc.corpus.entries(t))) - }) - } -} - // TestPBinVerifyRootRecordIsUnique pins the shape the recompute relies on: one // record has no ancestor, and its path is the root node's prefix. func TestPBinVerifyRootRecordIsUnique(t *testing.T) { From 8ad722763610a390866484a52a9ff0cf43d2e292 Mon Sep 17 00:00:00 2001 From: awskii Date: Sat, 1 Aug 2026 16:04:04 +0700 Subject: [PATCH 45/56] docs: drop the pbin implementation plans Working documents for the M0/M1 milestones; the engine doc and the M1b gate record carry what outlives them. --- .../20260729-pbin-patricia-hashed.md | 349 --------------- .../completed/20260730-pbin-m1-local-el.md | 418 ------------------ 2 files changed, 767 deletions(-) delete mode 100644 docs/plans/completed/20260729-pbin-patricia-hashed.md delete mode 100644 docs/plans/completed/20260730-pbin-m1-local-el.md diff --git a/docs/plans/completed/20260729-pbin-patricia-hashed.md b/docs/plans/completed/20260729-pbin-patricia-hashed.md deleted file mode 100644 index 7080badaea8..00000000000 --- a/docs/plans/completed/20260729-pbin-patricia-hashed.md +++ /dev/null @@ -1,349 +0,0 @@ -# PBinPatriciaHashed — binary commitment engine (EIP-8297) - -## Overview - -Add `PBinPatriciaHashed`: a **binary** commitment engine implementing EIP-8297 (Partitioned Binary Tree), as a sibling of `HexPatriciaHashed` reusing the same grid/fold/unfold idea with a different node model. - -**Problem it solves.** EIP-8297 is the EF's Standards Track successor to Verkle for the state tree: arity 2, hash-only (post-quantum), no `storage_root`, code chunked into the tree. Erigon has no binary trie. This lands one as a self-contained engine so the design can be evaluated — in particular its single-pass root computation, which has no account→storage sequential dependency and is therefore a clean testbed for parallel fold work. - -**Key constraint: new engine, no external API changes.** `Trie`, `PatriciaContext`, `Updates`/`Update`, `keyHasher`, `cellEncodeData`, `BranchData` and the `nibbles` package are **not modified**. PBin is additive: new files plus one variant registration. `PatriciaContext.Branch` returns opaque bytes, so PBin uses its own branch record codec without touching the shared one. - -**Scope: M0 only.** In-memory over `MockState`, `ModeDirect`, account + storage zones. Correctness against a reference oracle is the deliverable — not production wiring. - -## Context (from discovery) - -- Repo `/Users/awskii/org/wrk/erigon`, branch `main` @ `1e078ffb04`. Package `execution/commitment` (29,422 lines incl. tests; `hex_patricia_hashed.go` 3,164; `commitment.go` 2,345). -- Spec: `/Users/awskii/org/wrk/EIPs/EIPS/eip-8297.md`. Anchors: tags/merkelization `:187-222`, `insert`/split `:137-183`, constants `:271-278`, header values `:311-347`, storage `:399-437`, no-deletion `:441-447`, test cases `:583-630`. -- Reference points in HPH: grid `:129`, existing `cell` `:300-315`, `needUnfolding` `:1263-1319` (reads `cell.hashedExtension` at `:1310` — the mechanism that makes the cell prefix load-bearing for navigation), `fold` dispatch `:2031-2038`, `foldBranch` `:1660-1725`, `foldPropagate` `:1915-1953`, `RootHash` `:362`/`:1249`, branch DB key `:1443`, `updateKey` `:2023`. -- `BranchEncoder.CollectUpdate` `commitment.go:501-546` merges with `prev` before `PutBranch`. `keyHasher` `:1478`, `hasherReusesAddrPrefix` `:1485`. -- `MockState` test driver `patricia_state_mock_test.go:39-202`; note `Account`/`Storage` return `Flags = DeleteUpdate` for a **missing** key (`:92-95`, `:129-134`). -- Invariant tests worth porting: `hex_patricia_hashed_test.go:157-249`. - -## Development Approach - -- **testing approach**: TDD — in every task the failing test is written **before** the implementation it covers. The reference oracle (Task 4) exists before the engine it validates. -- **CRITICAL naming rule**: `package commitment` already declares `cell`, `computeCellHash`, `fold`, `unfold` and more. **Every new package-level identifier MUST carry a `pbin` prefix** — `pbinCell`, `pbinFold`, `pbinLeafHash`, `pbinTreeKeyAccount`, `pbinEmptyTreeHash`. Methods on new types need no prefix. A collision is a compile error, so this applies to every task. -- **CRITICAL no-API-change rule**: do not modify `Trie`, `PatriciaContext`, `Updates`/`Update`, `keyHasher`'s signature, `cellEncodeData`, `BranchData`, or the `nibbles` package. If a task appears to require it, stop and record it with ⚠️ rather than proceeding. -- complete each task fully before moving to the next -- **every task MUST include new/updated tests**, listed as separate checklist items -- **all tests must pass before starting the next task** -- **update this plan file when scope changes during implementation** -- plan is self-contained from a clean git state; no task depends on transient working-tree state - -## Testing Strategy - -- **unit tests**: required per task, table-driven where the input space is enumerable -- **differential tests**: root equality against the EIP reference oracle (Task 4). Note its blind spot: the oracle consumes the same value encoder as the engine, so it can **not** catch value-encoding bugs — those are pinned against hand-written hex in Task 3. -- **property tests**: permutation independence, fold/unfold round-trip, branch-record recompute across batches -- **fuzz**: codec round-trip across all bit lengths; process fuzzers with a low-entropy slot generator -- no e2e tests — library-internal engine, no UI surface - -## Hazard Register - -Each hazard has exactly one detecting guard. Guards are acceptance criteria, not nice-to-haves. - -| ID | Hazard | Detecting guard | Task | -|----|--------|-----------------|------| -| H1 | Stale branch-cell hash after a prefix split (prefix is inside the branch hash, so shrinking it invalidates a cached hash) | Oracle diff on a mined deep-shared-prefix corpus; debug assert that a cell whose prefix bit length changed has `hashLen == 0` | 8, 11 | -| H2 | Untouched sibling dropped across `Process` batches (at arity 2 the sibling is the entire other half of the subtree) | Two-phase test: batch A writes both children, batch B touches one, assert root equals oracle over A∪B | 11 | -| H3 | Implicit prefix bit length — byte length silently carries up to 7 spurious bits into `encode_bit_prefix` | Explicit uvarint bit count; decode asserts `byteLen == ceil(bitLen/8)` and zero pad bits | 5 | -| H4 | Prefix buffer truncation (a 66-byte prefix into a smaller field; Go `copy` is min-length and silent) | Cell encode/decode round-trip with prefix bit length drawn from `[0, 529)` | 5 | -| H5 | DB branch-key aliasing — two bit paths encoding to one key means one read, one stale record | Codec round-trip fuzz over every bit length 0..528 + explicit non-canonical-pad rejection | 1 | -| H6 | State-blob depth truncation (`byte(depth)` maps bit-depth 300 → 44) | **N/A in M0** — no state blob. Re-arm when save/restore lands. | — | -| H7 | Zero-length prefix overloaded to mean "not a stored branch" (EIP-8297 permits an empty branch prefix) | Unfold a stored branch record with `prefixBitLen == 0`, assert it is descended into, not treated as leaf/empty | 7 | -| H8 | Zone mis-routing of slots 0..63 (hottest slots land in the wrong zone; tree stays internally consistent) | Zone-boundary tests at slots 63/64/255/256 + plain-key validator over every written record | 2, 11 | -| H9 | Terminator arithmetic carried over from hex (`hashedExtLen-1` at `:1310` exists only to strip the hex terminator) | Table of `(cellPrefix, probeKey) → expected needUnfolding result` covering `cpl==0`, `cpl==len(prefix)`, `cpl=64` in `STORAGE_ZONE` with `tree_index = slot/256`, `sub_index = slot%256` — the sub-index is the **raw** low byte, not hashed, so adjacent slots co-locate. -2. **Key representation** `[9]uint64` big-endian words + `bitLen int16`. Divergence = XOR + `bits.LeadingZeros64`, **clamped** by `min(aLen,bLen)`. Both 272 and 528 are `8k+2` bytes, so both end in a 16-bit tail word — one mask constant. The tail **must** be masked or XOR reads garbage (H10). -3. **Hash = Keccak-256** via erigon's `keccak.KeccakState`, used for both `H` and `key_hash`. EIP-8297 defines `H` abstractly (`:187-189`) and names Keccak as a candidate (`:513`), so this is spec-conformant. Reached through one interface so it can be swapped. -4. **Grid** `[528][2]pbinCell`. Row-indexed arrays are `[528]`; depth-indexed arrays are `[529]` because depth is inclusive of 528. HPH measured: cell 456 B, grid 933,888 B. PBin ≈416 B/cell → ≈439 KB. -5. **touchMap/afterMap** stay `uint16` using bits 0-1 only, so `OnesCount16`/`TrailingZeros16` logic ports unchanged. Assert `(touch|after) &^ 0b11 == 0` at fold entry. -6. **Prefix lives in two places**: the branch record's DB **key** (full path from root, as HPH does at `:1443`/`:2023`) *and* in the parent's stored cell. The cell copy is **navigation** — `unfold` cannot reconstruct the descent key without it (verified at `hex_patricia_hashed.go:1310`). Representation changes nibbles→bits only. -7. **Branch DB key codec**: `packBitsMSBFirst(path) || byte(bitLen mod 8)`, zero-padded, **non-canonical pad rejected on read**. Max 67 B, `MaxPathBits = 528`. `bitLen == 0` → single `0x00`. A *leading* length field is forbidden because it would scatter a subtree's records across the keyspace; the property relied on is **subtree-range contiguity**, not ancestor-before-descendant ordering (a 7-bit path encodes `[0x00,0x07]` while its 8-bit descendant encodes `[0x00,0x00]`, so descendants can sort before ancestors — that is acceptable and must not be assumed away). -8. **Split rehash = materialize-on-split.** Because the prefix is inside the branch hash and `_insert` shrinks a split survivor's prefix to `node.prefix[matched+1:]` (`eip:174-176`), a split invalidates the cached child hash — a problem HPH never has, since `extensionHash` hashes at the parent over the child's hash. Resolution: when `needUnfolding` reports divergence **inside** a cell's prefix, unfold the survivor at its own path and recompute from its two children. **If the survivor is a leaf it has no record and needs no read** — its hash commits the complete key (`eip:106-109`). The survivor's DB key does not change, only its hash. -9. **Branch records are self-contained**: always encode **both** cells (`bitmap = afterMap`), so no merge-with-previous path exists. This diverges deliberately from `BranchEncoder.CollectUpdate`'s merge (`commitment.go:501-546`) and is what makes H2 tractable at arity 2. -10. **Oracle** = a naive Go transcription of the EIP's `BinaryTree`/`_insert`/`merkelize` (`eip:112-222`) in the test package. Root equality against it is the M0 gate, with the value-encoding blind spot noted in Testing Strategy. - -**How the no-API-change constraint is satisfied:** - -- `keyHasher` stays `func([]byte) []byte`, returning the **primary** leaf's tree key. PBin writes the `CODE_HASH` sibling leaf at `sub_index+1` during the same stem visit. Ordering holds because sub-indices ascend `0 → 1 → 64..`, so `Updates`, `HashSort`, `TouchPlainKey` are untouched. `hasherReusesAddrPrefix` (`:1485`) pointer-compares against `KeyToHexNibbleHash`, so a PBin hasher yields `addrCacheReuse=false` with no edit. -- PBin uses its **own** branch record codec; a 66-byte prefix does not fit the shared `cellEncodeData.extension [64]byte`. `PatriciaContext.Branch` returns opaque bytes, so nothing shared changes. -- The only edit to a pre-existing non-test file is additive: a variant constant plus a switch case in `commitment.go`. - -## Technical Details - -**bitpath** (`pbin_bitpath.go`) — named `pbinBitpath` per the `pbin` prefix rule, which wins over this sketch. -```go -type pbinBitpath struct { - w [9]uint64 // big-endian words; byte order == descent order - bitLen int16 // 0..528 -} -func (p *pbinBitpath) bit(d int16) uint64 -func (p *pbinBitpath) maskTail() -func pbinCommonPrefixBits(a, b *pbinBitpath) int16 // XOR + LeadingZeros64, clamped by min(aLen,bLen) -``` - -**Values** (`pbin_values.go`) — BASIC_DATA is 32 bytes (`eip:332-339`): `version(1) || reserved(3) || code_size(4) || nonce(8) || balance(16)`, big-endian. `Update.Balance` is a `uint256.Int` (`commitment.go:2187`) but the EIP field is 16 bytes, so balances `>= 2^128` **error** rather than truncate. Storage values are left-padded to exactly 32 bytes (`eip:132`). - -**Branch record** (`pbin_branch.go`, PBin-local): `touchMap(2) || afterMap(2) || per-cell{ fields(1), prefixBitLen(uvarint), prefixBytes, hash|leafKey }`, both cells always present. - -## What Goes Where - -- **Implementation Steps** (`[ ]`): all code, tests, and the additive variant registration inside this repo -- **Post-Completion** (no checkboxes): measurements, deferred decisions, follow-on milestones - -## Implementation Steps - -### Task 1: bitpath type and bit-path DB key codec - -**Files:** -- Create: `execution/commitment/pbin_bitpath.go` -- Create: `execution/commitment/pbin_bitpath_test.go` - -- [x] write failing tests for `pbinCommonPrefixBits` at bit lengths 271, 272, 273, 527, 528; a case seeding `w[]` with `0xFF` beyond `bitLen`; and a 272-bit path that is a bitwise prefix of a 528-bit path asserting the result is 272 (guards H10) -- [x] write a failing fuzz test for codec round-trip across every bit length 0..528, plus explicit non-canonical-padding rejection cases (guards H5) -- [x] write a failing unit test asserting no valid encoding equals the literal `"state"` (`0x7374617465`) -- [x] implement `bitpath` with `[9]uint64` words, `bitLen int16`, `MaxPathBits = 528`, and `bit`/`slice`/`append`/`hasPrefix`/`maskTail` — as `pbinBitpath`/`pbinMaxPathBits` per the naming rule -- [x] implement `pbinCommonPrefixBits` using XOR + `bits.LeadingZeros64`, clamped by `min(aLen, bLen)` -- [x] implement `pbinEncodeBitPath`/`pbinDecodeBitPath` as `packBitsMSBFirst(path) || byte(bitLen mod 8)`, rejecting non-canonical padding on read; `bitLen == 0` encodes to a single `0x00` -- [x] run tests - must pass before task 2 - -### Task 2: EIP-8297 tree key derivation and zone routing - -**Files:** -- Create: `execution/commitment/pbin_keys.go` -- Create: `execution/commitment/pbin_keys_test.go` - -- [x] write failing tests reproducing the EIP's vectors (`eip:583-630`), each asserting the **full** 34/66-byte key against a `keccak` computed inline in the test body rather than via the helper under test: BASIC_DATA key; slot 5 → sub-index `0x45`; slot 1000 → `tree_index 3`/`sub_index 0xE8` with `tree_index` as 32-byte big-endian -- [x] write failing zone-routing tests at slots 63/64/255/256 and for the 12-byte address padding (guards H8) -- [x] implement `pbinAddr32`, `pbinTreeKeyAccount(addr, subIdx)`, `pbinTreeKeyStorage(addr, slot)` with the `slot < 64` account-zone route -- [x] implement the two-level digest cache: `H(addr32)` per address, `H(addr32||tree_index)` per 256-slot group, with `tree_index` encoded as 32-byte big-endian -- [x] provide a `keyHasher`-compatible `func([]byte) []byte` returning the primary leaf's tree key, and assert `len` is 34 or 66 at every construction site -- [x] run tests - must pass before task 3 - -### Task 3: Leaf value encoding - -**Files:** -- Create: `execution/commitment/pbin_values.go` -- Create: `execution/commitment/pbin_values_test.go` - -- [x] write failing tests pinning BASIC_DATA byte offsets 0/4/8/16 against hand-written hex — **not** against the encoder, since the Task 4 oracle shares this encoder and cannot catch its bugs -- [x] write a failing test asserting a balance `>= 2^128` returns an error rather than truncating -- [x] write failing tests for the CODE_HASH leaf value and for storage values left-padded to exactly 32 bytes -- [x] implement `pbinEncodeBasicData` per `eip:332-339`: `version(1) || reserved(3) || code_size(4) || nonce(8) || balance(16)` big-endian -- [x] implement `pbinCodeHashValue` and `pbinEncodeStorageValue` -- [x] run tests - must pass before task 4 - -### Task 4: EIP reference oracle in the test package - -**Files:** -- Create: `execution/commitment/pbin_oracle_test.go` - -- [x] transcribe the spec's `LeafNode`, `BranchNode`, `_insert` and `merkelize` (`eip:112-222`) as a naive in-memory Go tree, Keccak-256, no optimisation -- [x] implement `encode_bit_prefix` exactly per `eip:196-201` and define the empty-tree hash as 32 zero bytes per `eip:208` -- [x] add corpus builders: empty; single key (root **is** a leaf, `eip:133-135`); two keys diverging at bit 0; two diverging at bit 527; a split-inside-prefix triple forcing `node.prefix[matched+1:]`; a mined deep-shared-prefix cluster -- [x] write tests asserting the oracle is self-consistent: permutation independence and prefix-freedom over every corpus -- [x] run tests - must pass before task 5 - -### Task 5: pbinCell, grid, and branch record codec - -**Files:** -- Create: `execution/commitment/pbin_cell.go` -- Create: `execution/commitment/pbin_branch.go` -- Create: `execution/commitment/pbin_cell_test.go` - -- [x] write failing cell encode/decode round-trip tests with prefix bit length drawn from `[0, 529)` (guards H4) -- [x] write failing tests for record decode rejecting inconsistent `prefixBitLen`/byte length and non-zero pad bits (guards H3) -- [x] define `pbinCell` with a tree-key-space `bitpath` prefix and plain-key fields; **use one prefix, not two** — HPH's `hashedExtension`/`extension` split exists to hold hashed and plain spaces separately, whereas PBin derives the tree key from the plain key on demand. No `stateHash` field: a leaf hash is `H(0x00||key||value)` with nothing to memoize -- [x] define the grid as `[528][2]pbinCell` with row-indexed arrays `[528]` and depth-indexed arrays `[529]`, plus `reset`/`resetForReuse` clearing `bitLen` -- [x] implement the PBin branch record codec with `prefixBitLen` as an explicit uvarint **bit** count, always encoding both cells (`bitmap = afterMap`, no merge path) -- [x] run tests - must pass before task 6 - -### Task 6: node merkelization - -**Files:** -- Create: `execution/commitment/pbin_hash.go` -- Create: `execution/commitment/pbin_hash_test.go` - -- [x] write failing tests asserting each node hash matches the Task 4 oracle for hand-built shapes: single leaf, one branch, nested branch with non-empty prefix, branch with **empty** prefix -- [x] write a failing node-level test asserting the empty subtree is 32 zero bytes, explicitly not `empty.RootHash` (guards H11) -- [x] implement `pbinLeafHash = H(0x00 || key || value)` over the complete 34/66-byte key -- [x] implement `pbinBranchHash = H(0x01 || encode_bit_prefix(prefix) || left || right)` with one scratch buffer sized 133 B (1 tag + 2 count + 66 prefix + 64 children) -- [x] implement exactly **one** cell hasher — do not port both `computeCellHash` and `witnessComputeCellHashWithStorage` (guards H14) -- [x] run tests - must pass before task 7 - -### Task 7: unfold and needUnfolding - -**Files:** -- Create: `execution/commitment/pbin_patricia_hashed.go` -- Create: `execution/commitment/pbin_unfold_test.go` - -- [x] write a failing table test of `(cellPrefix, probeKey) → expected pbinNeedUnfolding result` covering `cpl == 0`, `cpl == len(prefix)` (full match, descend), and `cpl < len(prefix)` (split signal) (guards H9) -- [x] write a failing test unfolding a stored branch record whose `prefixBitLen == 0`, asserting it is descended into rather than treated as leaf or empty (guards H7) -- [x] write failing unfold tests for divergence at bits 0, 63, 64, 65, 271 and 527 -- [x] create `PBinPatriciaHashed` with the grid, `currentKey bitpath`, context and Keccak state -- [x] implement `pbinNeedUnfolding` with bit reads and clamped common-prefix, dropping hex terminator arithmetic and `clampToAccountBoundary`; its return contract MUST distinguish "prefix fully matched" from "diverges inside prefix" — landed as the method `needUnfolding` returning `pbinUnfolding{action, matched}`; the `pbin` prefix rule covers package-level identifiers only, and methods on `PBinPatriciaHashed` cannot collide with the hex engine's -- [x] implement `pbinUnfold`/`pbinUnfoldBranchNode` reading the parent's stored cell prefix to reconstruct the descent key, with an explicit node-kind flag so a zero-length prefix is not overloaded — landed as the methods `unfold`/`unfoldBranchNode` -- [x] run tests - must pass before task 8 - -### Task 8: fold primitives - -**Files:** -- Modify: `execution/commitment/pbin_patricia_hashed.go` -- Create: `execution/commitment/pbin_fold_test.go` - -- [x] write failing grid-seeded unit tests: hand-build one row, fold it, assert the emitted hash equals the oracle's `merkelize` of that node and that the record bytes round-trip -- [x] write a failing test for a split whose survivor is a **leaf**, asserting no branch record is read -- [x] write failing tests forcing splits inside prefixes at several depths, asserting each rehashed node matches the oracle -- [x] implement `pbinFold` dispatching the three kinds — delete / propagate / branch — mirroring `hex_patricia_hashed.go:2031-2038` — landed as the methods `fold`/`foldBranch`/`foldPropagate`/`foldDelete`, same reasoning as Task 7's `unfold` -- [x] implement `pbinFoldBranch` writing records keyed by the encoded bit path, asserting `(touchMap|afterMap) &^ 0b11 == 0` at entry and `popcount(afterMap) == 2` (guards H12) -- [x] implement `pbinFoldPropagate` accumulating the child's prefix bits into the parent cell and writing **no** record, asserting `prefixBits == depth - upDepth - 1` (guards H12) — landed as the equivalent post-condition on the assembled prefix, which also catches a dropped branch bit -- [x] implement materialize-on-split with the leaf-survivor short circuit, plus a debug assert that a cell whose prefix bit length changed has `hashLen == 0` (guards H1) — landed as `rehashAfterPrefixChange`, which enforces the invariant rather than asserting it: a cell that knows its children re-derives, one that does not is marked stale and materializes on demand -- [x] add instrumentation counters for splits-inside-prefix and extra `ctx.Branch` reads -- [x] run tests - must pass before task 9 - -⚠️ **Scope note (discovered here, resolved here).** Decision 8 covered only `needUnfolding`-reported splits, but a cell's node prefix also changes on the *normal* descent: `unfold` consuming a branch cell's prefix leaves the cell holding none of it, and the propagate that follows hands it back. Both directions invalidate a hash the prefix sits inside, and the propagate direction cannot be fixed by a record read at fold time without re-reading every descended node. Resolved by carrying the two child hashes in memory on cells this run built (`pbinCell.children`/`childrenSet`, not serialised), so a prefix change re-derives instead of re-reading; materialize-on-split stays the fallback for cells that arrived from a record. One Task 7 assertion (`pbin_unfold_test.go`, descended cell keeps the parent's hash) encoded the wrong behaviour and now pins `hashLen == 0`. - -### Task 9: drive loop, Process and RootHash - -**Files:** -- Modify: `execution/commitment/pbin_patricia_hashed.go` -- Create: `execution/commitment/pbin_process_test.go` - -- [x] write a failing test asserting `RootHash()` on a fresh engine is 32 zero bytes, not `empty.RootHash` (guards H11) -- [x] write a failing test for a one-key tree asserting the root **is** the leaf hash `H(0x00||key||value)` (`eip:133-135`), and for a two-key tree asserting it is the branch hash -- [x] write failing `Process` tests over `MockState` for account-only, storage-only and mixed corpora, asserting root equality with the oracle -- [x] implement `pbinUpdateCell`, the key-path descent, and the `Process` drive loop — landed as the methods `updateCell`/`followAndUpdate`/`processKey`, same reasoning as Task 7's `unfold` -- [x] implement `RootHash` including the root-as-leaf case -- [x] implement the account fan-out: write the `CODE_HASH` leaf at `sub_index+1` during the same stem visit, leaving `Updates`/`HashSort`/`TouchPlainKey` untouched -- [x] reject deletes originating from the **update stream** only; a missing-key `ctx.Account`/`ctx.Storage` read returns `DeleteUpdate` (`patricia_state_mock_test.go:92-95`, `:129-134`) and MUST be treated as absent, not as a delete (guards H13) -- [x] run tests - must pass before task 10 - -### Task 10: variant registration - -**Files:** -- Modify: `execution/commitment/commitment.go` -- Create: `execution/commitment/pbin_variant_test.go` - -- [x] write `TestInitializeTrieAndUpdates_BinVariant` first as the red test, asserting the constructed type, `Variant()`, and `Mode() == ModeDirect` -- [x] add `VariantBinPatriciaTrie` plus a case in `ParseTrieVariant`/`InitializeTrieAndUpdates` — **additive only** -- [x] implement the remaining `Trie` methods to satisfy the interface unchanged: `Reset`, `ResetContext`, `Release`, `Variant`, `SetTraceWriter`, `EnableCsvMetrics` -- [x] write a test asserting `Reset` then reuse produces the same root as a fresh engine -- [x] run tests - must pass before task 11 - -**Registration notes.** `InitializeTrieAndUpdates` pins `ModeDirect` for this variant whatever mode the caller passes, mirroring how the parallel variant pins `ModeParallel`: `ModeParallel` allocates a hex-nibble prefix trie that has no meaning at arity 2. `SetTraceWriter` traces one line per run — the Task 8 counters — which is also how Task 12 reads them. `EnableCsvMetrics` is a no-op: M0 collects no metrics. `Release` pools the engine as the hex one does, since the grid is ~439 KB. - -### Task 11: hazard guards and differential fuzzing - -**Files:** -- Create: `execution/commitment/pbin_verify_test.go` -- Create: `execution/commitment/pbin_hazard_test.go` -- Create: `execution/commitment/pbin_fuzz_test.go` - -- [x] implement an independent branch-record recompute oracle: walk every written record, decode, recompute bottom-up, assert it reproduces the root — landed as `pbinVerifier`, which finds the root record as the one no other record is a bit-prefix of -- [x] implement a bit-space plain-key validator asserting `treeKey(plainKey) == branchPath || cellPrefix` for every written record (guards H8) -- [x] write the two-phase sibling test: `Process` batch A writing both children, then batch B touching one child, asserting the root equals the oracle over A∪B (guards H2) -- [x] write the mined deep-shared-prefix corpus test and assert oracle equality (guards H1) -- [x] write permutation-independence tests porting `Test_HexPatriciaHashed_UniqueRepresentation`/`2`/`BrokenUniqueRepr` (`hex_patricia_hashed_test.go:157-249`) -- [x] write a differential fuzzer over `Process` against the oracle with a **low-entropy slot generator** — random 32-byte slots essentially never share a stem, so a default corpus never exercises sub-index sharing -- [x] run tests - must pass before task 12 - -### Task 12: Verify acceptance criteria - -- [x] verify all requirements from Overview are implemented and M0 scope boundaries were respected — `VariantBinPatriciaTrie`/`PBinPatriciaHashed`/`pbinKeyHasher` appear outside the `pbin_*` files only at the three additive `commitment.go` sites, so nothing reaches the domain layer -- [x] verify no shared type, interface or signature was modified: `git diff --stat` shows `commitment.go` as the only pre-existing non-test file, additive only — every other touched file is new; `commitment.go` +11/-0 -- [x] verify every hazard in the register except H6 has a named passing test -- [x] verify every new package-level identifier carries the `pbin` prefix and the package compiles without collision — 274 identifiers checked by AST walk; the only ones not starting at position 0 are `errPBin*` and `NewPBinPatriciaHashed`, where Go's `err`/`New` convention precedes the marker -- [x] run the package test suite: `go test ./execution/commitment/...` -- [x] run fuzzers briefly, one target per invocation — `-fuzz` refuses a regex matching several: `go test ./execution/commitment/ -run=Fuzz -fuzz=FuzzPBinBitPathCodec -fuzztime=60s` then the same for `FuzzPBinProcessMatchesOracle` -- [x] verify `go build ./...` and `go vet ./execution/commitment/...` are clean -- [x] record the Task 8 instrumentation counters under Post-Completion - -**Verification results.** - -| Check | Result | -|-------|--------| -| `go test ./execution/commitment/...` | ok, 9.6s | -| `go vet ./execution/commitment/...` | clean | -| `go build ./...` | clean | -| `FuzzPBinBitPathCodec -fuzztime=60s` | pass, 8.2M execs, 0 new interesting | -| `FuzzPBinProcessMatchesOracle -fuzztime=60s` | pass, 231k execs, 97 new interesting | - -Hazard → guard, all passing (H6 is N/A in M0, H14 is a review item): - -| Hazard | Guard | -|--------|-------| -| H1 | `TestPBinFoldSplitInsidePrefixMatchesOracle`, `TestPBinSplitInsideStoredPrefix`, `TestPBinDeepSharedPrefixCorpus` | -| H2 | `TestPBinUntouchedSiblingSurvivesBatch` | -| H3 | `TestPBinBranchDecodeRejects` (`pbin_cell_test.go:172`) | -| H4 | `TestPBinBranchCodecRoundTripPrefixBitLengths` (`pbin_cell_test.go:63`) | -| H5 | `FuzzPBinBitPathCodec`, `TestPBinBitPathNeverEncodesToStateKey` | -| H7 | `TestPBinUnfoldEmptyPrefixBranchRecord` | -| H8 | `TestPBinStorageZoneRouting`, `TestPBinAddr`, `pbinVerifier.checkPlainKeys` | -| H9 | `TestPBinNeedUnfolding` | -| H10 | `TestPBinCommonPrefixBits_IgnoresBitsBeyondBitLen`, `TestPBinCommonPrefixBits_ShorterPathIsPrefix` | -| H11 | `TestPBinEmptyTreeHash`, `TestPBinRootHashEmptyEngine`, `TestPBinOracleEmptyTreeHash` | -| H12 | `TestPBinFoldRejectsInconsistentGrid`, `TestPBinFoldBranchRejectsWrongArity` | -| H13 | `TestPBinProcessRejectsStreamDelete`, `TestPBinProcessMissingStateIsAbsent` | -| H14 | `pbinHasher.cellHash` is the only cell hasher; `PBinPatriciaHashed.cellHash` delegates to it and `leafCellHash` is reachable only through it | - -⚠️ **Fuzz-harness note.** `FuzzPBinProcessMatchesOracle` at the documented invocation can end in `context deadline exceeded`. It is the harness, not the engine: Go's default `-fuzzminimizetime` is 60s, so a newly interesting input found late in a 60s run keeps minimizing past the coordinator's shutdown deadline. Symptom is `execs` falling to 0/sec near the end. Adding `-fuzzminimizetime=2s` holds ~4,900 exec/s throughout and exits clean. Separately checked that no input is slow: 20,000 generated corpora ran with a worst case of 23ms. - -### Task 13: [Final] Update documentation - -- [x] add a package-level doc comment on `pbin_patricia_hashed.go` naming the EIP, the Keccak suite choice and the M0 scope boundaries — landed as a file-header comment separated from `package commitment` by a blank line, matching the package's own convention; attaching it would have made it the doc comment for all of `commitment`, which this engine does not own -- [x] update `CLAUDE.md` if new patterns were discovered — no change: the `pbin` prefix rule is plan-local, and nothing repo-wide came out of M0 -- [x] move this plan to `docs/plans/completed/` - -## Post-Completion - -*Items requiring manual intervention, measurement, or follow-on milestones — no checkboxes* - -**Task 8 instrumentation counters (measured in Task 12).** - -`splitsInsidePrefix` counts probes diverging inside a cell's prefix; `materializeReads` counts the `ctx.Branch` reads that follow, i.e. the ones the descent alone would not have made. - -| Corpus | keys | leaves | splitsInsidePrefix | materializeReads | -|--------|-----:|-------:|-------------------:|-----------------:| -| mixed, one batch | 54 | 60 | 59 | 0 | -| deep shared prefix, one batch | 4 | 8 | 7 | 0 | -| mixed, two batches | 54 | 60 | 59 | 6 | -| deep shared prefix, two batches | 4 | 8 | 7 | 0 | -| mixed, one key per batch | 54 | 60 | 59 | 33 | -| fuzz generator space, 2,000 runs | 200,329 | — | 79,437 | 751 | - -Splits inside a prefix are the common case, not the exception — roughly one per key. What makes them cheap is Task 8's in-memory child hashes: **within a single `Process` call `materializeReads` is 0**, because every cell that splits was built by that same run and re-derives. A read costs only when a cell arrives from a record an earlier batch wrote, so the counter tracks batch granularity rather than tree shape — 6 reads at two batches, 33 at one key per batch (the drive loop's worst case), and 751 over 200,329 keys in the fuzz space (0.37% of keys, 0.95% of splits). - -**Decisions deferred to data:** -- Split-rehash strategy. M0 ships materialize-on-split, narrowed by Task 8's in-memory child hashes to cells that arrived from a record — a node this run folded re-derives for free. The numbers above say the residual is small and, crucially, driven by how work is batched rather than by the corpus. Promoting the two child hashes into the record (32 B per branch cell, plus a migration story) would remove the hazard outright, but on M0 evidence it buys under 1% of splits; revisit against production batch sizes, where a batch spans one block and the cross-batch fraction will be higher than these tests show. -- Record the one-prefix-per-cell rationale (Task 5) here and in the commit body rather than as a source comment. - -**Out of scope, in rough dependency order:** -- Code chunks (`chunkify_code`, `eip:374-397`), including the stateful PUSHDATA boundary byte and content-addressed overflow chunks shared between contracts. Discovered in Task 6: `Update` carries **no code size**, and adding one is an external API change, so M0 encodes BASIC_DATA `code_size` as 0. Both the engine and the oracle see the same value, so the M0 gate still holds, but a conformance claim needs a real code size sourced alongside code chunking. -- Deletion semantics. EIP-8297 never removes entries, but erigon's `StorageDomain` represents never-written and explicitly-zeroed identically (`execution/state/rw_v3.go:965` calls `DomainDel` on an empty value). Production needs a tombstone-capable encoding or a documented deviation. Under EIP-8297 SELFDESTRUCT must **not** remove storage leaves, which removes the rationale for erigon's storage-subtree collapse. -- Commitment state save/restore (re-arms H6). `SetState`/`EncodeCurrentState` are concrete `*HexPatriciaHashed` methods and `commitmentdb` type-switches on them (`commitment_context.go:895-901`, `:935-949`, panics at `:103`, silently no-ops `SetCollapseTracer` at `:411`). Promoting a `StatefulTrie` interface is an external API change, deliberately excluded from M0; `:411` should error rather than no-op before any variant ships. -- Parallel mounting. `mountedNib 0..15` plus a depth-63 fold wall does not translate to arity 2; a 2-way root split silently serialises rather than failing. -- Domain-layer wiring and branch-cache tuning (dense tiers land on bit depths 4/8/12/16; a literal port covers ~1 in 8 bit depths). - -**Upstream:** -- The EIP is a Draft with an unfixed hash function, unfixed witness gas constants, and an unresolved header code-chunk count (EIP-7864 sets `CODE_CHUNKS_IN_HEADER = 16` at `eip-7864.md:219`; EIP-8297 puts 128 chunks in the header via `CODE_OFFSET = 128` at `eip-8297.md:271`; neither cites data). Any conformance claim should name the spec commit it was built against. diff --git a/docs/plans/completed/20260730-pbin-m1-local-el.md b/docs/plans/completed/20260730-pbin-m1-local-el.md deleted file mode 100644 index 24fb8237829..00000000000 --- a/docs/plans/completed/20260730-pbin-m1-local-el.md +++ /dev/null @@ -1,418 +0,0 @@ -# PBin M1 — binary trie as a local EL state trie - -## Overview - -M0 landed `PBinPatriciaHashed`, an EIP-8297 binary commitment engine that computes correct roots in memory and reproduces 6 of 7 root vectors from the reference implementation. It is not wired to anything: the domain path panics, code never enters the tree, and the hash is Keccak-256 while every other client uses BLAKE3. - -M1 makes it run. **Target: a dev-chain container started with `--experimental.bin-commitment`, booting and producing blocks on the binary trie.** - -Two deliberate scope choices define what that means: - -- **Keccak-256 stays the production hash.** BLAKE3 is a **test-only** override, used to replay the reference vectors. This is not cross-client compatibility and must not be described as such — no other client would agree with our roots. -- **The header state-root check becomes independently togglable, defaulting to ON.** It is *not* gated on the variant. On a chain we produce ourselves the check is worth keeping — it cross-checks the builder's root against the executor's, a real if weak oracle — and a dev chain therefore keeps a root oracle. It must be switchable off for a chain whose headers we cannot reproduce, which is the mainnet case below. - -Defaulting to ON is the safety property: a bin run against foreign headers fails loudly at block 1 rather than silently building a wrong chain, and hex behaviour is untouched. - -This needs no overlay or migration mechanism — verified: dev pins no genesis hash (`execution/chain/spec/genesis.go:141-171`), the dev beacon takes `Eth1Data` from the runtime-computed EL genesis hash (`cmd/utils/flags.go:2243-2250`), and the header root and block-0 exec root come from the same function (`genesiswrite.ComputeGenesisCommitment` → `sd.ComputeCommitment`, `genesis_write.go:468`), so they flip together. - -But dev is not a cheap target. Its alloc (`execution/chain/spec/allocs/dev.json`) has 18 entries, 7 code-bearing, and the deposit contract `0x00000000219ab540...705Fa` is 6358 bytes = 206 chunks = 128 header + **78 CODE_ZONE overflow chunks**. Overflow keys are `key_hash(code_hash ‖ tree_index)`, which cannot be derived from a 20-byte plain key. The plan expected that to force a plain-key namespace break on day one; it did not — Task 12 put the chunk value in the branch record, so a chunk leaf has no plain key and the engine derives the overflow key itself (Task 13). The contract cannot be dropped: dev is PoS-from-genesis (`TerminalTotalDifficulty: 0`, `CancunTime: 0`, `DepositContract` set, `genesis.go:157-162`). - -**M1a is a mandatory intermediate gate, not acceptance.** pbin over a real MDBX datadir with no consensus, via `RebuildCommitmentFiles` (`db/state/squeeze.go:876`) or `backtester` (`execution/commitment/backtester/backtester.go:199-215`). It is the only place collation, merge, restart and branch-record round-trip get exercised without consensus noise — but **it has no header-root oracle**. A wrong root there surfaces only as non-determinism between a forward run and a rebuild. Do not mistake a green M1a for a correct engine. - -## Context (from discovery) - -- Repo `/Users/awskii/org/wrk/wt/pbin`, branch `awskii/pbin-patricia`, base `1e078ffb04`. Prior plan: `docs/plans/completed/20260729-pbin-patricia-hashed.md` (M0, complete). -- Spec: `/Users/awskii/org/wrk/EIPs/EIPS/eip-8297.md`. Reference implementation: `ethereum/execution-specs` branch `projects/binary-trie`. -- Engine: `execution/commitment/pbin_*.go` (~6.3k lines incl. tests). -- External oracles already green and to be kept green: `pbin_specroots_test.go` (7 fixed + 600 sequence roots, via the oracle), `pbin_specengine_test.go` (6/7, via the engine), `pbin_specvectors_test.go` (BASIC_DATA + key routing). -- Integration surfaces: `execution/commitment/commitmentdb/commitment_context.go`, `db/state/execctx/{domain_shared,options}.go`, `db/state/{squeeze,erigondb_settings,domain_stream}.go`, `execution/commitment/branch_cache.go`. - -## Development Approach - -- **testing approach**: TDD — the failing test comes first in every task. -- **CRITICAL naming rule** (carried from M0): `package commitment` already declares `cell`, `fold`, `unfold`, `computeCellHash` and more. **Every new package-level identifier MUST carry a `pbin` prefix.** A collision is a compile error, so this applies to every task. -- **The M0 "no external API changes" rule is relaxed, but only for three sanctioned breaks** — Task 7 (option semantics), Task 6 (new persisted toml key), Task 13 (plain-key namespace). Everything else stays additive. If a task appears to need a fourth break, stop and record it with ⚠️ rather than proceeding. **Two of the three were taken**: Task 13 turned out additive (see its checklist), so the plain-key namespace is unchanged. -- complete each task fully before the next; small focused changes -- **every task MUST include new/updated tests**, listed as separate checklist items -- **all tests must pass before starting the next task** -- **update this plan file when scope changes during implementation** -- self-contained from a clean git state; no task depends on transient working-tree state - -## Testing Strategy - -- **unit tests**: required per task, table-driven where the input space is enumerable -- **external conformance**: the three `pbin_spec*_test.go` files are the ground truth and must stay green. Task 1 makes the test path run under BLAKE3 — `pbin_specroots_test.go:55` already hard-asserts `meta.hasher == "blake3"`, so they only become meaningful after Task 1. -- **determinism as a proxy oracle** (M1a): forward-run root vs rebuild-from-domains root over the same datadir. Note this proxy is only valid if the answer to Q2 is "pure function of state". -- **structural asserts** where a test cannot cover the failure: variant/cache combinations, monotonic visit order. -- no e2e tests in the erigon sense; the M1b gate is a node smoke run. - -## Progress Tracking - -- mark completed items `[x]` immediately -- add newly discovered tasks with ➕ -- document blockers with ⚠️ -- keep the plan in sync with the work actually done - -## Solution Overview - -Settled decisions — do not revisit during implementation: - -1. **Keccak-256 is the production hash; BLAKE3 is test-only.** Both injection seams (`pbinHasher.sum`, `pbinDigestCache.sum`) keep their Keccak nil-default. The test harness drives the engine under BLAKE3 through **both** seams so the reference vectors mean something. Never describe this as cross-client compatibility, and never as a speedup — BLAKE3 is slower than erigon's `fastkeccak` on arm64 at the 133-byte branch preimage. - - The vector conformance still transfers to the production path: the trie treats keys as opaque bytes, so an algorithm correct for BLAKE3-derived keys is correct for Keccak-derived ones. What does **not** transfer is any claim of agreement with another client. The residual risk is a hash call site that bypasses the injectable seam — caught because the vectors run under BLAKE3 and a hardcoded Keccak site would break them. - -2. **The header state-root check is independently togglable, default ON**, at all five comparison sites: `exec3.go:810`, `exec3.go:730`, `exec3_serial.go:205`, `committer.go:557`, `:659`, `:764`. Follow the existing `common/dbg/experiments.go` `EnvBool` convention (as `DiscardCommitment` does) — one definition site, no CLI plumbing, easy to set in a container. Do **not** gate it on the variant: a self-produced chain keeps the check as an oracle, and only a foreign-header chain needs it off. Note `dbg.DiscardCommitment()` is a different thing — it skips computing the root at all (`exec3.go:788`) — and must not be reused for this. -3. **The CODE_HASH leaf value stays Keccak** (eip:344-347, :578-579) — already correct at `pbin_values.go:68-73`. The empty-tree hash is 32 zero bytes and hash-independent. -4. **Zero-vs-absent is fixed in the engine, not the domain.** The engine holds the presence bit the domain lacks. Domain encoding is untouched. -5. **`code_size` on `Update` is additive, not a break** — verified below. -6. **Overflow code chunks carry their value in the branch record**, and the new plain-key shape is **tag-discriminated, never length-discriminated**. -7. **pbin is a whole-datadir property**, resolved at first start and persisted. No mid-chain activation. -8. **pbin stays `ModeDirect`, sequential.** Parallel/streaming mounting is structurally excluded. - -### Why `code_size` on `Update` is not an external break - -`Update.Encode/Decode` (`commitment.go:2253-2335`) has exactly two production call sites, both in `RecordingContext` (`recording_context.go:72,85`) feeding `BuildTrieTrace` into a debug TOML (`trie_trace.go:36-127`). `Update` is never in an MDBX table, never a domain value, never crosses gRPC. The `ModeDirect` ETL spill carries only `(hashedKey → plainKey)` (`commitment.go:1938-1942`), and the pbin branch record does not serialize `Update` at all. All 141 `Update` composite literals repo-wide are keyed, so a new field compiles everywhere unchanged. The comment at `pbin_hash.go:138-139` claiming otherwise is wrong and must be deleted. - -### The push side is dead for pbin - -The bin variant is hardwired to `ModeDirect` (`commitment.go:165-170`), whose `TouchPlainKey` ignores both `val` and `fn` (`:1668-1672`), and `HashSort` passes `update = nil` (`:1966`). So `Updates.TouchCode` (`:1834-1844`) can **never** deliver code to pbin. Everything comes from the read side, at `TrieContext.Account`. Do not patch `calc_state.go:353-360` expecting code to land. - -## Technical Details - -**Hash injection** — `pbinHasher.hash` (`pbin_hash.go:63-68`) and `pbinDigestCache.hash` (`pbin_keys.go:127-132`) both nil-fallback today. Flipping the fallback costs **0 edits** at the 43 `pbinTreeKeyAccount`/`pbinTreeKeyStorage` call sites and 12 `pbinKeyHasher()` sites; threading a parameter would cost ~45. - -**Root record key** — bit-path keys always end in a byte ≤ 7 (`pbin_bitpath.go:191-193`), so a single-byte sentinel ≥ `0x08` cannot collide. It must also avoid `0x00`, which `pbinEncodeBitPath` produces for the empty path and which the row-0 fold already writes (`pbin_patricia_hashed.go:669`). - -**State blob** — hex writes depths as one byte per row (`hex_patricia_hashed.go:2777-2779`); pbin depths are `[528]int16` (`pbin_cell.go:80`), so a naive port truncates ≥256. Preferred shape is root cell + 3 flags ≈ 160 B, resting on an unproven inference (see Thin/Unverified). The 16-byte `txNum‖blockNum` header stays byte-identical — it is read raw and variant-blind at `commitment_context.go:1140-1150`. - -**Code chunking** (eip:374-397) — pad to a multiple of 31 **before** the pushdata scan; `bytes_to_exec_data` sized `len(padded)+32`; residual pushdata carries **across** chunk boundaries; `byte0 = min(bytes_to_exec_data[pos], 31)`. `MaxCodeSize` 24576 → 793 chunks → 128 header + 665 overflow across 3 CODE_ZONE stems. A 7702 designator is 23 bytes → 1 chunk. - -## Hazard Register - -Each hazard needs a named test or a structural assert. These are the plan's real acceptance criteria. - -| ID | Hazard | Task | Guard | -|----|--------|------|-------| -| H1 | **BranchCache slot collision** — `trunkSlot` returns another node's *well-formed* record; `pbinDecodeBranch` accepts it, the subtree hashes, root is wrong, no error. Deterministic, concentrated at the top of the tree (8 slots for every ≤8-bit path) | 4 | structural assert in the ctor + test that a bin SharedDomains has no shared branch cache | -| H2 | **Root record lost to empty-key iteration truncation** — `loadRoot` treats absent as an empty tree (`pbin_patricia_hashed.go:349-351`); looks like a fresh datadir | 2 | round-trip a stored root through a real domain iteration | -| H3 | **A hash call site bypassing the injectable seam** — with Keccak in production and BLAKE3 only in tests, a site hardcoding either one drifts silently. Also a pooled engine inheriting a stale `hasher.sum` | 1 | full 32-byte key equality in `TestPBinSpecKeyRouting` under BLAKE3 — a hardcoded site breaks the vectors; `Release()` must clear `hasher.sum` | -| H4 | **Variant mismatch across processes** — genesis hex + exec pbin, flagless restart, rpcdaemon defaulting to hex, `integration commitment rebuild` overwriting pbin records | 6, 7 | persisted `trie_variant` + refusal on disagreement | -| H5 | **Backwards visit from the header-chunk fan-out** — `fold` writes with `prevData = nil` and the record replaces its predecessor outright; re-descending a folded row rewrites it with a `touchMap` that no longer names the previously-touched bit | 12 | ✔ `followAndUpdate` refuses a non-ascending visit (`errPBinVisitOrder`); chunks emitted at stem exit — `TestPBinVisitOrderIsMonotonic`, `TestPBinCodeChunksFollowHeaderSlots` | -| H6 | **State-blob depth truncation** — `byte(depth)` truncates ≥256; paths reach 528 bits | 5 | restart round-trip with a >256-bit path | -| H7 | **Code key misread as storage** — a 52-byte length-discriminated code key read as `(addr, slot)` | 13 | ✔ dissolved: a chunk leaf carries no plain key, so no code key ever enters the plain-key namespace. `TestPBinCodeKeyNeverRoutesToTheStorageZone` + the verifier's zone assert on record-resident leaves | -| H8 | **Stale high code chunks after a shortening redeploy** — header chunks overwrite in place and are never removed, so a forward run keeps residue while a rebuild emits only `ceil(code_size/31)`. Two internally-consistent, different roots. **Breaks recompute-from-domains as an oracle** | 12 | ✔ confirmed real, not fixed: `TestPBinShorteningRedeployKeepsStaleChunks` pins both roots and the exact residue. Q2 answered — the tree is history-dependent for code | -| H9 | **Unconditional `CodeDomain` read promotes tolerated inconsistency to root divergence** — cleared 7702 residue, `eth_simulateV1` overlays. The existing code documents the residue as benign (`commitment_context.go:1054-1057`); PBT removes that license | 11 | decide and test the residue case explicitly | -| H10 | **`ReplacePlainKeys` over pbin records** if references are ever enabled — rewrites bytes at hex cell offsets during background merge. Inert by default, one flag away, no variant check in that path | 6 | refuse the combination | -| H11 | **Overflow-chunk sibling rehash via `CodeStore` by-hash** → cache *miss* (not error) → zero-valued chunk leaf | 13 | ✔ dissolved in Task 12: `pbinFieldLeafValue` puts the chunk in the record, so no by-hash lookup exists to miss | -| H13 | **Root verification switched off leaves nothing validating the node path** — a silently wrong chain looks healthy. Only relevant when the toggle is used, i.e. against foreign headers; a self-produced chain keeps the check | 6 | default ON so it is opt-out not opt-in; loud startup log when off; a bin run against foreign headers without the toggle must fail at block 1, not degrade | -| H12 | **`foldDelete` "enabled" to make a test pass** — collapses nodes the reference leaves in place | 10 | guarded by plan text + a test asserting it stays unreachable from `Process` | - -## Open Questions - -Blocking items needing a human or upstream answer. Do not proceed past the task that depends on one without recording the answer here. - -- **Q1 (highest value) — what does the reference produce for a removed account?** EIP-161 empty-removal and EIP-6780 destruct both hand pbin a `DeleteUpdate` on a committed leaf. Task 9 would turn that into BASIC_DATA `{version 0, code_size 0, nonce 0, balance 0}` + CODE_HASH `keccak(empty)`. Consistent with eip:345-347 but **not verified against the reference**, and `testdata/eip8297_vectors.json` does not cover it. It silently changes the root. ⚠️ **Deferred at Task 9, still unanswered.** Account removal keeps erroring at both sites (`updateCell`, the `loadCellState` account arm); only storage was reinterpreted. Note the `zero_value_present` vector *is* an account-zone BASIC_DATA leaf of 32 zero bytes, so the reference at least admits that leaf shape — it does not say a removal produces it. Unblocks nothing in M1: a dev chain reaches neither removal path. -- **Q2 — is the tree a pure function of current state, or of history?** (H8.) eip-8297 is silent. Determines whether recompute-from-domains is a valid oracle at all, hence whether the M1a gate means anything. Possibly an upstream spec question. **Answered (Task 12): of history, for code chunks only.** Accounts and storage are a pure function of current state — every leaf's value comes from a domain read. Code chunks are not: eip:439-443 says EVM execution never removes entries, so a redeploy to shorter code leaves the chunks above the new length in the tree holding the old code's bytes, and nothing in the current state records that they exist. A forward run commits them; a recompute from the domains emits only `ceil(code_size/31)`. Both roots are internally consistent and different — `TestPBinShorteningRedeployKeepsStaleChunks` pins both, including the exact residue. Consequences: **recompute-from-domains is not an oracle for a code-bearing account** (the M1a gate stays valid only because its datadir is code-free), and `integration commitment rebuild` over a chain that has seen a shortening redeploy will not reproduce the chain's roots. Reachable in practice by an EIP-7702 delegation clear and by a metamorphic CREATE2 redeploy; not by the M1b dev chain. Zeroing the tail instead would need the previous code length, which no read on the commitment path has. Still worth raising upstream — the same divergence exists for any client that rebuilds state from a snapshot. -- **Q3 — does the reference create a present-zero leaf on `SSTORE 0` to a virgin slot?** Erigon drops it at `execution/state/state_object.go:245-246` before any writer sees it. If yes, that guard must be variant-gated and every zero-SSTORE becomes a state write. ⚠️ **Deferred at Task 9, still unanswered.** Task 9 deliberately kept the virgin case a no-op: a delete only zeroes a cell that already holds a leaf. Unblocks nothing in M1 — the drop happens above the commitment layer, so pbin never sees the write on either answer, and M1 compares roots only against itself (M1a) and against the header the same node produced (M1b), never against the reference. It becomes blocking the moment a pbin chain is compared with another client's. -- **Q4 — confirm `github.com/zeebo/blake3`** as a **test-only** dependency (lower stakes now that production stays Keccak; the in-graph `lukechampine.com/blake3` may simply be enough). Its measured advantage was 13–18% arm64 / 58–78% amd64, not re-verified. **Answered (Task 1):** `zeebo` not added — production stays Keccak so BLAKE3 speed is irrelevant, and `lukechampine.com/blake3` is already a direct dependency used by `pbin_specroots_test.go`; it now backs `pbinBlake3Hash` for all three seam tests. -- **Q5 — forward/backward compatibility of a new `erigondb.toml` key** across erigon binaries, given the file is downloader-delivered and **wins over the CLI** (`erigondb_settings.go:74-89`). **Answered (Task 6):** `readErigonDBSettings` uses `go-toml/v2` `Unmarshal`, which ignores unknown keys — older binaries parse a `trie_variant` toml fine. The key is written only when bin, so published/downloader tomls stay byte-identical, and a downloader-delivered hex toml under a bin process is refused at resolve. Residual risk: a binary **predating the key** opens a bin datadir as hex with no guard — inherent to any new key; acceptable while bin is experimental and fresh-datadir-only. - -## Thin / Unverified - -Do not treat these as established: - -- ~~`pbin_branch.go` record field-bit layout beyond the leaf-kind rejection at `:156-165`~~ — **read and extended in Task 12.** Layout: `uint16 touchMap ‖ uint16 afterMap`, then one cell body per bit set in afterMap, ascending. A cell body is `byte fields ‖ uvarint prefixBitLen ‖ packed prefix bits ‖ present fields in bit order`. Field bits: `leaf=1, branch=2, accountAddr=4, storageAddr=8, hash=16, leafValue=32`. Every variable-length field is `uvarint len ‖ bytes` but each has one legal length, which `pbinDecodeFixedVal` enforces, so the record has exactly one spelling. Exactly one kind bit and, for a leaf, exactly one of `accountAddr | storageAddr | leafValue`; a branch carrying `leafValue` is rejected. Task 13's overflow chunks need no new field — they reuse `leafValue`. -- ~~The "grid arrays are restorable-as-zero" argument underpinning Task 5's ~160-byte blob~~ — **proven in Task 5** (see the Task 5 checklist for the read/write-site audit); the root-cell blob landed. -- ~~Whether pbin branch records are truly opaque to the pass-through merge path~~ — **exercised in Task 10** with references off: collation, prune and merge round-trip the records byte-for-byte, checked against a db snapshot with a positive count of records provably served from files. -- ~~Task 8's deferral mis-attribution~~ — **Task 8**: still no concrete failing sequence, and the exposure is bounded: deferral is only ever requested by `ExecV3` (fork validation / parallel apply), and the fork-validation writes it would mis-route land in a validation overlay that is never flushed. The guards are structural — bin cannot reach the deferred path at all now. - -## What Goes Where - -- **Implementation Steps** (`[ ]`): code, tests and asserts inside this repo -- **Post-Completion** (no checkboxes): the node smoke run, upstream questions, follow-on milestones - -## Implementation Steps - -### Task 1: BLAKE3 as a test-only hash, wired through both seams - -**Files:** -- Modify: `execution/commitment/pbin_patricia_hashed.go` -- Modify: `execution/commitment/pbin_specvectors_test.go` -- Modify: `execution/commitment/pbin_specengine_test.go` -- Modify: `go.mod` - -Production keeps Keccak-256. This task only makes the **test** path run the whole engine — node hashing *and* key derivation — under BLAKE3, so the reference vectors become meaningful for the key-derivation surface too. - -- [x] write a failing test asserting `TestPBinSpecKeyRouting` compares **full 32-byte tree keys** against `embedding_vectors`, not just zone/length/sub-index — this is what proves no hash site bypasses the seam (guards H3) — full-key equality passes: derivation matches the reference under BLAKE3 -- [x] write a failing test asserting a pooled engine does not inherit a previous `hasher.sum` after `Release()` — `TestPBinReleaseClearsHashSuite`, red before the fix -- [x] add `github.com/zeebo/blake3` (test use only; confirm Q4 first) — Q4 answered: not added, in-graph `lukechampine.com/blake3` suffices (see Open Questions) -- [x] give the engine a way to set BLAKE3 on **both** seams together — `pbinHasher.sum` and the `pbinDigestCache` behind `pbinKeyHasher` — so a half-configured test is impossible — `setHashSuite(sum)` sets the node seam and returns a matching `pbinKeyHasherWith(sum)` -- [x] clear `hasher.sum` in `Release()` (`pbin_patricia_hashed.go:107-115`) -- [x] leave the production nil-defaults on Keccak, the CODE_HASH leaf value on Keccak, and the empty-tree hash at 32 zero bytes -- [x] run tests — the three `pbin_spec*_test.go` files must all pass under BLAKE3 before task 2 - -### Task 2: Root record key sentinel - -**Files:** -- Modify: `execution/commitment/pbin_patricia_hashed.go` -- Create: `execution/commitment/pbin_rootkey_test.go` - -- [x] write a failing test that stores a root record and reads it back through a real `TblCommitmentVals` iteration, asserting the iteration does not truncate (guards H2) — `TestPBinRootRecordRealTableIteration`, red before the fix: MDBX accepts the empty-key Put but hands the key back zero-length mid-iteration, which `domain_stream.go:343,577` reads as end-of-stream -- [x] replace `pbinRootKey = []byte{}` with a single-byte sentinel ≥ `0x08` — `0x08` -- [x] assert the sentinel cannot be produced by `pbinEncodeBitPath` for any bit length 0..528 — `TestPBinRootKeySentinelNotABitPath`, plus `pbinDecodeBitPath` rejecting the sentinel outright -- [x] write a test asserting `loadRoot` distinguishes "no record" from "empty tree" — `TestPBinLoadRootNoRecordVersusStoredTree`: no record reads as the empty tree with `rootPresent == false`; a stored record reproduces the stored root -- [x] run tests — must pass before task 3 - -### Task 3: No nil values into the domain - -**Files:** -- Modify: `execution/commitment/pbin_patricia_hashed.go` -- Create: `execution/commitment/pbin_domainwrite_test.go` - -- [x] write a failing test asserting neither `storeRoot` nor `foldDelete` ever hands a nil value to `PutBranch` — `TestPBinStoreRootEmptiedTreeWritesNonNil` + `TestPBinFoldDeleteWritesNonNilWithRealPrev` over `pbinStrictWriteContext`, which refuses nil the way `SharedDomains.DomainPut` does; both red before the fix -- [x] route the empty-root `storeRoot` path (`:328-337`) and `foldDelete` (`:725-727`) through `DomainDel` or a non-nil zero-length slice — non-nil zero-length: `PatriciaContext` has no `DomainDel`, and `TemporalMemBatch.putHistory` routes any `len(v) == 0` write to `DeleteWithPrev`, so `[]byte{}` IS the deletion encoding at the domain boundary -- [x] pass real `prevData` at both `PutBranch` sites to avoid the extra `GetLatest` per branch write — the grid retains each row's record bytes at unfold (`pbinGrid.prevRecord`), the engine retains the root record across load/store (`rootPrev`); all three write sites (`foldBranch`, `foldDelete`, `storeRoot`) now pass it. `TestPBinProcessPutBranchCarriesRealPrev` checks every write's prev equals the record it replaces, red before -- [x] write a test asserting a zero-length branch value round-trips as a deletion — `TestPBinZeroLengthBranchRoundTripsAsDeletion`: the engine empties a stored tree, the zero-length records stay in the store, a fresh engine reads them back as no tree -- [x] run tests — `go test ./execution/commitment/... -count=1` green, `make lint` clean - -### Task 4: Disable the shared BranchCache for the bin variant - -**Files:** -- Modify: `execution/commitment/commitmentdb/commitment_context.go` -- Create: `execution/commitment/commitmentdb/pbin_nocache_test.go` - -- [x] write a failing test asserting a bin-variant `SharedDomains` has no shared branch cache — `TestPBinSharedDomainsHasNoSharedBranchCache`, red while the ctor assert saw the shared cache; written to survive Task 5 (tolerates the save/restore panic, asserts directly on the SD once construction succeeds) -- [x] write a failing test demonstrating the `trunkSlot` collision for two distinct ≤8-bit bit-path keys, so the reason is pinned in the suite (guards H1) — `TestPBinBranchCacheTrunkSlotCollision`: 3-bit paths 000 (`00 03`) and 001 (`20 03`) both index `d2[0x03]`; `Get` serves the other path's record as a well-formed hit. Pinning test — it passes against current `trunkSlot` by design and fails if the collision ever disappears -- [x] construct the bin-variant `SharedDomains` with `execctx.WithoutSharedBranchCache()` — `NewSharedDomains` applies it whenever `trieCfg.Variant` is bin; the co-located `AdaptivePinController` is gated on the same option and stays off too -- [x] add a structural assert in the commitment-context ctor that the bin variant never has a shared branch cache — enforce, do not document — the commitmentdb `sd` interface gained `HasSharedBranchCache()` (implemented by `execctx.SharedDomains`); `NewSharedDomainsCommitmentContext` panics on bin+shared-cache ahead of the save/restore panic Task 5 removes -- [x] run tests — `go test ./execution/commitment/... -count=1` and `./db/state/... -short` green, `make lint` clean twice - -### Task 5: SetState / EncodeCurrentState for pbin, and remove the panic - -**Files:** -- Modify: `execution/commitment/pbin_patricia_hashed.go` -- Create: `execution/commitment/pbin_state.go` -- Modify: `execution/commitment/commitmentdb/commitment_context.go` -- Create: `execution/commitment/pbin_state_test.go` - -- [x] write a failing restart round-trip test covering a path deeper than 256 bits (guards H6) — `TestPBinRestartRoundTripDeepPath`: same-group slots 256/257 share the first 520 tree-key bits, so the root branch prefix is 527 bits; encode → restore → continue reproduces the oracle root -- [x] prove or refute that the grid arrays are restorable-as-zero — **proven**: every row-indexed array (`rows`, `depths`, `touchMap`, `afterMap`, `branchBefore`, `prevRecord`) is written only in `unfold`/`unfoldBranchNode` before `activeRows++` exposes the row, and read only at indexes < `activeRows` (`updateCell`, `needUnfolding`, `fold` and its three arms). At `activeRows == 0` — which both state calls enforce — the live state is exactly the root cell plus the three root flags. Chose the root-cell blob: `0xB1 marker ‖ flags ‖ uint16 len ‖ pbinAppendCell(root)`. `rootPrev` is deliberately not serialized: a post-restore `storeRoot` passes nil prev and `DomainPut` fetches the stored value itself -- [x] implement `pbin` `SetState`/`EncodeCurrentState` — the chosen blob serializes no depths at all, so no depth ever meets a one-byte encoding; the marker byte also rejects a hex blob outright (hex starts with a flags byte ≤ 0x07) -- [x] remove the `VariantBinPatriciaTrie` panic and fix the hardcoded `variant:` in the struct literal — the stale `Test_NewSharedDomainsCommitmentContext_RejectsBinVariant` that pinned the panic became `..._AcceptsBinVariant` -- [x] extend the three variant gates: `LatestCommitmentState`, `encodeCommitmentState`, `restorePatriciaState` — all three assert `commitment.StatefulTrie`; the trie-trace state capture in `ComputeCommitment` now uses the same seam instead of a hex/parallel type switch -- [x] promote `StatefulTrie` as an **optional** interface asserted at those 3 sites; do not widen `Trie` — declared beside `Trie`; hex satisfies it as-is, `ParallelPatriciaHashed` delegates to its template trie, pbin implements it in `pbin_state.go` -- [x] write a test asserting the 16-byte `txNum‖blockNum` header is byte-identical to hex's — `TestPBinCommitmentStateHeaderMatchesHex` (➕ white-box `commitmentdb/pbin_state_header_test.go`, not in the planned file list) also round-trips block/tx through `restorePatriciaState` under bin -- [x] run tests — `go test ./execution/commitment/... ./db/state/... -count=1` green, `make lint` clean - -### Task 6: The --experimental.bin-commitment flag, persistence, and root-check gating - -**Files:** -- Modify: `db/state/execctx/domain_shared.go` -- Modify: `db/state/erigondb_settings.go` -- Modify: `db/state/squeeze.go` -- Modify: `cmd/utils/flags.go` -- Modify: `node/cli/default_flags.go` -- Modify: `node/ethconfig/config.go` -- Modify: `node/eth/backend.go` -- Modify: `cmd/integration/commands/flags.go` -- Create: `db/state/pbin_variant_persist_test.go` - -- [x] write a failing test asserting a datadir created with the bin variant is **refused** when opened with a conflicting config (guards H4) — `db/state/pbin_variant_persist_test.go`: bin datadir + streaming/parallel flags refused; hex datadir (absent or explicit `trie_variant`) + bin flag refused; a downloader-delivered hex toml under an in-memory-bin process refused; legacy datadir (preverified.toml) + bin flag refused -- [x] write a failing test asserting `references_in_commitment_branches = true` is refused under the bin variant (guards H10) — `TestPBinVariantRefusesReferences`: both the persisted refs=true+bin toml and the first-start refs-override+bin combination error -- [x] add a `statecfg` global for the variant — `statecfg.ExperimentalBinCommitment` (`COMMITMENT_BIN` env, mirroring `COMMITMENT_PARALLEL`) -- [x] add the `--experimental.bin-commitment` flag across the 7-site experimental-commitment template — flag def + ctx→cfg (`cmd/utils/flags.go`), `node/cli/default_flags.go`, `ethconfig.Config` field, cfg→statecfg (`node/eth/backend.go`), `cmd/integration/commands/flags.go`, statecfg global -- [x] replace the duplicated inline switch at `squeeze.go:1023-1029` with `PickTrieVariant()` — the `EnableParaTrieDB` gate below it now derives from the returned variant instead of the raw flags -- [x] add `trie_variant` to `ErigonDBSettings`, resolved first-start exactly as `ReferencesInCommitmentBranches` is, and note in a comment that `erigondb.toml` wins over the CLI — `*string` ("hex"/"bin", absent = hex), written only when bin so published tomls stay unchanged; `reconcileTrieVariant` runs at every resolve: a persisted bin adopts bin process-wide (sets the statecfg global), all conflicts refuse rather than degrade -- [x] write a failing test asserting the header state-root comparison is enforced by default and skipped only when the new toggle is set — under **both** variants, since the toggle is variant-independent — `TestHeaderRootCheckDefaultOnAndTogglable` drives `headerRootMismatch` under both settings of the bin global -- [x] add the third case to `PickTrieVariant()` reachable via `--experimental.bin-commitment` — bin wins over streaming/parallel (the resolver refuses the combination anyway); `TestPickTrieVariant_BinFlag` -- [x] add a root-check toggle to `common/dbg/experiments.go` following the `DiscardCommitment` `EnvBool` pattern, **defaulting to check-enabled**, and honour it at all five sites: `exec3.go:810`, `exec3.go:730`, `exec3_serial.go:205`, `committer.go:557`, `:659`, `:764` — `dbg.CheckHeaderStateRoot` (`CHECK_HEADER_STATE_ROOT`, default true), applied via a shared `headerRootMismatch` helper at all five comparisons; `handleIncorrectRootHashError` (the `:730` arm) is only reachable from gated comparisons -- [x] log loudly once at startup when the check is disabled, so a running node says so out loud (guards H13) — `backend.go` Warn at node construction -- [x] write a test asserting a flagless restart of a bin datadir stays bin — `TestPBinVariantFlaglessRestartStaysBin`: persisted bin re-adopts with the global off, `PickTrieVariant()` returns bin -- [x] run tests — `go test ./execution/commitment/... ./db/state/... -count=1` and `./execution/stagedsync/... -short` green, `make lint` clean twice - -### Task 7: Un-pin the genesis variant - -**Files:** -- Modify: `db/state/execctx/options.go` -- Modify: `execution/state/genesiswrite/genesis_write.go` -- Modify: `rpc/rpchelper/commitment.go` -- Create: `db/state/execctx/pbin_options_test.go` - -- [x] write a failing test asserting genesis under the bin variant computes a **binary** root, not a hex one — `TestPBinGenesisComputesBinaryRoot` (➕ `execution/state/genesiswrite/pbin_genesis_test.go`, not in the planned file list), red before: `GenesisToBlock` returned the hex root under bin. Asserts both `bin != hex` and `bin == ` the root of a SharedDomains explicitly running the bin trie over the same alloc. Code-free alloc — code chunking is Task 12/13 -- [x] add `WithoutParallelCommitment()` that demotes streaming/parallel to hex and leaves bin as bin; keep `WithSequentialCommitment()` as a deprecated alias or migrate all 11 call sites — migrated all 11 and removed `WithSequentialCommitment`; no alias, so a new call site has to pick a variant policy deliberately -- [x] switch `genesis_write.go:381` to the new option -- [x] make the 10 RPC/integrity sites return an explicit unsupported-variant error rather than silently forcing hex over pbin records — `WithHexCommitmentOnly()` + `ErrBinCommitmentUnsupported`, refused in `NewSharedDomains` before any domain work. ➕ an 11th site (`commitment_integrity.go:1194`, the per-block SD inside `CheckCommitmentHistAtBlkRange`) carried no variant option at all and got the same gate -- [x] write a test asserting each of those paths errors under bin instead of returning a hex root — functional per-caller tests: `CheckCommitmentHistAtBlk` + `CheckCommitmentHistAtBlkRange` (`db/integrity/pbin_hex_only_test.go`), `ComputeCustomCommitmentFromStateHistory` (`rpc/rpchelper/pbin_commitment_test.go`), `eth_getProof` + `eth_simulateV1` (`rpc/jsonrpc/pbin_hex_only_test.go`). The remaining sites (`getWitness`, `buildWitnessResult`, both receipt-regeneration sites, `checkCommitmentRootViaSd`) reach their SharedDomains only after full block re-execution or over snapshot files, so they are covered by the shared refusal itself, tested directly in `TestPBinHexOnlyCommitmentRefusesBin` -- [x] run tests — `./db/state/... ./execution/commitment/... ./execution/state/genesiswrite ./db/integrity ./rpc/rpchelper ./rpc/jsonrpc/...` green, `make lint` clean twice - -### Task 8: Make the silent degradations loud - -**Files:** -- Modify: `execution/commitment/commitmentdb/commitment_context.go` -- Modify: `execution/commitment/pbin_patricia_hashed.go` (➕ `ErrPBinUnsupported`, the sentinel both packages wrap) -- Modify: `execution/stagedsync/exec3.go` -- Modify: `node/eth/backend.go` (➕ startup limitation log) -- Create: `execution/commitment/commitmentdb/pbin_unsupported_test.go` -- Create: `execution/stagedsync/pbin_defer_test.go` (➕) - -- [x] write a failing test asserting `SetLeaveDeferredForCaller` and the deferred-update take reject the bin variant instead of silently no-opping — `TestPBinRefusesDeferredCommitmentUpdates` (enabling side) + `TestPBinComputeCommitmentRefusesDeferredTake` (taking side), both red before: the flag was accepted and the post-`Process` type switch matched no bin trie -- [x] reject the bin variant explicitly where `exec3.go:206-210` enables deferral for fork validation and the parallel apply path — `deferCommitmentUpdates(variant, isForkValidation, parallel, isApplyingBlocks)` excludes bin, so `ExecV3` never makes a request the context panics on. **Not an ExecV3 error**: `ValidateChain` runs fork validation on every `engine_newPayload` (`exec_module.go:589`), so erroring there would make the M1b dev-chain gate unreachable; deferral is a re-org-overhead optimisation and the inline path it falls back to is the default one, over a validation overlay that is never flushed (`exec_module.go:600-602`) -- [x] make `SetCollapseTracer` (`:415-420`), `BranchChildCount` (`:424-431`) and trace-state capture (`:501-506`) error under bin rather than degrade — `BranchChildCount` and the trace capture (in `ComputeCommitment`) return `commitment.ErrPBinUnsupported`; the two void setters (`SetDeferCommitmentUpdates`, `SetCollapseTracer`) panic with the same wrapped error, matching this file's existing misuse convention (`EnableParaTrieDB`, the Task 4 ctor assert) instead of taking a fourth API break for an error return. Both are unreachable under bin in production — their only callers reach a hex-only SharedDomains (Task 7) -- [x] write tests asserting each of the four paths errors under bin — `commitmentdb/pbin_unsupported_test.go`: deferral enable, deferral take, trie-trace capture, collapse tracer, branch child count; each also pins that hex still accepts. ➕ `stagedsync/pbin_defer_test.go` (not in the planned file list) table-tests the exec3 decision -- [x] ➕ log the bin variant's unsupported paths once at startup, after the erigondb resolve so a flagless bin restart says it too (`node/eth/backend.go`) -- [x] run tests — `./execution/commitment/... ./db/state/... ./execution/stagedsync/... ./rpc/jsonrpc/...` green, `make lint` clean twice - -### Task 9: Zero-vs-absent in the engine - -**Files:** -- Modify: `execution/commitment/pbin_patricia_hashed.go` -- Create: `execution/commitment/pbin_zerovalue_test.go` -- Modify: `execution/commitment/pbin_process_test.go` (➕ retire the two tests pinning the replaced behaviour) - -- [x] write a failing test asserting a `DeleteUpdate` on an existing **storage** leaf writes 32 zero bytes and keeps the leaf, matching the reference `zero_value_present` root — `TestPBinStorageDeleteKeepsLeafAsPresentZero` (storage zone + account-header zone) and `TestPBinStorageDeleteOnUntouchedSiblingIsPresentZero`, both red before. ⚠️ correction: `zero_value_present`'s single entry is an **account-zone BASIC_DATA** key (`0x00 ‖ stem ‖ 0x00`), so it is Q1's shape, not a storage one, and the engine already reproduces it via `pbin_specengine_test.go`. The storage roots are anchored on the oracle instead — the same tree code that vector pins in `pbin_specroots_test.go`. Each test also asserts the zeroed leaf is *not* dropped: present-zero ≠ absent -- [x] reinterpret `DeleteUpdate` for storage at the three reject sites — `updateCell` and the `loadCellState` storage arm now route through `pbinZeroedLeafUpdate`, which returns a zero `StorageUpdate` for a 52-byte plain key and `errPBinDeleteUnsupported` for an account. The `loadCellState` **account** arm keeps its own explicit rejection. A delete landing on an empty cell stays a no-op (no leaf to zero) — Q3's virgin-slot case is untouched. `TestPBinLoadCellStateAbsentRead` pins the two arms apart directly. The fourth guard, `processKey` (`:184-186`), is deliberately left rejecting: it only sees a non-nil stream update, which `ModeDirect` — the mode bin is hardwired to — never passes -- [x] for the **account-removal** encoding only: record the answer to Q1 in this plan first; if unanswered, mark ⚠️, leave account removal rejecting, and continue — ⚠️ **Q1 remains unanswered**: no reference behaviour for a removed account, and no vector covers it. Account removal still errors; `TestPBinAccountRemovalStillRefused` pins that. A dev chain reaches neither EIP-161 clearing nor the EIP-6780 pre-funded-CREATE2 case, so M1b is not blocked -- [x] leave the domain encoding and the three zero-write `DomainDel` sites untouched — `git diff --stat` for this task touches only `pbin_patricia_hashed.go` and two test files -- [x] write a test asserting `foldDelete` remains unreachable from `Process` (guards H12) — `TestPBinFoldDeleteUnreachableFromProcess`: a run zeroing every stored leaf (both zones) plus an absent key, asserted through `pbinStrictWriteContext` to write no zero-length record. That is foldDelete's only observable — `storeRoot` is the sole other zero-length write and only at the root key -- [x] ➕ retired `TestPBinProcessRejectsDeletedLeaf` / `TestPBinProcessRejectsDeletedSibling` from `pbin_process_test.go`: they pinned the storage behaviour this task replaces, and the two present-zero tests are their successors -- [x] run tests — `go test ./execution/commitment/... -count=1` and `./db/state/... -short` green, `go build ./...` clean, `make lint` clean twice - -### Task 10: M1a gate — pbin over a real datadir - -**Files:** -- Create: `execution/commitment/backtester/pbin_m1a_test.go` -- Modify: `db/state/squeeze.go` (➕ phantom empty-key touch in `rebuildCommitmentShard`) - -- [x] write a test driving pbin over a real MDBX datadir via `RebuildCommitmentFiles` or the backtester, with no consensus — `backtester_test` builds its own datadir (real MDBX under a temp dir + real `.kv` domain files) and drives it through `execctx.SharedDomains` with `statecfg.ExperimentalBinCommitment` on; every SD open asserts the trie really is `*PBinPatriciaHashed`, so a hex fallback cannot make the suite vacuous. The `Backtester` type itself is unusable here — it needs a synced datadir with canonical headers -- [x] assert the forward-run root equals the rebuild-from-domains root over the same input — `TestPBinM1AForwardRunMatchesRebuildFromDomains`. Two arms: a full-touch recompute over the same datadir, and `RebuildCommitmentFiles` after wiping every commitment record and file. ➕ **found a bug in shared code**: `rebuildCommitmentShard` touches the key from `next()` before testing `ok`, so at stream exhaustion it touches a 0-length plain key. Hex hashes it into a spurious absent update; pbin panics (a plain key is neither 20 nor 52 bytes). Fixed by skipping the touch for an empty key — `next()` signals exhaustion as `(false, nil)` but a shard boundary as `(false, key)`, so the key has to be checked separately from `ok`. ➕ the comparison point is the **last collated** step boundary, not the last forward root: collation always leaves the newest step in the db, so a files-only rebuild reproduces the root as of `TxNumsInFiles` -- [x] assert a restart mid-run resumes to the same root (exercises Task 5) — `TestPBinM1ARestartResumesToSameRoot`: two halves of one input across an aggregator reopen, second half touching only its own keys, must reach the uninterrupted root; plus a fresh SD restoring the saved root before folding anything. ⚠️ **correction**: this does not exercise Task 5's state blob. `RootHash()` calls `loadRoot()` whenever `rootChecked` is false, so a gutted `SetState` still returns the right root — for pbin the restart carrier is Task 3's root record in the commitment domain, and the blob is a cache. Verified by mutation: gutting `SetState` leaves this test and `TestPBinRestartRoundTripDeepPath` green, and only the blob's own unit tests (`TestPBinStateBlobRoundTripsFlags`, `TestPBinSetStateRejectsForeignBlob`) go red -- [x] assert collation and merge preserve branch records byte-for-byte — `TestPBinM1ABranchRecordsSurviveCollationAndMerge`: latest records snapshotted from the db before collation must read back identically after `BuildFiles` + prune + `MergeLoop`, and again after a folder reopen. Non-vacuity is asserted, not assumed: every record is db-resident before collation, and a positive number (12 of 36) are gone from `TblCommitmentVals` afterwards, so their latest read can only come from the files -- [x] record in this plan that M1a has **no header-root oracle** and is not acceptance — stated in the file's package doc and here: nothing outside the engine validates these roots. Both rebuild arms and the restart arm are self-consistency checks over the same engine, so a green M1a means deterministic, not correct. H8 (stale high code chunks) is also out of reach until Task 12 puts code in the tree, which is where the forward-vs-rebuild comparison first gets a chance to fail for a real reason -- [x] run tests — `go test ./execution/commitment/... ./db/state/... -count=1` and `./execution/stagedsync/... -short` green, `go build ./...` clean, `make lint` clean twice - -### Task 11: code_size on Update - -**Files:** -- Modify: `execution/commitment/commitment.go` -- Modify: `execution/commitment/commitmentdb/commitment_context.go` -- Modify: `execution/commitment/pbin_hash.go` -- Create: `execution/commitment/pbin_codesize_test.go` -- Create: `execution/commitment/commitmentdb/pbin_codesize_test.go` (➕ the read side) - -- [x] write a failing test asserting BASIC_DATA for a code-bearing account carries the real `code_size`, checked against `basic_data_vectors` — `TestPBinBasicDataLeafCarriesCodeSize` drives `pbinLeafValue` over every vector; `TestPBinEngineRootCarriesCodeSize` then takes the size through the whole engine (context read → cell merge → leaf hash) and asserts a size-less variant of the same account roots differently. Both red before -- [x] add the `code_size` field to `Update` plus handling in `Reset`/`Copy`/`Merge`/`Encode`/`Decode`/`String` — `CodeSize uint64`, carried under the existing `CodeUpdate` flag at every hook (a size and a hash describe the same code, so a merge can never take one from the old account and the other from the new). Encode appends a varint inside the `CodeUpdate` block; ➕ `TestUpdate_EncodeDecode`/`TestUpdate_Merge` in `hex_patricia_hashed_test.go` gained the field, not in the planned file list -- [x] populate it at `TrieContext.Account` (`:1026-1070`) by reading `kv.CodeDomain` unconditionally — "unconditionally" in the sense that matters: the read no longer hides behind `dbg.AssertEnabled`. It is gated on `TrieContext.readCodeSize`, set from the variant at the one construction site the bin trie can reach (`trieContext`), so hex takes no extra domain read per code-bearing account. The warmup/concurrent factories are deliberately left alone: they need `paraTrieDB` and only ever serve page-cache warmup or a `*ParallelPatriciaHashed` fold, neither of which bin can reach -- [x] delete the wrong comment at `pbin_hash.go:138-139` and pass the real size instead of `0` -- [x] decide and test the cleared-7702-residue case explicitly — the existing benign-residue license no longer holds (guards H9) — **decision: code_size follows the account's own code hash, never CodeDomain presence.** A code-less account keeps code_size 0 whatever residue a cleared delegation left behind, so the tolerated inconsistency stays out of the root (`TestPBinTrieContextIgnoresClearedDelegationResidue`). The mirror case cannot be tolerated: a code-bearing account with no code behind it would hash as code_size 0 and produce a silently wrong root, so it errors (`TestPBinTrieContextRefusesCodeBearingAccountWithoutCode`). That is only reachable under bin — the overlay callers it would otherwise break (`eth_simulateV1`) are already refused by Task 7 -- [x] write a test asserting the push side is inert for pbin, so nobody patches `calc_state.go` expecting code to arrive — `TestPBinPushSideNeverDeliversCode`: the bin variant overrides the requested mode to `ModeDirect`, and a `TouchCode` touch reaches `HashSort` as a nil update. Pinning test — it passes against current behaviour by design and fails if the push side ever starts carrying values -- [x] run tests — `./execution/commitment/... ./db/state/... ./execution/state/genesiswrite ./db/integrity` and `./execution/stagedsync/... -short` green, `go build ./...` clean, `make lint` clean twice. ➕ the tests for the read side live in `commitmentdb/pbin_codesize_test.go` (the trie context is in that package), not in the planned `execution/commitment/pbin_codesize_test.go`; the wiring test `TestPBinSharedDomainsReadsCodeSizeUnderBin` pins variant → read and is non-vacuous by mutation - -### Task 12: chunkify_code and header code chunks - -**Files:** -- Create: `execution/commitment/pbin_code.go` -- Modify: `execution/commitment/pbin_keys.go` -- Modify: `execution/commitment/pbin_hash.go` -- Modify: `execution/commitment/pbin_patricia_hashed.go` -- Create: `execution/commitment/pbin_code_test.go` - -- [x] write failing tests for `pbinChunkifyCode` against `chunkify_vectors`, covering pushdata straddling a chunk boundary and a 7702 designator — `TestPBinChunkifyCodeVectors` (all 5 reference vectors), `TestPBinChunkifyCodePushdataStraddlesBoundary` (PUSH32 as the last byte of chunk 0: chunk 1 saturates at 31, chunk 2 still carries 1), `TestPBinChunkifyCode7702Designator`, plus `TestPBinChunkifyCodeChunkCount` pinning ceil(len/31) at the header/overflow boundary. ➕ the `chunkify_vectors` decode landed in `pbin_specvectors_test.go` (where `loadPBinSpecVectors` lives), not in the planned file list -- [x] write a failing test for a batch touching both a header storage slot and code on one account, asserting monotonic visit order (guards H5) — `TestPBinCodeChunksFollowHeaderSlots` (code + slots 5, 63 and a storage-zone slot on one account) and `TestPBinVisitOrderIsMonotonic`. The assert is structural and lives in `followAndUpdate`: every visit must exceed the last, so an inline fan-out fails loudly instead of rewriting a folded row's record. It is cleared at the start of each `Process` — a run starts back at the root, so the previous run's last key bounds nothing -- [x] write a failing shortening-redeploy test comparing forward-run and rebuild roots (guards H8); if they differ, record Q2's answer before proceeding — `TestPBinShorteningRedeployKeepsStaleChunks`, both the leaf-sibling case (2 chunks → 1) and the whole-subtree case (7 → 2). **They differ, and Q2 is answered below: the tree is a function of history for code chunks.** The forward root is pinned to the exact residue (new chunks plus the old code's chunks above the new length), so the divergence is a stated behaviour rather than an unexplained mismatch -- [x] implement `pbinChunkifyCode` per eip:374-397 exactly — pad to 31 before the scan, carry residual pushdata across boundaries -- [x] add `pbinCodeZone` and make the zone explicit at the three places a code key currently passes by accident (`pbin_keys.go:62-66`, `pbin_hash.go:132-148`, `pbin_hash.go:117-119`) — one `pbinZoneKeyLength(zone)` names the length per zone and is the authority at all three: `pbinTreeKey` panics on an unallocated zone instead of defaulting to the account length, `leafCellHash` requires the key's length to match its own zone byte, and `pbinLeafValue` switches on the zone before it looks at a sub-index. Tests: `TestPBinZoneKeyLengthIsExplicit`, `TestPBinLeafValueRoutesByZone`, `TestPBinLeafCellHashChecksZoneLength` -- [x] emit header chunks 0..127 with a stem-exit flush or as their own sorted stream keys, never mid-fan-out — stem-exit flush (`pbinPendingCode`): the account visit queues the chunks, and they are emitted when the next stream key leaves the 33-byte stem, or at the end of the stream. Chunk sub-indices are the highest in a stem, so a stem the stream has left is one no key returns to; queueing over an unflushed stem is a loud error, not a silent overwrite -- [x] ➕ **a code chunk's value lives in the branch record** (`pbinFieldLeafValue`, `pbin_branch.go` — Task 13's file, taken early because the forward run needs it). No state domain holds a chunk: chunking is a property of the tree, not of the account, and the reference never rewrites a chunk it has written. An untouched chunk leaf that is the direct sibling of a touched one must therefore reload its own bytes — `TestPBinCodeChunksSurviveAsRecordSiblings`. This dissolves H11 (no by-hash reverse lookup exists) and means a chunk leaf carries no plain key at all; `pbinDecodeCell` now requires a leaf to name exactly one value source of the three -- [x] ➕ the code read: `pbinCodeContext` (an optional interface on `PatriciaContext`, additive — not a fourth API break) implemented by `commitmentdb.TrieContext.Code` over `kv.CodeDomain`. A context that cannot serve code refuses a code-bearing account (`TestPBinCodelessContextRefusesCodeBearingAccount`), and the code the chunks come from is cross-checked against the `code_size` the BASIC_DATA leaf hashes (`TestPBinCodeSizeMustMatchTheCodeBehindIt`), since those are two separate domain reads. Read-side tests in `commitmentdb/pbin_code_test.go`, non-vacuous by mutation (a `Code` returning nothing fails all three) -- [x] ➕ code past the account header (>128 chunks, >3968 bytes) is refused with `ErrPBinUnsupported` until Task 13 — `TestPBinCodeBeyondHeaderRefused` and `TestPBinSharedDomainsRefusesCodeBeyondHeader`. The dev deposit contract is 6358 bytes, so **M1b stays blocked on Task 13**, as planned. (Task 13 lifted the refusal and replaced both tests with their committing counterparts) -- [x] ➕ `TestPBinEngineRootCarriesCodeSize` (Task 11) had a code_size with no code behind it, which this task makes an error. It now runs real code and isolates the size claim at the leaf-set level: the same leaf set with BASIC_DATA re-packed at code_size 0 roots differently -- [x] run tests — `go test ./execution/commitment/... ./db/state/... -count=1` green, `./execution/stagedsync/... ./execution/state/genesiswrite ./db/integrity -short` green, `go build ./...` clean, `make lint` clean twice - -### Task 13: CODE_ZONE overflow chunks - -**Files:** -- Modify: `execution/commitment/pbin_keys.go` -- Modify: `execution/commitment/pbin_branch.go` -- Modify: `execution/commitment/pbin_patricia_hashed.go` -- Modify: `execution/commitment/pbin_specengine_test.go` -- Create: `execution/commitment/pbin_overflow_test.go` - -- [x] verify the `pbin_branch.go` record field-bit layout before designing the new field; record the layout in this plan — read in Task 12 and re-checked here; recorded in Thin/Unverified and restated: `uint16 touchMap ‖ uint16 afterMap`, then one cell body per bit set in afterMap ascending. A cell body is `byte fields ‖ uvarint prefixBitLen ‖ packed prefix bits ‖ present fields in bit order`, field bits `leaf=1, branch=2, accountAddr=4, storageAddr=8, hash=16, leafValue=32`. Every variable-length field is `uvarint len ‖ bytes` with exactly one legal length per field, so a record has one spelling. **No new field was needed** — overflow chunks reuse `pbinFieldLeafValue`, which Task 12 added -- [x] write a failing test asserting `full_header_stem` reproduces through the **engine**, and empty the asserted exclusion list in `pbin_specengine_test.go` — 7/7. The vector fills a whole stem, so it also covers the sub-indices 2..63 the embedding reserves. Those and the code chunks now share one rule in `pbinLeafValue`: **the sub-index picks a packing only where there is state to pack**; a position with no defined packing can only hold a value that is already 32 bytes, which is the one the record carries (`pbinRecordLeafValue`, renamed from `pbinCodeChunkValue`). The state-derived positions keep their own encodings unchanged -- [x] write a failing test asserting a code key never routes to the storage zone (guards H7) — `TestPBinCodeKeyNeverRoutesToTheStorageZone`: every derived overflow key is zone `0x01` at `CODE_KEY_LENGTH`, and the stream's `pbinKeyHasher` refuses every length that is not a plain key, including the 34-byte code key and its own 64-byte preimage. The structural half is in the verifier: a record-resident leaf must sit in the code zone or at an account sub-index ≥ `CODE_OFFSET`, never in the storage zone — exercised by 5 subtests, checked non-vacuous by mutation -- [x] ⚠️ **scope change: no third plain-key shape exists.** Task 12 settled that a chunk leaf carries no plain key at all — its value lives in the record — so nothing routes a code key through `pbinKeyHasher` or the plain-key arm of `updateCell`, and there is nothing to tag-discriminate. The overflow key is derived inside the engine from `code_hash ‖ tree_index` (`pbinTreeKeyCodeOverflow`), which never meets the plain-key namespace. **The third sanctioned API break was therefore not taken** — Task 13 is additive -- [x] add a `pbinCellFields` bit carrying the 32-byte chunk value in the branch record, so no reverse lookup is needed (guards H11) — landed in Task 12 as `pbinFieldLeafValue`; overflow chunks needed no further field -- [x] extend `pbinDecodeCell` and `loadCellState` for the new shape — also landed in Task 12: `pbinDecodeCell` requires a leaf to name exactly one value source, and `loadCellState` leaves a plain-keyless leaf alone because there is no state read to make -- [x] ➕ emit the code-zone chunks as one sorted block between the account-header keys and the storage-zone ones. Overflow keys are content-addressed, so they follow neither the stream's order nor the account that produced them: they accumulate across the whole account-zone pass and flush at the first key of a higher zone (or at end of stream), sorted and deduped — two accounts running the same bytecode name the same leaves, which is the point of content-addressing. `TestPBinOverflowChunksFollowEveryAccountZoneKey` and `TestPBinOverflowChunksAreSharedByIdenticalCode`; both go red under a dropped sort and under a stem-exit flush -- [x] ➕ `TestPBinCodeOverflowKeyMatchesSpec` diffs the derivation against a transcription of `get_tree_key_for_code_chunk` at the header/zone boundary, both ends of a code stem and the last chunk `MaxCodeSize` produces. The refusals the header-only boundary carried are gone: `TestPBinCodeBeyondHeaderRefused` and `TestPBinSharedDomainsRefusesCodeBeyondHeader` became `TestPBinEngineCommitsOverflowCodeChunks` and `TestPBinSharedDomainsCommitsCodeBeyondHeader` -- [x] run tests — all 7 engine vectors pass; `go test ./execution/commitment/... ./db/state/... -count=1` green, `./execution/stagedsync/... ./execution/state/genesiswrite ./db/integrity -short` green, `go build ./...` clean, `make lint` clean twice - -### Task 14: M1b gate — --chain=dev from genesis - -**Files:** -- Create: `docs/pbin-m1b-smoke.md` -- Modify: `db/state/erigondb_settings.go` (➕ persist the variant at first start even with a downloader) -- Modify: `db/state/pbin_variant_persist_test.go` (➕) -- Modify: `cmd/utils/flags.go` (➕ the variant must reach statecfg before dev computes genesis) -- Modify: `execution/stagedsync/exec3.go`, `execution/stagedsync/stage_execute.go` (➕ `executeInParallel`) -- Create: `execution/stagedsync/pbin_parallel_exec_test.go` (➕) - -- [x] verify genesis block 0 computes a binary root and the dev beacon accepts it — bin genesis root `a314dd2e…` / block hash `a6d15d43…` vs hex `eed1da97…` / `3aa9a433…`; the beacon takes `Eth1Data` from the EL genesis hash and produced from slot 1. Block 0 already carries the deposit contract's 206 code chunks (128 header + 78 overflow), so Task 13 is exercised at genesis -- [x] run a local `--chain=dev` node to a few blocks, deploying and calling a contract — reached head 241; deployed a storage setter (call → slot 0 = `0x2a`) and a 4983-byte contract (161 chunks, 33 in CODE_ZONE overflow at runtime), both verified through RPC. Zero `Wrong trie root` with the header check at its default ON -- [x] verify a restart resumes at the same root — flagless restart re-adopts the persisted `trie_variant = 'bin'`; roots identical across the restart, including over a datadir with collated **and merged** commitment files. ⚠️ block *production* does not always resume: Caplin's forward sync stalls after a restart — reproduced identically on hex, so it is a dev-mode CL limitation, not a trie one -- [x] record the observed genesis root and block roots in `docs/pbin-m1b-smoke.md` with the exact command line -- [x] verify `integration commitment rebuild` on the resulting datadir reproduces the same roots — ⚠️ **it cannot be verified on a dev datadir, on either variant.** The rebuild itself runs to completion under bin (adopts the persisted variant, rebuilds all 3 shards from the pbin state files), but the per-shard roots it prints are partial, and its documented follow-up `integration stage_exec --reset` panics on `--chain=dev` (`readGenesis`: unknown chain spec). Without it, the first post-rebuild block reports a wrong root **on hex exactly as on bin**, so the check is not variant-discriminating. The forward-vs-rebuild oracle for pbin stays the M1a gate -- [x] ➕ **bug found and fixed: a fresh bin datadir was refused on its own second resolve.** The snapshots stage commits an empty `preverified.toml` for a chain with no published hashes, which `ResolveErigonDBSettings` reads as a legacy datadir; with the variant not yet persisted (first start + downloader deferred the write) the bin run was refused on the datadir it had just created. A bin datadir now persists its variant at first start whatever the downloader does — nothing publishes a bin `erigondb.toml` for it to pre-empt. `TestPBinVariantFreshWithDownloaderPersistsBin`, `TestPBinVariantSurvivesEmptyPreverifiedFromSnapshotsStage`; `…RefusesDeliveredHexToml` keeps the delivered-hex refusal -- [x] ➕ **bug found and fixed: the dev beacon pinned the hex genesis.** Dev mode computes the EL genesis hash for `Eth1Data` while still assembling the config, long before the backend copies the flag into `statecfg`, so the CL asked for a genesis hash the EL never wrote. The flag now reaches `statecfg` where the CLI is read -- [x] ➕ ⚠️ **parallel execution gated off under bin.** The parallel executor's normalized write set roots differently than the same block executed serially — block 0 gave `e557bca8…` against the genesis root `a314dd2e…`, while hex agrees on both executors, so the difference is something only the bin trie hashes (code chunks / `code_size` are the candidates; not root-caused). `executeInParallel` keeps bin on the serial executor rather than leaving a wrong-root path reachable, matching Task 8's refuse-don't-degrade rule. `TestPBinExecuteInParallelExcludesBin`. **Open for M2** -- [x] run the package suite — must pass before task 15 - -### Task 15: Verify acceptance criteria - -- [x] verify every hazard H1–H12 has a named passing test or a structural assert — all thirteen (H13 included) are guarded and every named test passes: - - | ID | Guard | Where | - |----|-------|-------| - | H1 | `TestPBinCtorRefusesSharedBranchCache`, `TestPBinSharedDomainsHasNoSharedBranchCache`, `TestPBinBranchCacheTrunkSlotCollision` | `commitmentdb` — the first is the structural ctor assert, the last pins the collision itself | - | H2 | `TestPBinRootRecordRealTableIteration`, `TestPBinLoadRootNoRecordVersusStoredTree` | `commitment` | - | H3 | `TestPBinSpecKeyRouting` (full 32-byte keys under BLAKE3), `TestPBinReleaseClearsHashSuite` | `commitment` | - | H4 | the 11 `TestPBinVariant*` | `db/state` | - | H5 | `TestPBinVisitOrderIsMonotonic`, `TestPBinCodeChunksFollowHeaderSlots` over the `errPBinVisitOrder` assert in `followAndUpdate` | `commitment` | - | H6 | `TestPBinRestartRoundTripDeepPath` (527-bit prefix) | `commitment` | - | H7 | `TestPBinCodeKeyNeverRoutesToTheStorageZone` | `commitment` | - | H8 | `TestPBinShorteningRedeployKeepsStaleChunks` — confirms the divergence, does not fix it (Q2) | `commitment` | - | H9 | `TestPBinTrieContextIgnoresClearedDelegationResidue`, `TestPBinTrieContextRefusesCodeBearingAccountWithoutCode` | `commitmentdb` | - | H10 | `TestPBinVariantRefusesReferences` | `db/state` | - | H11 | `TestPBinCodeChunksSurviveAsRecordSiblings` | `commitment` | - | H12 | `TestPBinFoldDeleteUnreachableFromProcess` | `commitment` | - | H13 | `TestHeaderRootCheckDefaultOnAndTogglable`; all five comparisons go through `headerRootMismatch` (`exec3.go:835`, `exec3_serial.go:202`, `committer.go:554/:656/:763`) and `backend.go:334` warns when the check is off | `execution/stagedsync` | - -- [x] verify all five open questions are answered and recorded, or explicitly deferred with ⚠️ and a reason — Q2, Q4, Q5 answered in place; Q1 and Q3 deferred with ⚠️ and a stated reason each. ➕ Q3 carried a reason but no ⚠️ marker and no statement of what it does not block; both added here -- [x] verify only the three sanctioned API breaks were taken; `git diff --stat` shows no fourth — over every non-test file in `1e078ffb04..HEAD`, exactly one exported declaration is removed or changed: `WithSequentialCommitment` → `WithoutParallelCommitment` (Task 7). Task 6's `trie_variant` is a new toml key, additive to any reader and refused rather than degraded on disagreement. Task 13 turned out additive, so its namespace break was never taken. `Update.Encode/Decode`'s wire change stays inside the debug-trace path the plan's own analysis bounds it to, and the two bin-variant panics (`SetDeferCommitmentUpdates`, `SetCollapseTracer`) change no signature and are unreachable under hex -- [x] verify every new package-level identifier carries the `pbin` prefix — checked by AST, not by grep: package-level declarations of `package commitment` at `1e078ffb04` diffed against HEAD give 394 new identifiers. 11 were unprefixed test helpers (`mustHex`, `runHex`, `blake3Sum`, the `pbin_vs_hex_compare_test.go` corpus builders and `engineShape`, both spec-vector loaders) — all generic enough to collide with a future test file in the same package, all renamed here. Five stay unprefixed deliberately: `NewPBinPatriciaHashed` (Go constructor form, still carries `PBin`), `StatefulTrie` (Task 5 promoted it as a variant-neutral interface that hex and parallel also implement), `VariantBinPatriciaTrie` (member of the `Variant*` enum family), and the two `Test…` names that follow the production symbol they exercise (`TestInitializeTrieAndUpdates_BinVariant`, `TestParseTrieVariantBin`). The rule's rationale is collision inside `package commitment`, so identifiers added to `db/state`, `execctx`, `stagedsync`, `genesiswrite`, `db/integrity` and `statecfg` follow their own packages' conventions instead (`WithHexCommitmentOnly` beside `WithoutSharedBranchCache`, `ExperimentalBinCommitment` beside `ExperimentalParallelCommitment`); the two generic test helpers among them (`writeToml`, `withVariantFlags` in `db/state`) were prefixed anyway -- [x] run `go test ./execution/commitment/... ./db/state/... -count=1` — green -- [x] run `go build ./...` and `make lint` until clean -- [x] verify the three `pbin_spec*_test.go` oracles pass under BLAKE3 with 7/7 engine vectors — `pbin_specroots_test.go:60` hard-asserts the vectors' `hasher` is `blake3` before replaying them, and `pbin_specengine_test.go:140` asserts the exclusion list is empty. All 7 engine vectors run and pass: `empty`, `single_account_leaf`, `one_header_stem_two_leaves`, `two_accounts`, `cross_zone_small`, `zero_value_present`, `full_header_stem` - -### Task 16: [Final] Update documentation - -- [x] update the package doc comment on `pbin_patricia_hashed.go` to state BLAKE3, the M1 scope, and the stated limitations (no witness, no getProof, no parallel) — ⚠️ wording correction against the checklist item: the doc states BLAKE3 as the **test-only** override set through `setHashSuite`, since production is Keccak (Solution Overview point 1); writing "the engine uses BLAKE3" would have made the doc claim the cross-client compatibility the plan forbids. Limitations named: sequential `ModeDirect` only, parallel/streaming structurally out, and the four record-reinterpreting paths (witness, `eth_getProof`, `eth_simulateV1`, receipt regeneration) refusing rather than reading bit-path records as hex -- [x] ➕ fix the stale comment on `VariantBinPatriciaTrie` (`commitment.go:154-155`), which still says the variant is not wired to the domain layer and has no state save/restore — both untrue since Task 5 (found in the Task 15 audit) — now names what is actually true of the variant (experimental, whole-datadir, sequential) and points at the engine doc for the unsupported paths instead of restating them -- [x] update `CLAUDE.md` if new patterns were discovered — one addition, the `pbin` prefix rule under Conventions. It is the only M1 convention that outlives this plan: `package commitment` carries two engines in one namespace and the hex one owns the generic names, so the rule binds every future binary-trie change, not just M1. Everything else discovered here is either recorded in the code or specific to this milestone. (`CLAUDE.md` is a symlink to `AGENTS.md`; the edit lands in the target) -- [x] move this plan to `docs/plans/completed/` - -## Post-Completion - -*Manual, external, or follow-on — no checkboxes* - -**Upstream questions to raise:** -- Q1 (removed-account encoding) and Q2 (pure function of state vs history) are plausibly spec questions for EIP-8297, not just implementation ones. Q2 in particular determines whether recompute-from-domains is a legitimate oracle for any client. -- A root vector with a code-bearing account is absent from the exported reference vectors (all 8 BASIC_DATA leaves have `code_size = 0`, no zone `0x01` keys). EELS may already have one in its own suite; check before offering. - -**Deliberately out of M1:** -- Access events / witness gas (EIP-4762 recalibration). Zero repo hits, and the EIP says `WITNESS_BRANCH_COST` is not yet fixed. The state root is computable without it. -- Cross-client devnet and EEST fixtures **as acceptance**. BLAKE3 buys reference-vector comparability and a geth `--chain=dev` diff, not consensus parity, because the gas rules do not exist. Debugging tool only. -- State expiry; parallel/streaming mounting for pbin (structurally excluded — `ModeParallel`'s prefix trie is nibble-based); witness / `eth_getProof` / `eth_simulateV1` / receipt regeneration under pbin (Task 8 makes them error — a stated limitation); mid-chain fork activation (no precedent, no state-format field in `chain.Config`, and a mid-chain switch would straddle step `.kv` files with no discriminator); referenced/squeezed commitment branches; real node deletion. - -**Publishing:** -- `ethpandaops/eth-client-docker-image-builder` issue #398 tracks binary-trie branches per client; Erigon is unchecked. Its convention is a branch named literally `binary-trie`. An image is only worth building once Task 14 passes — before that it would produce a node that cannot sync. Nothing to publish from M0. From 858d8bcefeded04210e4393b2a1a592af5ac3344 Mon Sep 17 00:00:00 2001 From: awskii Date: Sat, 1 Aug 2026 16:21:37 +0700 Subject: [PATCH 46/56] execution/commitment: shrink pbin comments to the repo policy Cut milestone and plan narration, hazard-list tags pointing at the deleted plan docs, rationale repeated across sites, and docstrings restating the code. Comment-only: 632 insertions, 890 deletions, no code line changed. --- db/integrity/pbin_hex_only_test.go | 4 +- db/state/execctx/pbin_options_test.go | 13 +- db/state/pbin_variant_persist_test.go | 13 +- .../commitment/backtester/pbin_m1a_test.go | 66 +++---- .../commitment/commitmentdb/pbin_code_test.go | 26 ++- .../commitmentdb/pbin_codesize_test.go | 25 +-- .../commitmentdb/pbin_nocache_test.go | 20 +-- .../commitmentdb/pbin_state_header_test.go | 6 +- .../commitmentdb/pbin_unsupported_test.go | 26 ++- execution/commitment/pbin_bitpath.go | 20 +-- execution/commitment/pbin_bitpath_test.go | 14 +- execution/commitment/pbin_branch.go | 29 ++- execution/commitment/pbin_cell.go | 22 +-- execution/commitment/pbin_cell_test.go | 26 ++- execution/commitment/pbin_code.go | 24 ++- execution/commitment/pbin_code_test.go | 108 +++++------ execution/commitment/pbin_codesize_test.go | 28 ++- execution/commitment/pbin_domainwrite_test.go | 30 ++-- execution/commitment/pbin_fold_test.go | 60 +++---- execution/commitment/pbin_fuzz_test.go | 34 ++-- execution/commitment/pbin_hash.go | 59 +++---- execution/commitment/pbin_hash_test.go | 27 ++- execution/commitment/pbin_hashsuite_test.go | 13 +- execution/commitment/pbin_hazard_test.go | 39 ++-- execution/commitment/pbin_keys.go | 63 +++---- execution/commitment/pbin_keys_test.go | 34 ++-- execution/commitment/pbin_oracle_test.go | 60 +++---- execution/commitment/pbin_overflow_test.go | 36 ++-- execution/commitment/pbin_patricia_hashed.go | 167 ++++++++---------- execution/commitment/pbin_process_test.go | 80 ++++----- execution/commitment/pbin_rootkey_test.go | 24 ++- execution/commitment/pbin_specengine_test.go | 20 +-- execution/commitment/pbin_specroots_test.go | 27 +-- execution/commitment/pbin_specvectors_test.go | 18 +- execution/commitment/pbin_state.go | 9 +- execution/commitment/pbin_state_test.go | 27 ++- execution/commitment/pbin_unfold_test.go | 44 ++--- execution/commitment/pbin_values.go | 13 +- execution/commitment/pbin_values_test.go | 6 +- execution/commitment/pbin_variant_test.go | 37 ++-- execution/commitment/pbin_verify_test.go | 45 ++--- .../commitment/pbin_vs_hex_compare_test.go | 17 +- execution/commitment/pbin_zerovalue_test.go | 39 ++-- execution/stagedsync/pbin_defer_test.go | 6 +- .../stagedsync/pbin_parallel_exec_test.go | 6 +- .../state/genesiswrite/pbin_genesis_test.go | 8 +- rpc/jsonrpc/pbin_hex_only_test.go | 4 +- 47 files changed, 632 insertions(+), 890 deletions(-) diff --git a/db/integrity/pbin_hex_only_test.go b/db/integrity/pbin_hex_only_test.go index 8855b943c19..4235077dac0 100644 --- a/db/integrity/pbin_hex_only_test.go +++ b/db/integrity/pbin_hex_only_test.go @@ -35,8 +35,8 @@ func withBinCommitment(t *testing.T, on bool) { statecfg.ExperimentalBinCommitment = on } -// The history checks recompute roots with the hex trie, so on a bin datadir they -// must refuse rather than report a mismatch against correct bin records. +// The history checks recompute roots with the hex trie: on a bin datadir they must +// refuse, not report a mismatch against correct bin records. func TestPBinCommitmentHistChecksRefuseBin(t *testing.T) { // No t.Parallel: mutates process-global statecfg flags. db := temporaltest.NewTestDB(t, datadir.New(t.TempDir())) diff --git a/db/state/execctx/pbin_options_test.go b/db/state/execctx/pbin_options_test.go index 0ed5d9690cb..8d1e4dae5f7 100644 --- a/db/state/execctx/pbin_options_test.go +++ b/db/state/execctx/pbin_options_test.go @@ -27,6 +27,7 @@ import ( "github.com/erigontech/erigon/execution/commitment" ) +// Mutates a process-global flag, so no test using it may run in parallel. func withBinCommitmentFlag(t *testing.T, on bool) { t.Helper() orig := statecfg.ExperimentalBinCommitment @@ -34,11 +35,9 @@ func withBinCommitmentFlag(t *testing.T, on bool) { statecfg.ExperimentalBinCommitment = on } -// The genesis-style option demotes only the experimental parallel/streaming tries; -// bin is a persisted datadir property, so demoting it would compute a hex block-0 -// root over a datadir the executor then reads as bin. +// Bin is a persisted datadir property, so WithoutParallelCommitment demotes only the +// experimental parallel/streaming tries: demoting bin would give a hex block-0 root. func TestPBinWithoutParallelCommitmentKeepsBin(t *testing.T) { - // No t.Parallel: mutates process-global statecfg flags. for _, tc := range []struct { name string flag commitment.TrieVariant @@ -67,10 +66,9 @@ func TestPBinWithoutParallelCommitmentKeepsBin(t *testing.T) { } } -// Paths that can only read hex branch records must fail loudly on a bin datadir -// instead of reinterpreting bit-path records as hex ones. +// WithHexCommitmentOnly callers can only read hex branch records, so a bin datadir +// must fail loudly instead of having its bit-path records read as hex ones. func TestPBinHexOnlyCommitmentRefusesBin(t *testing.T) { - // No t.Parallel: mutates process-global statecfg flags. withBinCommitmentFlag(t, true) db := newTestDb(t, 16) @@ -84,7 +82,6 @@ func TestPBinHexOnlyCommitmentRefusesBin(t *testing.T) { } func TestPBinHexOnlyCommitmentDemotesParallel(t *testing.T) { - // No t.Parallel: mutates process-global statecfg flags. withBinCommitmentFlag(t, false) withCommitmentFlag(t, commitment.VariantParallelHexPatricia) diff --git a/db/state/pbin_variant_persist_test.go b/db/state/pbin_variant_persist_test.go index 978baaa0650..cb47fbed63d 100644 --- a/db/state/pbin_variant_persist_test.go +++ b/db/state/pbin_variant_persist_test.go @@ -31,8 +31,7 @@ import ( "github.com/erigontech/erigon/execution/commitment" ) -// The tests below mutate process-wide statecfg flags, so none of them may run -// in parallel; save/restore keeps the rest of the package unaffected. +// Mutates process-wide statecfg flags, so no test in this file may run in parallel. func pbinWithVariantFlags(t *testing.T, bin, streaming, parallel bool) { t.Helper() origBin := statecfg.ExperimentalBinCommitment @@ -88,8 +87,7 @@ func TestPBinVariantFlaglessRestartStaysBin(t *testing.T) { _, err := ResolveErigonDBSettings(dirs, log.New(), true) require.NoError(t, err) - // Flagless restart: the persisted trie_variant wins over the CLI default - // and is adopted process-wide. + // Flagless restart: the persisted trie_variant wins over the CLI default, process-wide. statecfg.ExperimentalBinCommitment = false settings, err := ResolveErigonDBSettings(dirs, log.New(), true) require.NoError(t, err) @@ -182,10 +180,9 @@ func TestPBinVariantFreshWithDownloaderPersistsBin(t *testing.T) { require.Equal(t, uint64(config3.DefaultStepSize), written.StepSize) } -// A chain with no published snapshot hashes gets an empty preverified.toml -// committed by the snapshots stage. Without a persisted variant that reads as a -// legacy datadir at the next resolve, and the bin run is refused on its own -// fresh datadir. +// The snapshots stage writes an empty preverified.toml for a chain with no published +// snapshot hashes. Without a persisted variant that reads as a legacy datadir at the +// next resolve, and the bin run gets refused on its own fresh datadir. func TestPBinVariantSurvivesEmptyPreverifiedFromSnapshotsStage(t *testing.T) { pbinWithVariantFlags(t, true, false, false) dirs := datadir.New(t.TempDir()) diff --git a/execution/commitment/backtester/pbin_m1a_test.go b/execution/commitment/backtester/pbin_m1a_test.go index 00b7c80222b..fe8433844b7 100644 --- a/execution/commitment/backtester/pbin_m1a_test.go +++ b/execution/commitment/backtester/pbin_m1a_test.go @@ -15,10 +15,9 @@ // along with Erigon. If not, see . // These tests drive the bin commitment trie over a real MDBX datadir with no -// consensus layer, so no header validates the roots from outside. The only -// cross-check available is determinism: a forward run and a rebuild that has only -// the account and storage domains to work from must agree. A green run therefore -// says the engine is self-consistent, not that it is correct. +// external oracle for the roots. The cross-check is determinism: a forward run +// and a rebuild that has only the account and storage domains to work from must +// agree. package backtester_test import ( @@ -54,8 +53,8 @@ const ( pbinM1ASlots = 4 ) -// pbinM1ABinVariant makes PickTrieVariant() resolve to the bin trie. The flag is -// process-wide, so these tests never run in parallel. +// Makes PickTrieVariant() resolve to the bin trie. The flag is process-wide, so +// these tests never run in parallel. func pbinM1ABinVariant(t *testing.T) { t.Helper() orig := statecfg.ExperimentalBinCommitment @@ -68,8 +67,7 @@ func pbinM1ANewAgg(t *testing.T, rawDB kv.RwDB, dirs datadir.Dirs, stepSize uint agg := state.NewTest(dirs).StepSize(stepSize).Logger(log.New()).MustOpen(t.Context(), rawDB) t.Cleanup(agg.Close) // Referenced branches rewrite bytes at hex cell offsets during merge. Production - // refuses the combination when resolving settings, which a test aggregator built - // straight from NewTest does not go through. + // refuses that combination when resolving settings; NewTest bypasses it. agg.ForTestReferencesInCommitmentBranches(kv.CommitmentDomain, false) require.NoError(t, agg.OpenFolder()) return agg @@ -89,8 +87,8 @@ func pbinM1ANewDatadir(t *testing.T, stepSize uint64) (kv.TemporalRwDB, *state.A return db, agg, dirs } -// pbinM1AReopen closes the aggregator and reopens it over the same folder — the -// file-visibility half of a node restart. +// Reopens the aggregator over the same folder — the file-visibility half of a +// node restart. func pbinM1AReopen(t *testing.T, db kv.TemporalRwDB, agg *state.Aggregator, dirs datadir.Dirs, stepSize uint64) (kv.TemporalRwDB, *state.Aggregator) { t.Helper() agg.Close() @@ -116,8 +114,8 @@ func pbinM1ASlotKey(addr []byte, j int) []byte { return k } -// pbinM1ABinSharedDomains opens a SharedDomains and pins that it really runs the -// bin trie — a hex fallback would make every assertion below vacuous. +// Pins that the bin trie is really in play — a hex fallback would make every +// assertion below vacuous. func pbinM1ABinSharedDomains(t *testing.T, tx kv.TemporalTx) *execctx.SharedDomains { t.Helper() sd, err := execctx.NewSharedDomains(t.Context(), tx, log.New()) @@ -126,9 +124,9 @@ func pbinM1ABinSharedDomains(t *testing.T, tx kv.TemporalTx) *execctx.SharedDoma return sd } -// pbinM1AForwardRun writes accounts and storage for txNums [fromTx, toTx), saving -// the commitment state at every step boundary. It returns the root at each of those -// boundaries keyed by the boundary txNum, plus the last root. +// Writes accounts and storage for txNums [fromTx, toTx), saving the commitment +// state at every step boundary. Returns the root at each boundary keyed by the +// boundary txNum, plus the last root. func pbinM1AForwardRun(t *testing.T, db kv.TemporalRwDB, stepSize, fromTx, toTx uint64) (map[uint64][]byte, []byte) { t.Helper() rwTx, err := db.BeginTemporalRw(t.Context()) @@ -172,8 +170,8 @@ func pbinM1AForwardRun(t *testing.T, db kv.TemporalRwDB, stepSize, fromTx, toTx return roots, last } -// pbinM1ARecomputeRoot re-folds the whole tree from the account and storage -// domains: every leaf is touched, so no leaf value comes from a branch record. +// Re-folds the whole tree with every leaf touched, so no leaf value comes from +// a branch record. func pbinM1ARecomputeRoot(t *testing.T, db kv.TemporalRwDB) []byte { t.Helper() rwTx, err := db.BeginTemporalRw(t.Context()) @@ -198,8 +196,8 @@ func pbinM1ARecomputeRoot(t *testing.T, db kv.TemporalRwDB) []byte { return root } -// pbinM1ARestoredRoot returns the root a freshly opened SharedDomains restores -// from the saved commitment state, without folding anything. +// The root a freshly opened SharedDomains restores from the saved commitment +// state, without folding anything. func pbinM1ARestoredRoot(t *testing.T, db kv.TemporalRwDB) []byte { t.Helper() tx, err := db.BeginTemporalRw(t.Context()) @@ -213,9 +211,9 @@ func pbinM1ARestoredRoot(t *testing.T, db kv.TemporalRwDB) []byte { return root } -// pbinM1ACollatedTxNum returns the first txNum not yet in the account and storage -// files. Collation always leaves the newest step in the db, so a files-only rebuild -// reproduces the root as of this boundary, not the last one the forward run computed. +// The first txNum not yet in the account and storage files. Collation always +// leaves the newest step in the db, so a files-only rebuild reproduces the root +// as of this boundary, not the last one the forward run computed. func pbinM1ACollatedTxNum(t *testing.T, db kv.TemporalRwDB) uint64 { t.Helper() tx, err := db.BeginTemporalRo(t.Context()) @@ -228,8 +226,6 @@ func pbinM1ACollatedTxNum(t *testing.T, db kv.TemporalRwDB) uint64 { return accTxNum } -// pbinM1ABranchRecords reads the latest commitment branch records, skipping the -// commitment-state record. func pbinM1ABranchRecords(t *testing.T, db kv.TemporalRwDB) map[string][]byte { t.Helper() tx, err := db.BeginTemporalRo(t.Context()) @@ -251,8 +247,8 @@ func pbinM1ABranchRecords(t *testing.T, db kv.TemporalRwDB) map[string][]byte { return out } -// pbinM1AFileServedRecords counts the branch records that are gone from the db -// table, so a latest read of them can only come from the collated files. +// Counts branch records gone from the db table, so a latest read of them can +// only come from the collated files. func pbinM1AFileServedRecords(t *testing.T, db kv.TemporalRwDB, records map[string][]byte) int { t.Helper() tx, err := db.BeginTemporalRo(t.Context()) @@ -270,9 +266,8 @@ func pbinM1AFileServedRecords(t *testing.T, db kv.TemporalRwDB, records map[stri return fromFiles } -// pbinM1AWipeCommitment removes every commitment record from the db tables and -// every commitment file from the snapshot dir, so a rebuild has to derive the tree -// from the account and storage domains alone. +// Wipes commitment from the db tables and the snapshot dir, so a rebuild has to +// derive the tree from the account and storage domains alone. func pbinM1AWipeCommitment(t *testing.T, db kv.TemporalRwDB, agg *state.Aggregator, dirs datadir.Dirs, stepSize uint64) (kv.TemporalRwDB, *state.Aggregator) { t.Helper() rwTx, err := db.BeginRw(t.Context()) @@ -299,7 +294,7 @@ func pbinM1AWipeCommitment(t *testing.T, db kv.TemporalRwDB, agg *state.Aggregat require.NoError(t, dir.RemoveFile(p)) base := strings.TrimSuffix(p, ".kv") for _, ext := range []string{".kvi", ".kvei", ".bt"} { - _ = dir.RemoveFile(base + ext) // best-effort, may not exist + _ = dir.RemoveFile(base + ext) // accessors may not exist } } @@ -309,9 +304,6 @@ func pbinM1AWipeCommitment(t *testing.T, db kv.TemporalRwDB, agg *state.Aggregat return newDB, newAgg } -// TestPBinM1AForwardRunMatchesRebuildFromDomains is the M1a gate: over the same -// state the incremental forward fold and a rebuild that starts from wiped -// commitment must produce the same root. func TestPBinM1AForwardRunMatchesRebuildFromDomains(t *testing.T) { pbinM1ABinVariant(t) txCount := 4 * pbinM1AStepSize @@ -343,9 +335,8 @@ func TestPBinM1AForwardRunMatchesRebuildFromDomains(t *testing.T) { "the rebuilt commitment records must fold back to the forward root") } -// TestPBinM1ARestartResumesToSameRoot restarts between two halves of the same -// input. The second half touches only its own keys, so the root can only come out -// right if the saved trie state and the persisted branch records both round-trip. +// The second half touches only its own keys, so the root can only come out right +// if the saved trie state and the persisted branch records both round-trip. func TestPBinM1ARestartResumesToSameRoot(t *testing.T) { pbinM1ABinVariant(t) half := 2 * pbinM1AStepSize @@ -365,9 +356,6 @@ func TestPBinM1ARestartResumesToSameRoot(t *testing.T) { require.Equal(t, wantRoot, resumedRoot, "a restart mid-run must resume to the uninterrupted root") } -// TestPBinM1ABranchRecordsSurviveCollationAndMerge pins that collation and merge -// are byte-transparent for bin branch records. The db is pruned after collation, so -// the post-merge reads come from the files. func TestPBinM1ABranchRecordsSurviveCollationAndMerge(t *testing.T) { pbinM1ABinVariant(t) txCount := 4 * pbinM1AStepSize diff --git a/execution/commitment/commitmentdb/pbin_code_test.go b/execution/commitment/commitmentdb/pbin_code_test.go index 1faa703bd74..b4e8949e219 100644 --- a/execution/commitment/commitmentdb/pbin_code_test.go +++ b/execution/commitment/commitmentdb/pbin_code_test.go @@ -34,9 +34,8 @@ func pbinTestCode(n int) []byte { return code } -// TestPBinTrieContextCodeReadsCodeDomain pins the read the binary trie's code -// chunking rests on. Chunk leaves hold bytecode, which no other trie needs and -// no account read returns. +// Code chunk leaves hold raw bytecode, which no account read returns and no +// other trie needs. func TestPBinTrieContextCodeReadsCodeDomain(t *testing.T) { t.Parallel() @@ -53,11 +52,10 @@ func TestPBinTrieContextCodeReadsCodeDomain(t *testing.T) { require.Empty(t, absent) } -// TestPBinSharedDomainsCommitsCodeBearingAccount is the wiring end to end: the -// engine chunks code it reads through the trie context, and cross-checks the -// chunk count against the code_size it hashes, so a context that cannot serve -// code fails the commit rather than committing a code-less tree. Chunk values -// themselves are pinned against the reference tree in the commitment package. +// The engine cross-checks the chunk count against the code_size it hashes, so a +// context that cannot serve code fails the commit instead of committing a +// code-less tree. Chunk values are pinned against the reference tree in the +// commitment package. func TestPBinSharedDomainsCommitsCodeBearingAccount(t *testing.T) { t.Parallel() @@ -74,8 +72,7 @@ func TestPBinSharedDomainsCommitsCodeBearingAccount(t *testing.T) { withCode, err := sd.ComputeCommitment(t.Context(), tx, false, 0, 0, "pbin-code", nil) require.NoError(t, err) - // The same account with one byte of code roots differently: the chunk leaves - // are part of what is committed, not a side table. + // Chunk leaves are part of what is committed, not a side table. short := pbinTestCode(1) shortSd, shortTx := pbinCodeSizeSharedDomains(t, []execctx.SharedDomainOption{execctx.WithTrieConfig(cfg)}, addr, pbinCodeSizeAccount(crypto.Keccak256Hash(short)), short) @@ -84,9 +81,8 @@ func TestPBinSharedDomainsCommitsCodeBearingAccount(t *testing.T) { require.NotEqual(t, withShortCode, withCode) } -// TestPBinSharedDomainsCommitsCodeBeyondHeader is the domain-layer half of the -// code zone: a contract whose code outgrows the account header commits, and the -// chunks past the header are part of what it commits. +// The account header holds the first 128 code chunks; this code is one byte +// past that, so it spills into the code zone. func TestPBinSharedDomainsCommitsCodeBeyondHeader(t *testing.T) { t.Parallel() @@ -101,8 +97,8 @@ func TestPBinSharedDomainsCommitsCodeBeyondHeader(t *testing.T) { overflowing, err := sd.ComputeCommitment(t.Context(), tx, false, 0, 0, "pbin-code-overflow", nil) require.NoError(t, err) - // Dropping the one byte that spills into the code zone must change the root: - // the overflow chunk is committed, not silently left out. + // Dropping the spilling byte must change the root: the overflow chunk is + // committed, not silently left out. header := code[:len(code)-1] headerSd, headerTx := pbinCodeSizeSharedDomains(t, []execctx.SharedDomainOption{execctx.WithTrieConfig(cfg)}, addr, pbinCodeSizeAccount(crypto.Keccak256Hash(header)), header) diff --git a/execution/commitment/commitmentdb/pbin_codesize_test.go b/execution/commitment/commitmentdb/pbin_codesize_test.go index 03f142b2483..2acca46cb11 100644 --- a/execution/commitment/commitmentdb/pbin_codesize_test.go +++ b/execution/commitment/commitmentdb/pbin_codesize_test.go @@ -40,8 +40,6 @@ func pbinCodeSizeAddr(i byte) []byte { return a } -// pbinCodeSizeSharedDomains opens a SharedDomains over a fresh datadir holding -// one account and, when non-nil, one CodeDomain entry for it. func pbinCodeSizeSharedDomains(t *testing.T, opts []execctx.SharedDomainOption, addr []byte, acc *accounts.Account, code []byte) (*execctx.SharedDomains, kv.TemporalTx) { t.Helper() db := pbinNewTestDb(t) @@ -60,8 +58,6 @@ func pbinCodeSizeSharedDomains(t *testing.T, opts []execctx.SharedDomainOption, return sd, tx } -// pbinCodeSizeTrieContext builds a read context over that state, with the code -// size read switched the way the named variant would switch it. func pbinCodeSizeTrieContext(t *testing.T, readCodeSize bool, addr []byte, acc *accounts.Account, code []byte) *commitmentdb.TrieContext { t.Helper() sd, tx := pbinCodeSizeSharedDomains(t, nil, addr, acc, code) @@ -74,8 +70,7 @@ func pbinCodeSizeAccount(codeHash common.Hash) *accounts.Account { return &accounts.Account{Nonce: 3, Balance: *uint256.NewInt(77), CodeHash: accounts.InternCodeHash(codeHash)} } -// TestPBinTrieContextAccountReadsCodeSize is where BASIC_DATA's code_size comes -// from: the length of the account's code in the CodeDomain. +// BASIC_DATA's code_size is the length of the account's code in the CodeDomain. func TestPBinTrieContextAccountReadsCodeSize(t *testing.T) { t.Parallel() @@ -89,8 +84,8 @@ func TestPBinTrieContextAccountReadsCodeSize(t *testing.T) { require.NotZero(t, u.Flags&commitment.CodeUpdate) } -// TestPBinTrieContextLeavesCodeSizeZeroForHex pins the gate: the hex trie does -// not hash code_size, so it must not pay for the extra CodeDomain read. +// The hex trie does not hash code_size, so it must not pay for the extra +// CodeDomain read. func TestPBinTrieContextLeavesCodeSizeZeroForHex(t *testing.T) { t.Parallel() @@ -103,10 +98,9 @@ func TestPBinTrieContextLeavesCodeSizeZeroForHex(t *testing.T) { require.Zero(t, u.CodeSize) } -// TestPBinTrieContextIgnoresClearedDelegationResidue decides H9: a cleared -// EIP-7702 delegation leaves code in the CodeDomain that no longer belongs to -// the account. code_size follows the account's own code hash, so the residue -// changes nothing — otherwise a tolerated inconsistency would move the root. +// A cleared EIP-7702 delegation leaves code in the CodeDomain that no longer +// belongs to the account. code_size follows the account's own code hash, so the +// residue must not move the root. func TestPBinTrieContextIgnoresClearedDelegationResidue(t *testing.T) { t.Parallel() @@ -120,8 +114,7 @@ func TestPBinTrieContextIgnoresClearedDelegationResidue(t *testing.T) { require.Equal(t, empty.CodeHash, u.CodeHash) } -// TestPBinTrieContextRefusesCodeBearingAccountWithoutCode is H9's other half: a -// code hash with no code behind it (an eth_simulateV1 overlay, a truncated +// A code hash with no code behind it (an eth_simulateV1 overlay, a truncated // datadir) would hash as code_size 0 and silently produce a wrong root. func TestPBinTrieContextRefusesCodeBearingAccountWithoutCode(t *testing.T) { t.Parallel() @@ -133,9 +126,7 @@ func TestPBinTrieContextRefusesCodeBearingAccountWithoutCode(t *testing.T) { require.ErrorContains(t, err, "code missing") } -// TestPBinSharedDomainsReadsCodeSizeUnderBin ties the variant to the read: only -// the bin trie needs code_size, so only a bin SharedDomains must insist the code -// is there. +// Only a bin SharedDomains must insist the code is there. func TestPBinSharedDomainsReadsCodeSizeUnderBin(t *testing.T) { t.Parallel() diff --git a/execution/commitment/commitmentdb/pbin_nocache_test.go b/execution/commitment/commitmentdb/pbin_nocache_test.go index 4d2b9c827a7..cccc0d24f5d 100644 --- a/execution/commitment/commitmentdb/pbin_nocache_test.go +++ b/execution/commitment/commitmentdb/pbin_nocache_test.go @@ -57,9 +57,7 @@ func pbinRecoverMessage(t *testing.T, fn func()) (msg string) { return "" } -// TestPBinCtorRefusesSharedBranchCache pins the structural assert for H1: a -// bin-variant commitment context over a SharedDomains that shares the branch -// cache must be refused at construction, by name, before anything else runs. +// Why bin must not share the cache: TestPBinBranchCacheTrunkSlotCollision. func TestPBinCtorRefusesSharedBranchCache(t *testing.T) { t.Parallel() @@ -71,12 +69,11 @@ func TestPBinCtorRefusesSharedBranchCache(t *testing.T) { require.Contains(t, msg, "branch cache") } -// TestPBinBranchCacheTrunkSlotCollision demonstrates H1, the reason the bin -// variant must not share the BranchCache. The trunk-slot index reads a prefix -// as a hex compact path, which is injective for hex keys; a pbin bit-path key -// is packed MSB-first bits plus a trailing bitLen%8 byte, so distinct short -// paths land on one slot and the cache serves another node's record as a -// well-formed hit. +// The reason the bin variant must not share the BranchCache: the trunk-slot +// index reads a prefix as a hex compact path, injective only for hex keys. A +// pbin bit-path key is packed MSB-first bits plus a trailing bitLen%8 byte, so +// distinct short paths land on one slot and the cache serves another node's +// record as a well-formed hit. func TestPBinBranchCacheTrunkSlotCollision(t *testing.T) { t.Parallel() @@ -109,9 +106,8 @@ func pbinNewTestDb(tb testing.TB) kv.TemporalRwDB { return tdb } -// TestPBinSharedDomainsHasNoSharedBranchCache checks the execctx wiring: a -// bin-variant SharedDomains over an aggregator whose AggTx provides the shared -// BranchCache must reach the commitment-context ctor without it, and must open. +// execctx must strip the AggTx's shared BranchCache before the bin commitment +// context is constructed, and still open. func TestPBinSharedDomainsHasNoSharedBranchCache(t *testing.T) { t.Parallel() diff --git a/execution/commitment/commitmentdb/pbin_state_header_test.go b/execution/commitment/commitmentdb/pbin_state_header_test.go index 225cc9c6033..394a613f0a9 100644 --- a/execution/commitment/commitmentdb/pbin_state_header_test.go +++ b/execution/commitment/commitmentdb/pbin_state_header_test.go @@ -45,10 +45,8 @@ func pbinStateTestCtx(t *testing.T, variant commitment.TrieVariant) *SharedDomai return sdc } -// TestPBinCommitmentStateHeaderMatchesHex pins the commitment-state record -// layout across variants: the 16-byte txNum‖blockNum header is read raw and -// variant-blind (DecodeTxBlockNums, LatestBlockNumWithCommitment), so the bin -// variant must produce it byte-identically to hex. +// DecodeTxBlockNums and LatestBlockNumWithCommitment read the 16-byte +// txNum‖blockNum header raw and variant-blind. func TestPBinCommitmentStateHeaderMatchesHex(t *testing.T) { t.Parallel() diff --git a/execution/commitment/commitmentdb/pbin_unsupported_test.go b/execution/commitment/commitmentdb/pbin_unsupported_test.go index 2f16202755e..adc561cce90 100644 --- a/execution/commitment/commitmentdb/pbin_unsupported_test.go +++ b/execution/commitment/commitmentdb/pbin_unsupported_test.go @@ -40,10 +40,8 @@ func pbinRecoveredError(t *testing.T, fn func()) (err error) { return nil } -// TestPBinRefusesDeferredCommitmentUpdates pins the enabling side of the -// deferred-update path: hex and parallel take the request, bin refuses it by -// name. Silently accepting it would leave the flag set with no trie honouring -// it, so Process would apply inline while the caller waited for a flush. +// Silently accepting the request would leave the flag set with no trie +// honouring it: Process would apply inline while the caller waited for a flush. func TestPBinRefusesDeferredCommitmentUpdates(t *testing.T) { t.Parallel() @@ -59,9 +57,8 @@ func TestPBinRefusesDeferredCommitmentUpdates(t *testing.T) { require.NoError(t, pbinRecoveredError(t, func() { binCtx.SetDeferCommitmentUpdates(false) })) } -// TestPBinComputeCommitmentRefusesDeferredTake covers the taking side: were the -// flag ever set under bin, the post-Process type switch would find no trie -// carrying deferred updates and hand back an empty pendingUpdate. +// Were the flag ever set under bin, the post-Process type switch would find no +// trie carrying deferred updates and hand back an empty pendingUpdate. func TestPBinComputeCommitmentRefusesDeferredTake(t *testing.T) { t.Parallel() @@ -72,9 +69,8 @@ func TestPBinComputeCommitmentRefusesDeferredTake(t *testing.T) { require.ErrorIs(t, err, commitment.ErrPBinUnsupported) } -// TestPBinComputeCommitmentRefusesTrieTrace: the trace records branch records -// and replays them through the hex trie, so a bin trace would replay as a -// different tree. The trace is env-gated, so refusing costs a normal run nothing. +// The trace replays recorded branch records through the hex trie, so a bin +// trace would replay as a different tree. func TestPBinComputeCommitmentRefusesTrieTrace(t *testing.T) { prev := dbg.TrieTraceFile dbg.TrieTraceFile = t.TempDir() + "/trie-trace.toml" @@ -89,9 +85,8 @@ func TestPBinComputeCommitmentRefusesTrieTrace(t *testing.T) { require.NoError(t, err) } -// TestPBinRefusesCollapseTracer guards the witness path: the tracer only ever -// reaches a HexPatriciaHashed, so under bin it was installed nowhere and the -// caller collected no collapse paths. +// The tracer only ever reaches a HexPatriciaHashed, so under bin it would be +// installed nowhere and the caller would collect no collapse paths. func TestPBinRefusesCollapseTracer(t *testing.T) { t.Parallel() @@ -107,9 +102,8 @@ func TestPBinRefusesCollapseTracer(t *testing.T) { require.NoError(t, pbinRecoveredError(t, func() { binCtx.SetCollapseTracer(nil) }), "clearing must stay allowed") } -// TestPBinBranchChildCountRefusesBin: the prefix is a hex nibble path compacted -// into a commitment key, which addresses no bin record — the read used to miss -// and report a child count of zero. +// The prefix is a hex nibble path compacted into a commitment key, which +// addresses no bin record — the read would miss and report a child count of zero. func TestPBinBranchChildCountRefusesBin(t *testing.T) { t.Parallel() diff --git a/execution/commitment/pbin_bitpath.go b/execution/commitment/pbin_bitpath.go index de8c9289f2c..6588422a7d0 100644 --- a/execution/commitment/pbin_bitpath.go +++ b/execution/commitment/pbin_bitpath.go @@ -126,9 +126,7 @@ func (p *pbinBitpath) hasPrefix(o *pbinBitpath) bool { } // pbinCommonPrefixBitsAt reports how many leading bits of prefix agree with key -// read from bit `from`, never past the end of either operand. It is the one -// divergence primitive: bits past a path's length are masked to zero, so a whole -// word can be compared at a time and the answer clamped to what both hold. +// read from bit `from`, clamped to what both operands hold. func pbinCommonPrefixBitsAt(key *pbinBitpath, from int16, prefix *pbinBitpath) int16 { limit := min(key.bitLen-from, prefix.bitLen) if limit <= 0 { @@ -150,7 +148,7 @@ func pbinCommonPrefixBitsAt(key *pbinBitpath, from int16, prefix *pbinBitpath) i return min(n, limit) } -// pbinAppendPackedBits appends the path's bits MSB-first, zero-padded to a byte +// appendPackedBits appends the path's bits MSB-first, zero-padded to a byte // boundary. func (p *pbinBitpath) appendPackedBits(dst []byte) []byte { for i := range (int(p.bitLen) + 7) / 8 { @@ -167,11 +165,11 @@ var ( errPBinNonCanonicalPad = errors.New("pbin: non-canonical padding in bit-path key") ) -// pbinAppendBitPath appends the DB key for p: packed bits followed by a single -// byte holding bitLen mod 8. The trailing count is a suffix on purpose — a -// leading length field would scatter one subtree's records across the keyspace, -// whereas this layout keeps a subtree contiguous. It does not order ancestors -// before descendants, and callers must not assume it does. +// pbinAppendBitPath appends the DB key for p: packed bits followed by one byte +// holding bitLen mod 8. The count is a suffix so that a subtree stays +// contiguous; a leading length field would scatter its records across the +// keyspace. The order is not ancestors-before-descendants, and callers must not +// assume it is. func pbinAppendBitPath(dst []byte, p *pbinBitpath) []byte { return append(p.appendPackedBits(dst), byte(p.bitLen%8)) } @@ -180,8 +178,8 @@ func pbinEncodeBitPath(p *pbinBitpath) []byte { return pbinAppendBitPath(make([]byte, 0, (int(p.bitLen)+7)/8+1), p) } -// pbinDecodeBitPath is the inverse of pbinAppendBitPath and rejects every -// non-canonical spelling, so one path has exactly one DB key. +// pbinDecodeBitPath inverts pbinAppendBitPath, rejecting non-canonical +// spellings so that one path has exactly one DB key. func pbinDecodeBitPath(buf []byte) (pbinBitpath, error) { var p pbinBitpath if len(buf) == 0 { diff --git a/execution/commitment/pbin_bitpath_test.go b/execution/commitment/pbin_bitpath_test.go index de1ed712879..2ffea095594 100644 --- a/execution/commitment/pbin_bitpath_test.go +++ b/execution/commitment/pbin_bitpath_test.go @@ -71,9 +71,8 @@ func TestPBinCommonPrefixBits(t *testing.T) { } } -// A 272-bit account key that is a bitwise prefix of a 528-bit storage key must -// report exactly 272 shared bits: without clamping by min(aLen, bLen) the words -// keep agreeing past the shorter path's end (guards H10). +// Without clamping by min(aLen, bLen) the words keep agreeing past the shorter +// path's end, so an account key that prefixes a storage key over-reports. func TestPBinCommonPrefixBits_ShorterPathIsPrefix(t *testing.T) { t.Parallel() @@ -84,8 +83,7 @@ func TestPBinCommonPrefixBits_ShorterPathIsPrefix(t *testing.T) { require.Equal(t, int16(272), pbinCommonPrefixBitsAt(&long, 0, &short)) } -// Words carrying set bits beyond bitLen must not be read as real path bits -// (guards H10). +// Words carrying set bits beyond bitLen must not be read as real path bits. func TestPBinCommonPrefixBits_IgnoresBitsBeyondBitLen(t *testing.T) { t.Parallel() @@ -202,7 +200,7 @@ func TestPBinBitPathCodecRejects(t *testing.T) { } // The commitment domain stores its state blob under the literal key "state", so -// no encoded bit path may collide with it (guards H5). +// no encoded bit path may collide with it. func TestPBinBitPathNeverEncodesToStateKey(t *testing.T) { t.Parallel() @@ -240,8 +238,8 @@ func FuzzPBinBitPathCodec(f *testing.F) { }) } -// The word-at-a-time divergence scan must agree with a bit-by-bit walk at every -// offset, including the ones that straddle a word boundary. +// The word-at-a-time scan must agree with a bit-by-bit walk at every offset, +// including the ones that straddle a word boundary. func TestPBinCommonPrefixBitsAt_MatchesNaiveScan(t *testing.T) { t.Parallel() diff --git a/execution/commitment/pbin_branch.go b/execution/commitment/pbin_branch.go index 4b66401422f..aa4987454b6 100644 --- a/execution/commitment/pbin_branch.go +++ b/execution/commitment/pbin_branch.go @@ -25,7 +25,7 @@ import ( "github.com/erigontech/erigon/common/length" ) -// pbinCellBits are the only child slots a binary node has. +// pbinCellBits masks the only child slots a binary node has. const pbinCellBits = 0b11 type pbinCellFields uint8 @@ -52,14 +52,13 @@ var ( errPBinCellMaps = errors.New("pbin: branch maps address more than two cells") ) -// pbinBranchEncoder serialises a binary node. The payload is deliberately not -// BranchData: a 66-byte tree-key prefix does not fit the shared codec's cell -// fields, and PatriciaContext moves branch payloads as opaque bytes. +// pbinBranchEncoder serialises a binary node. The payload is not BranchData: a +// 66-byte tree-key prefix does not fit the shared codec's cell fields, and +// PatriciaContext moves branch payloads as opaque bytes. // // Every record carries both child cells, so a record read back replaces its -// predecessor outright and no merge-with-previous path exists — at arity 2 the -// untouched sibling is the whole other half of the subtree, and merging is what -// loses it. +// predecessor outright and there is no merge-with-previous path: at arity 2 the +// untouched sibling is the whole other half of the subtree, and merging loses it. type pbinBranchEncoder struct { buf []byte } @@ -132,8 +131,8 @@ func pbinAppendLenAndVal(dst, val []byte) []byte { return append(binary.AppendUvarint(dst, uint64(len(val))), val...) } -// pbinDecodeBranch fills both cells from a record, rejecting every spelling the -// encoder would not produce so a record has one canonical form. +// pbinDecodeBranch fills both cells from a record. It rejects every spelling the +// encoder would not produce, so a record has one canonical form. func pbinDecodeBranch(data []byte, cells *[2]pbinCell) (touchMap, afterMap uint16, err error) { cells[0].reset() cells[1].reset() @@ -172,8 +171,8 @@ func pbinDecodeCell(data []byte, pos int, c *pbinCell) (int, error) { switch fields & pbinFieldKind { case pbinFieldLeaf: c.kind = pbinNodeLeaf - // A leaf whose value has no source hashes a zero-valued state instead of - // failing, so the shape is rejected here rather than reaching the hasher. + // A leaf whose value has no source would hash a zero-valued state instead of + // failing, so reject the shape here rather than let it reach the hasher. switch fields & pbinFieldValue { case pbinFieldAccountAddr, pbinFieldStorageAddr, pbinFieldLeafValue: default: @@ -219,9 +218,9 @@ func pbinDecodeCell(data []byte, pos int, c *pbinCell) (int, error) { return pos, nil } -// pbinDecodePrefix reads the explicit bit count and exactly the bytes it -// implies. The count is the authority: a byte length left to speak for itself -// would carry up to seven pad bits into the branch hash. +// pbinDecodePrefix trusts the explicit bit count, not the byte length: pad bits +// left to speak for themselves would carry up to seven extra bits into the +// branch hash. func pbinDecodePrefix(data []byte, pos int, c *pbinCell) (int, error) { bitLen, n := binary.Uvarint(data[pos:]) if n <= 0 { @@ -258,8 +257,6 @@ func pbinDecodeFixedVal(data []byte, pos int, dst []byte, want int) (int, error) return pos + want, nil } -// pbinCheckCellMaps enforces the arity: a binary node has cells 0 and 1 and -// nothing else, in either map. func pbinCheckCellMaps(touchMap, afterMap uint16) error { if (touchMap|afterMap)&^pbinCellBits != 0 { return fmt.Errorf("%w: touch %016b after %016b", errPBinCellMaps, touchMap, afterMap) diff --git a/execution/commitment/pbin_cell.go b/execution/commitment/pbin_cell.go index 8e1e6d25f53..a4c7793ec1a 100644 --- a/execution/commitment/pbin_cell.go +++ b/execution/commitment/pbin_cell.go @@ -31,14 +31,9 @@ const ( pbinNodeBranch ) -// pbinCell is one of the two child slots of a binary node. -// -// It carries a single prefix, unlike the hex engine's cell: HPH keeps hashed and -// plain key spaces apart because it navigates in one and stores plain keys in -// the other, whereas PBin derives the tree key from the plain key on demand, so -// the one prefix is always tree-key-space bits. There is no memoized leaf hash -// either — H(0x00||key||value) commits the complete key and has nothing worth -// caching. +// pbinCell is one of the two child slots of a binary node. Its prefix is always +// tree-key-space bits: unlike the hex engine's cell it needs no second key +// space, because PBin derives the tree key from the plain key on demand. // // A branch cell's prefix is inside its hash, so re-cutting the prefix // invalidates it. Two invariants keep that from going unnoticed: a non-zero @@ -86,9 +81,8 @@ type pbinGrid struct { activeRows int } -// resetForReuse clears only the rows the finished run left live. Rows above -// activeRows keep stale cells, which is safe because unfold initializes a row -// before anything reads it. +// resetForReuse clears only the rows below activeRows. The stale cells above +// are safe because unfold initializes a row before anything reads it. func (g *pbinGrid) resetForReuse() { g.root.reset() for row := range g.activeRows { @@ -103,9 +97,9 @@ func (g *pbinGrid) resetForReuse() { g.activeRows = 0 } -// prevRecordFor is what the store holds at a row's record key: the bytes the -// row unfolded from, or zero-length when it had no record. Never nil, so the -// write layer takes it as the known previous value instead of reading its own. +// prevRecordFor returns the bytes the row unfolded from, zero-length when it had +// no record. Never nil, so the write layer takes it as the known previous value +// instead of reading the store itself. func (g *pbinGrid) prevRecordFor(row int) []byte { if g.prevRecord[row] == nil { return []byte{} diff --git a/execution/commitment/pbin_cell_test.go b/execution/commitment/pbin_cell_test.go index d52690f97f6..3e5aeabb25e 100644 --- a/execution/commitment/pbin_cell_test.go +++ b/execution/commitment/pbin_cell_test.go @@ -33,8 +33,6 @@ func pbinTestEmptyCell() pbinCell { return c } -// pbinTestBranchCell builds a branch-pointing cell with a prefix of the given -// bit length and a distinguishable hash. func pbinTestBranchCell(pattern byte, bitLen int16) pbinCell { c := pbinTestEmptyCell() c.kind = pbinNodeBranch @@ -46,8 +44,7 @@ func pbinTestBranchCell(pattern byte, bitLen int16) pbinCell { return c } -// pbinTestLeafCell builds a leaf cell carrying a storage plain key, the widest -// plain key a cell holds. +// pbinTestLeafCell carries a storage plain key — the widest a cell holds. func pbinTestLeafCell(pattern byte, bitLen int16) pbinCell { c := pbinTestBranchCell(pattern, bitLen) c.kind = pbinNodeLeaf @@ -58,8 +55,8 @@ func pbinTestLeafCell(pattern byte, bitLen int16) pbinCell { return c } -// pbinTestChunkLeafCell builds the one leaf shape that carries its value in the -// record instead of a plain key: a code chunk. +// pbinTestChunkLeafCell is the one leaf shape carrying its value in the record +// instead of a plain key: a code chunk. func pbinTestChunkLeafCell(pattern byte, bitLen int16) pbinCell { c := pbinTestBranchCell(pattern, bitLen) c.kind = pbinNodeLeaf @@ -70,9 +67,8 @@ func pbinTestChunkLeafCell(pattern byte, bitLen int16) pbinCell { return c } -// A prefix of any admissible bit length must survive a record round-trip: the -// 66-byte storage path does not fit the shared codec's fields, and a silent -// truncation would commit a wrong root (guards H4). +// The 66-byte storage path does not fit the shared codec's fields, so every +// admissible bit length is checked: a silent truncation commits a wrong root. func TestPBinBranchCodecRoundTripPrefixBitLengths(t *testing.T) { t.Parallel() @@ -167,7 +163,7 @@ func TestPBinBranchCodecIsCanonical(t *testing.T) { } // pbinTestRecord assembles a record by hand so decode can be probed with bytes -// the encoder would never produce. +// the encoder would never emit. func pbinTestRecord(touchMap, afterMap uint16, bodies ...[]byte) []byte { rec := make([]byte, 4) binary.BigEndian.PutUint16(rec, touchMap) @@ -178,8 +174,8 @@ func pbinTestRecord(touchMap, afterMap uint16, bodies ...[]byte) []byte { return rec } -// pbinTestCellBody spells one cell body: fields, uvarint bit count, then the -// caller's raw prefix bytes — deliberately not derived from the bit count. +// pbinTestCellBody takes the prefix bytes raw, deliberately not derived from the +// bit count, so a test can make the two disagree. func pbinTestCellBody(fields pbinCellFields, prefixBitLen uint64, prefix []byte, tail ...byte) []byte { body := []byte{byte(fields)} body = binary.AppendUvarint(body, prefixBitLen) @@ -191,9 +187,9 @@ func pbinTestLenAndVal(val []byte) []byte { return append(binary.AppendUvarint(nil, uint64(len(val))), val...) } -// A declared bit count that disagrees with the bytes behind it must be -// rejected rather than read as a shorter or longer prefix: the prefix is inside -// the branch hash, so spurious pad bits silently change the root (guards H3). +// A declared bit count that disagrees with the bytes behind it must be rejected, +// not read as a shorter or longer prefix: the prefix is inside the branch hash, +// so spurious pad bits silently change the root. func TestPBinBranchDecodeRejects(t *testing.T) { t.Parallel() diff --git a/execution/commitment/pbin_code.go b/execution/commitment/pbin_code.go index 9438945393e..230e9db5d5b 100644 --- a/execution/commitment/pbin_code.go +++ b/execution/commitment/pbin_code.go @@ -24,8 +24,8 @@ const ( // value carries the PUSHDATA count instead. pbinChunkDataLen = pbinValueLength - 1 - // pbinHeaderCodeChunks are the chunks the account header holds, at - // sub-indices CODE_OFFSET..255. Higher chunks live in the code zone. + // pbinHeaderCodeChunks are the chunks the account header holds, at sub-indices + // CODE_OFFSET..255. Higher chunks live in the code zone. pbinHeaderCodeChunks = pbinStemSubtreeWidth - pbinCodeOffset pbinPushOffset = 95 @@ -33,11 +33,10 @@ const ( pbinPush32 = pbinPushOffset + 32 ) -// pbinChunkifyCode splits code into the tree's chunk values (eip:374-397). Byte -// 0 of a chunk counts how many of its leading bytes are PUSHDATA, so the scan -// runs over the whole code and residual PUSHDATA carries across chunk -// boundaries. Padding to a multiple of 31 happens before the scan, which is what -// makes a PUSH whose data runs off the end count against the padded tail. +// pbinChunkifyCode splits code into the tree's chunk values (eip:374-397). The +// PUSHDATA scan runs over the whole code, so residual PUSHDATA carries across +// chunk boundaries. Padding to a multiple of 31 happens before the scan, which +// is what makes a PUSH whose data runs off the end count against the padded tail. func pbinChunkifyCode(code []byte) [][pbinValueLength]byte { if len(code) == 0 { return nil @@ -48,8 +47,8 @@ func pbinChunkifyCode(code []byte) [][pbinValueLength]byte { copy(padded, code) } - // pushdataAt[i] is how many bytes from i on are still PUSHDATA. The spec sizes - // it a whole chunk past the code so a PUSH32 on the last byte has room. + // pushdataAt[i] is how many bytes from i on are still PUSHDATA. It runs a whole + // chunk past the code so a PUSH32 on the last byte has room. pushdataAt := make([]byte, len(padded)+pbinValueLength) for pos := 0; pos < len(padded); { var pushdata int @@ -74,10 +73,9 @@ func pbinChunkifyCode(code []byte) [][pbinValueLength]byte { } // pbinRecordLeafValue is the value a leaf carries itself rather than deriving -// from state — a code chunk, or a sub-index the embedding reserves and defines -// no packing for. Unlike a storage value it is not left-padded into place: a -// chunk is positional, byte 0 being the PUSHDATA count, so a short value is an -// error. +// from state — a code chunk, or a sub-index the embedding reserves. Unlike a +// storage value it is not left-padded into place: a chunk is positional, so a +// short value is an error. func pbinRecordLeafValue(u *Update) ([pbinValueLength]byte, error) { if u.StorageLen != pbinValueLength { return [pbinValueLength]byte{}, fmt.Errorf("%w: record-resident leaf holds %d value bytes, want %d", diff --git a/execution/commitment/pbin_code_test.go b/execution/commitment/pbin_code_test.go index 7247f663ea2..9f08616a0c9 100644 --- a/execution/commitment/pbin_code_test.go +++ b/execution/commitment/pbin_code_test.go @@ -27,9 +27,8 @@ import ( "github.com/erigontech/erigon/common/empty" ) -// TestPBinChunkifyCodeVectors is the external check on chunk_code (eip:374-397): -// the reference's own chunkings, hash-independent because chunking is pure byte -// layout. +// TestPBinChunkifyCodeVectors checks chunking against the reference's own +// chunkings of chunk_code (eip:374-397). func TestPBinChunkifyCodeVectors(t *testing.T) { t.Parallel() v := pbinLoadSpecVectors(t) @@ -47,15 +46,13 @@ func TestPBinChunkifyCodeVectors(t *testing.T) { } } -// TestPBinChunkifyCodePushdataStraddlesBoundary pins the part of the scan a -// per-chunk implementation gets wrong: PUSHDATA that begins in one chunk and -// runs into the next, so the later chunk's byte 0 counts bytes pushed by an -// opcode it does not contain. +// TestPBinChunkifyCodePushdataStraddlesBoundary covers PUSHDATA that begins in +// one chunk and runs into the next: the later chunk's byte 0 counts bytes pushed +// by an opcode it does not contain, which is what a per-chunk scan gets wrong. func TestPBinChunkifyCodePushdataStraddlesBoundary(t *testing.T) { t.Parallel() - // PUSH32 at offset 30 is the last byte of chunk 0, so all 32 of its data - // bytes land in chunk 1 and 31 of them are still PUSHDATA at chunk 2. + // PUSH32 at offset 30 is the last byte of chunk 0, so its data spans chunks 1 and 2. code := append(make([]byte, 30), pbinPush32) code = append(code, bytes.Repeat([]byte{0xEE}, 32)...) @@ -66,9 +63,8 @@ func TestPBinChunkifyCodePushdataStraddlesBoundary(t *testing.T) { require.EqualValues(t, 1, chunks[2][0], "one PUSHDATA byte carries into chunk 2") } -// TestPBinChunkifyCode7702Designator covers the shortest code the tree holds: -// an EIP-7702 designator is 23 bytes, one padded chunk whose first byte is the -// 0xEF marker rather than PUSHDATA. +// TestPBinChunkifyCode7702Designator covers the shortest code the tree holds: a +// 23-byte EIP-7702 designator is one chunk, zero-padded to the full data length. func TestPBinChunkifyCode7702Designator(t *testing.T) { t.Parallel() @@ -90,8 +86,8 @@ func TestPBinChunkifyCodeEmpty(t *testing.T) { } // TestPBinChunkifyCodeChunkCount pins the sizing the header/overflow split rests -// on: chunks are ceil(len/31), and MaxCodeSize needs more than the 128 the -// account header holds. +// on: chunks are ceil(len/31), and MaxCodeSize needs more of them than the 128 +// the account header holds. func TestPBinChunkifyCodeChunkCount(t *testing.T) { t.Parallel() @@ -108,9 +104,8 @@ func TestPBinChunkifyCodeChunkCount(t *testing.T) { } // pbinTestCode is deterministic filler of a given length. Every byte is below -// PUSH1, so no chunk carries PUSHDATA and a root mismatch cannot be blamed on -// the scan the vector tests already pin. The fill depends on the length, so two -// different lengths never share a chunk. +// PUSH1, so no chunk carries PUSHDATA, and the fill depends on the length, so +// two different lengths never share a chunk. func pbinTestCode(n int) []byte { code := make([]byte, n) for i := range code { @@ -119,9 +114,8 @@ func pbinTestCode(n int) []byte { return code } -// TestPBinEngineEmitsHeaderCodeChunks is the first half of code in the tree: a -// code-bearing account's chunks have to reach the leaf set the reference tree -// builds for it, at the header sub-indices CODE_OFFSET.. . +// TestPBinEngineEmitsHeaderCodeChunks covers the first half of code in the tree: +// chunks reaching the reference leaf set at header sub-indices CODE_OFFSET and up. func TestPBinEngineEmitsHeaderCodeChunks(t *testing.T) { t.Parallel() @@ -136,10 +130,10 @@ func TestPBinEngineEmitsHeaderCodeChunks(t *testing.T) { require.Equal(t, corpus.oracleRoot(t), root) } -// TestPBinCodeChunksFollowHeaderSlots guards H5: chunks sit at the top -// sub-indices of the stem, so emitting them at the account's own visit descends -// past a header storage slot the stream has not delivered yet, and the fold that -// comes back for it rewrites a record it had already written. +// TestPBinCodeChunksFollowHeaderSlots pins the emit order inside a stem: chunks +// sit at the top sub-indices, so emitting them at the account's own visit +// descends past header storage slots the stream has not delivered yet, and the +// fold that comes back for them rewrites a record it already wrote. func TestPBinCodeChunksFollowHeaderSlots(t *testing.T) { t.Parallel() @@ -154,9 +148,9 @@ func TestPBinCodeChunksFollowHeaderSlots(t *testing.T) { require.Equal(t, corpus.oracleRoot(t), root) } -// TestPBinVisitOrderIsMonotonic is the structural assert behind H5: the grid only -// walks forward, so a visit that revisits a key already left behind is a bug in -// the caller's ordering, not something the fold can absorb. +// TestPBinVisitOrderIsMonotonic pins the rule behind the emit order: the grid +// only walks forward, so revisiting a key already left behind is a bug in the +// caller's ordering, not something the fold can absorb. func TestPBinVisitOrderIsMonotonic(t *testing.T) { t.Parallel() @@ -170,14 +164,13 @@ func TestPBinVisitOrderIsMonotonic(t *testing.T) { } // TestPBinCodeChunksSurviveAsRecordSiblings pins that a chunk leaf carries its -// own value: an untouched chunk sibling of a touched one has to hash from the -// branch record, and no state domain holds a chunk. +// own value: no state domain holds a chunk, so an untouched chunk sibling of a +// touched one has to hash from the branch record. func TestPBinCodeChunksSurviveAsRecordSiblings(t *testing.T) { t.Parallel() addr := pbinOracleAddr(14) - // Two chunks, then a shorter code touching only chunk 0: chunk 1 stays behind - // as a direct leaf sibling, which is the one shape that must reload its value. + // 62 bytes is two chunks; the redeploy to 31 touches only chunk 0. deploy := new(pbinTestCorpus).accountWithCodeBytes(addr, 1, 10, pbinTestCode(62)) stale := pbinChunkifyCode(pbinTestCode(62))[1] redeploy := new(pbinTestCorpus).accountWithCodeBytes(addr, 2, 10, pbinTestCode(31)) @@ -192,11 +185,10 @@ func TestPBinCodeChunksSurviveAsRecordSiblings(t *testing.T) { require.Equal(t, wantRoot[:], root, "the untouched chunk keeps the value the record holds") } -// TestPBinShorteningRedeployKeepsStaleChunks records the answer to Q2 as a test -// (guards H8). EIP-8297 has no removal, so a redeploy to shorter code leaves the -// chunks above the new length in place: a forward run commits them, a recompute -// from the state domains cannot know they exist. The two roots are each -// internally consistent and different, which is what makes recompute-from-domains +// TestPBinShorteningRedeployKeepsStaleChunks pins the residue a shorter redeploy +// leaves: EIP-8297 has no removal, so a forward run commits the chunks above the +// new length while a recompute from the state domains cannot know they exist. +// Both roots are internally consistent, which is what makes recompute-from-domains // invalid as an oracle for a code-bearing account. func TestPBinShorteningRedeployKeepsStaleChunks(t *testing.T) { t.Parallel() @@ -219,7 +211,6 @@ func TestPBinShorteningRedeployKeepsStaleChunks(t *testing.T) { require.NotEqual(t, rebuilt, forward, "a rebuild from state cannot reproduce the stale chunks the forward run kept") - // The residue is exactly the chunks the old code had and the new one does not. want := redeploy.entries(t) oldChunks := pbinChunkifyCode(long) for i := len(pbinChunkifyCode(short)); i < len(oldChunks); i++ { @@ -231,10 +222,9 @@ func TestPBinShorteningRedeployKeepsStaleChunks(t *testing.T) { } } -// TestPBinClearedCodeKeepsChunks covers the other half of H8: clearing an -// account's code — an EIP-7702 delegation reset is the common case — is a -// shortening redeploy down to zero chunks. The header leaves follow the account, -// the chunks stay behind, and nothing in the state records that they exist. +// TestPBinClearedCodeKeepsChunks takes the same residue down to zero chunks: +// clearing an account's code, as an EIP-7702 delegation reset does, moves the +// header leaves and leaves every chunk behind. func TestPBinClearedCodeKeepsChunks(t *testing.T) { t.Parallel() @@ -257,9 +247,8 @@ func TestPBinClearedCodeKeepsChunks(t *testing.T) { } // TestPBinGrowingRedeployReplacesChunks is the case a rebuild does reproduce: -// code that only grows overwrites every chunk it had and adds the rest, so the -// forward tree and a rebuild from state agree. Both a redeploy inside the -// account header and one spilling into the code zone. +// growing code overwrites every chunk it had and adds the rest, leaving no +// residue for the forward tree and a rebuild from state to disagree over. func TestPBinGrowingRedeployReplacesChunks(t *testing.T) { t.Parallel() @@ -284,9 +273,6 @@ func TestPBinGrowingRedeployReplacesChunks(t *testing.T) { } } -// TestPBinCodelessContextRefusesCodeBearingAccount pins that the code read is -// not optional: a context that cannot serve code cannot commit an account whose -// chunks the tree needs. func TestPBinCodelessContextRefusesCodeBearingAccount(t *testing.T) { t.Parallel() @@ -302,12 +288,13 @@ func TestPBinCodelessContextRefusesCodeBearingAccount(t *testing.T) { require.ErrorIs(t, err, ErrPBinUnsupported) } -// pbinCodelessContext is a PatriciaContext with no code read, which embedding the -// interface rather than the concrete state is what produces. +// pbinCodelessContext hides the concrete state's code read: code is served +// through an optional interface, so embedding PatriciaContext rather than the +// state makes that assertion fail. type pbinCodelessContext struct{ PatriciaContext } // TestPBinCodeSizeMustMatchTheCodeBehindIt pins that the two reads agree: the -// BASIC_DATA size and the chunks come from separate reads, and a size that +// BASIC_DATA size and the chunks come from separate reads, so a size that // disagrees with the code would commit a leaf set no reference tree holds. func TestPBinCodeSizeMustMatchTheCodeBehindIt(t *testing.T) { t.Parallel() @@ -324,9 +311,9 @@ func TestPBinCodeSizeMustMatchTheCodeBehindIt(t *testing.T) { require.ErrorContains(t, err, "the code domain holds") } -// TestPBinZoneKeyLengthIsExplicit pins that the code zone is recognised rather -// than passing as an account key because both are 34 bytes, and that the zones -// the embedding has not allocated are refused. +// TestPBinZoneKeyLengthIsExplicit pins that the zone byte decides the key +// length: an account key and a code key are both 34 bytes, so a code key would +// otherwise pass as an account one. Unallocated zones are refused. func TestPBinZoneKeyLengthIsExplicit(t *testing.T) { t.Parallel() @@ -349,9 +336,9 @@ func TestPBinZoneKeyLengthIsExplicit(t *testing.T) { require.Len(t, pbinTreeKey(pbinCodeZone, make([]byte, 32), 0), pbinCodeKeyLength) } -// TestPBinLeafValueRoutesByZone pins the second place a code key used to pass by -// accident: the leaf value is picked by the key's zone, so a code-zone key must -// not be read as an account header sub-index. +// TestPBinLeafValueRoutesByZone covers the same rule at the value encoder: the +// leaf value is picked by the key's zone, so a code-zone key must not be read as +// an account header sub-index. func TestPBinLeafValueRoutesByZone(t *testing.T) { t.Parallel() @@ -364,8 +351,8 @@ func TestPBinLeafValueRoutesByZone(t *testing.T) { require.NoError(t, err) require.Equal(t, chunk[:], got[:]) - // The same is true inside the account zone: sub-indices at CODE_OFFSET and - // above are chunks, not storage. + // Inside the account zone, sub-indices at CODE_OFFSET and above are chunks, + // not storage. addr := pbinOracleAddr(19) got, err = pbinLeafValue(pbinTreeKeyCodeChunk(addr, 0), &u) require.NoError(t, err) @@ -378,9 +365,8 @@ func TestPBinLeafValueRoutesByZone(t *testing.T) { require.ErrorIs(t, err, errPBinCellHash) } -// TestPBinLeafCellHashChecksZoneLength pins the third site: a leaf's key length -// has to match its own zone, so a 34-byte storage key or a 66-byte code key is -// rejected instead of hashing. +// TestPBinLeafCellHashChecksZoneLength covers the same rule at the leaf hash: a +// 34-byte storage key or a 66-byte code key is rejected instead of hashed. func TestPBinLeafCellHashChecksZoneLength(t *testing.T) { t.Parallel() diff --git a/execution/commitment/pbin_codesize_test.go b/execution/commitment/pbin_codesize_test.go index 85376af550c..1015498a8d3 100644 --- a/execution/commitment/pbin_codesize_test.go +++ b/execution/commitment/pbin_codesize_test.go @@ -27,9 +27,8 @@ import ( "github.com/erigontech/erigon/common" ) -// TestPBinBasicDataLeafCarriesCodeSize checks the BASIC_DATA leaf the engine -// builds for a code-bearing account against the reference's own packings: the -// code size has to reach the leaf value, not be forced to zero. +// TestPBinBasicDataLeafCarriesCodeSize checks the BASIC_DATA packing against the +// reference's own vectors: the code size has to reach the leaf value. func TestPBinBasicDataLeafCarriesCodeSize(t *testing.T) { t.Parallel() v := pbinLoadSpecVectors(t) @@ -52,7 +51,7 @@ func TestPBinBasicDataLeafCarriesCodeSize(t *testing.T) { // TestPBinEngineRootCarriesCodeSize drives a code-bearing account through the // whole engine, so the size has to survive the context read, the cell merge and -// the leaf hash — not just the value encoder. +// the leaf hash, not just the value encoder. func TestPBinEngineRootCarriesCodeSize(t *testing.T) { t.Parallel() @@ -63,8 +62,8 @@ func TestPBinEngineRootCarriesCodeSize(t *testing.T) { _, root := withCode.process(t) require.Equal(t, withCode.oracleRoot(t), root) - // The same leaf set with BASIC_DATA packed at code_size 0 isolates the size: - // every other leaf, the chunks included, stays where it was. + // Repacking BASIC_DATA at code_size 0 isolates the size: every other leaf, + // the chunks included, stays where it was. sizeless, err := pbinEncodeBasicData(4, uint256.NewInt(500), 0) require.NoError(t, err) entries := withCode.entries(t) @@ -81,9 +80,8 @@ func TestPBinEngineRootCarriesCodeSize(t *testing.T) { require.NotEqual(t, want[:], root, "code_size must reach the root") } -// TestPBinUpdateCodeSizeSurvivesCopyAndReset pins the two Update lifecycle -// hooks the engine relies on: a copied update keeps the size, a reset one drops -// it so a pooled cell cannot inherit a stale code size. +// TestPBinUpdateCodeSizeSurvivesCopyAndReset pins the Update lifecycle: Copy +// keeps the size, Reset drops it so a pooled cell cannot inherit a stale one. func TestPBinUpdateCodeSizeSurvivesCopyAndReset(t *testing.T) { t.Parallel() @@ -94,9 +92,9 @@ func TestPBinUpdateCodeSizeSurvivesCopyAndReset(t *testing.T) { require.Zero(t, u.CodeSize) } -// TestPBinUpdateCodeSizeMergesWithCodeHash pins that the size travels with the -// hash: they describe the same code, so a merge must never leave one of them -// from the old account and the other from the new. +// TestPBinUpdateCodeSizeMergesWithCodeHash pins that size and hash travel +// together: they describe the same code, so a merge must never take one from the +// old account and the other from the new. func TestPBinUpdateCodeSizeMergesWithCodeHash(t *testing.T) { t.Parallel() @@ -107,9 +105,9 @@ func TestPBinUpdateCodeSizeMergesWithCodeHash(t *testing.T) { } // TestPBinPushSideNeverDeliversCode pins that Updates.TouchCode cannot feed the -// bin trie: the variant is hardwired to ModeDirect, which interns plain keys -// only and hands the trie a nil update. Everything the tree hashes comes from -// the read side, so patching the push side to carry code would be dead code. +// bin trie: the variant is hardwired to ModeDirect, which interns plain keys only +// and hands the trie a nil update. Everything the tree hashes comes from the read +// side, so teaching the push side to carry code would add dead code. func TestPBinPushSideNeverDeliversCode(t *testing.T) { t.Parallel() diff --git a/execution/commitment/pbin_domainwrite_test.go b/execution/commitment/pbin_domainwrite_test.go index 15f21638875..5c4cc54a6f1 100644 --- a/execution/commitment/pbin_domainwrite_test.go +++ b/execution/commitment/pbin_domainwrite_test.go @@ -29,8 +29,7 @@ import ( // pbinStrictWriteContext mirrors the domain's write contract: SharedDomains // refuses a nil value outright, so a PutBranch handing one over fails here the -// way it would over a real datadir. Accepted writes are recorded in order with -// the prevData the engine claimed. +// way it would over a real datadir. type pbinStrictWriteContext struct { *MockState puts []pbinRecordedPut @@ -55,8 +54,8 @@ func pbinTestStrictEngine(t *testing.T) (*PBinPatriciaHashed, *pbinStrictWriteCo return NewPBinPatriciaHashed(ctx), ctx, ms } -// TestPBinStoreRootEmptiedTreeWritesNonNil pins the empty-root storeRoot path: -// an emptied tree deletes its record by writing a zero-length value, never nil. +// An emptied tree deletes its root record by writing a zero-length value, never +// nil. func TestPBinStoreRootEmptiedTreeWritesNonNil(t *testing.T) { t.Parallel() @@ -73,10 +72,8 @@ func TestPBinStoreRootEmptiedTreeWritesNonNil(t *testing.T) { require.NotNil(t, put.prev) } -// TestPBinFoldDeleteWritesNonNilWithRealPrev drives a stored record through the -// touched-but-gone unfold into foldDelete: the deletion write must carry a -// zero-length value, and prevData must be the record bytes the row unfolded -// from — likewise for the root record storeRoot then empties. +// A deletion write carries a zero-length value and, as prevData, the record +// bytes the row unfolded from — likewise for the root record storeRoot empties. func TestPBinFoldDeleteWritesNonNilWithRealPrev(t *testing.T) { t.Parallel() @@ -109,9 +106,8 @@ func TestPBinFoldDeleteWritesNonNilWithRealPrev(t *testing.T) { require.Equal(t, storedRoot, root.prev) } -// TestPBinZeroLengthBranchRoundTripsAsDeletion checks the deletion writes all -// the way back around: after the engine empties a stored tree, the zero-length -// records still sitting in the store must read back as no tree at all. +// After the engine empties a stored tree, the zero-length records still sitting +// in the store must read back as no tree at all. func TestPBinZeroLengthBranchRoundTripsAsDeletion(t *testing.T) { t.Parallel() @@ -139,10 +135,9 @@ func TestPBinZeroLengthBranchRoundTripsAsDeletion(t *testing.T) { require.Equal(t, make([]byte, length.Hash), root) } -// pbinRequirePutsMatchStore walks recorded writes in order against what the -// store held before the run, requiring each prevData to be exactly the value -// the write replaces — and non-nil, so the domain never falls back to its own -// read. Returns how many writes replaced an existing record. +// pbinRequirePutsMatchStore replays the recorded writes against the store, +// requiring each prevData to be exactly the value that write replaces — and +// non-nil, so the domain never falls back to its own read. func pbinRequirePutsMatchStore(t *testing.T, puts []pbinRecordedPut, store map[string][]byte) (overwrites int) { t.Helper() for _, put := range puts { @@ -157,9 +152,8 @@ func pbinRequirePutsMatchStore(t *testing.T, puts []pbinRecordedPut, store map[s return overwrites } -// TestPBinProcessPutBranchCarriesRealPrev runs a second batch over a stored -// tree and requires every branch write to carry the previous record it -// replaces: empty on a fresh store, the stored bytes on a rewrite. +// Every branch write carries the record it replaces: empty on a fresh store, the +// stored bytes on a rewrite. func TestPBinProcessPutBranchCarriesRealPrev(t *testing.T) { t.Parallel() diff --git a/execution/commitment/pbin_fold_test.go b/execution/commitment/pbin_fold_test.go index 9d3e7b10a05..bf7278f31b3 100644 --- a/execution/commitment/pbin_fold_test.go +++ b/execution/commitment/pbin_fold_test.go @@ -28,8 +28,6 @@ import ( "github.com/erigontech/erigon/db/kv" ) -// pbinTestCountingCtx counts branch reads, so a test can pin how many records a -// fold needed beyond the ones the descent itself read. type pbinTestCountingCtx struct { PatriciaContext branchReads int @@ -40,9 +38,9 @@ func (c *pbinTestCountingCtx) Branch(prefix []byte) ([]byte, kv.Step, error) { return c.PatriciaContext.Branch(prefix) } -// pbinTestLeaf is one storage entry in every form a fold needs it: the plain key -// its state is read by, the tree key its path is cut from, and the encoded value -// both the engine and the oracle hash. +// pbinTestLeaf is one storage entry in the three forms a fold needs: the plain +// key state is read by, the tree key the path is cut from, and the encoded value +// the engine and the oracle hash. type pbinTestLeaf struct { plainKey []byte treeKey []byte @@ -95,8 +93,7 @@ func pbinTestPutState(t *testing.T, ms *MockState, leaves ...pbinTestLeaf) { require.NoError(t, ms.applyPlainUpdates(keys, updates)) } -// pbinTestTreeKeyFlipped derives a key diverging from the original at exactly -// one named bit. The zone byte is off limits: it selects the value encoding. +// The zone byte is off limits: it selects the value encoding. func pbinTestTreeKeyFlipped(t *testing.T, key []byte, d int16) []byte { t.Helper() require.GreaterOrEqual(t, d, int16(8), "bit %d is inside the zone byte", d) @@ -110,8 +107,6 @@ func pbinTestBaseStorageKey() []byte { return pbinTreeKeyStorage(pbinOracleAddr(7), pbinOracleSlot(1000)) } -// pbinTestKeyPrefix is the first bitLen bits of a tree key, the shape both a -// descent key and a node prefix take. func pbinTestKeyPrefix(key []byte, bitLen int16) pbinBitpath { full := pbinPathFromBytes(key) return full.slice(0, bitLen) @@ -125,8 +120,7 @@ func pbinTestSeedRow(pph *PBinPatriciaHashed, currentKey pbinBitpath, depth int1 pph.grid.activeRows = 1 } -// pbinTestFillCell drops a cell into a live row the way updateCell will, marking -// it both touched and present. +// pbinTestFillCell fills a row cell the way updateCell does: touched and present. func pbinTestFillCell(pph *PBinPatriciaHashed, row int, bit uint64, c pbinCell) { pph.grid.rows[row][bit] = c pph.grid.touchMap[row] |= uint16(1) << bit @@ -142,10 +136,9 @@ func pbinTestBranchOrder(t *testing.T, a, b pbinTestLeaf, divergence int16) (lef return a, b } -// TestPBinFoldBranchMatchesOracle folds a hand-built row and checks the node it -// emits against the reference tree, at divergence points spanning both word -// boundaries of the path. The record it writes must also survive a decode and -// re-encode unchanged, since nothing merges it with a predecessor. +// Divergence points span both word boundaries of the path. Nothing merges the +// record a branch fold writes with a predecessor, so it must also survive a +// decode and re-encode unchanged. func TestPBinFoldBranchMatchesOracle(t *testing.T) { t.Parallel() @@ -198,9 +191,8 @@ func TestPBinFoldBranchMatchesOracle(t *testing.T) { } } -// A binary node has two children. Folding a row as a branch with any other count -// is a lost or duplicated sibling, which at arity 2 is half the subtree -// (guards H12). +// Folding a row as a branch with anything but two children is a lost or +// duplicated sibling, which at arity 2 is half the subtree. func TestPBinFoldBranchRejectsWrongArity(t *testing.T) { t.Parallel() @@ -236,10 +228,9 @@ func TestPBinFoldRejectsInconsistentGrid(t *testing.T) { }) } -// TestPBinFoldPropagateRestoresDescendedNode is the round trip a shared prefix -// forces: unfold consumes the prefix into the descent key, so the branch fold -// below sees none of it, and the propagate that follows has to hand the node -// back its full prefix — which is inside its hash. +// Unfold consumes a shared prefix into the descent key, so the branch fold below +// sees none of it. The propagate that follows has to hand the node back its full +// prefix, which is inside its hash. func TestPBinFoldPropagateRestoresDescendedNode(t *testing.T) { t.Parallel() @@ -256,8 +247,8 @@ func TestPBinFoldPropagateRestoresDescendedNode(t *testing.T) { ms := NewMockState(t) pbinTestPutState(t, ms, a, b) - // Store the node the descent will walk into, then meet it again through a - // cell that only knows its prefix and hash, as a reloaded one would. + // Build the node once, then meet it again through a cell that knows only + // its prefix and hash, the way a reload would. builder := NewPBinPatriciaHashed(ms) cells := [2]pbinCell{left.cell(t, divergence+1), right.cell(t, divergence+1)} pbinTestSeedRow(builder, prefix, divergence+1, cells, 0b11, 0b11) @@ -301,9 +292,8 @@ func TestPBinFoldPropagateRestoresDescendedNode(t *testing.T) { } } -// TestPBinFoldSplitLeafSurvivorReadsNoBranch pins the short circuit: a leaf -// commits its complete key, so shortening the prefix it sits behind cannot -// invalidate anything and no record has to be read to rebuild it. +// A leaf commits its complete key, so shortening the prefix it sits behind +// invalidates nothing and no record has to be read to rebuild it. func TestPBinFoldSplitLeafSurvivorReadsNoBranch(t *testing.T) { t.Parallel() @@ -339,10 +329,9 @@ func TestPBinFoldSplitLeafSurvivorReadsNoBranch(t *testing.T) { } } -// TestPBinFoldSplitInsidePrefixMatchesOracle guards H1: the survivor of a split -// keeps prefix[matched+1:], and the prefix is inside its hash, so the cached one -// is stale. The engine has to rebuild it from the survivor's own children before -// the fold above can use it. +// The survivor of a split keeps prefix[matched+1:], and the prefix is inside its +// hash, so the cached hash is stale. The engine has to rebuild it from the +// survivor's own children before the fold above can use it. func TestPBinFoldSplitInsidePrefixMatchesOracle(t *testing.T) { t.Parallel() @@ -429,9 +418,8 @@ func TestPBinFoldSplitInsidePrefixMissingRecord(t *testing.T) { require.ErrorIs(t, pph.fold(), errPBinMissingBranch) } -// TestPBinFoldLoadsSiblingState covers the untouched half of a branch: a record -// carries plain keys, not values, so a sibling that nothing in this run touched -// has to be read back from state before it can be hashed. +// A record carries plain keys, not values, so a sibling that nothing in this run +// touched has to be read back from state before it can be hashed. func TestPBinFoldLoadsSiblingState(t *testing.T) { t.Parallel() @@ -459,8 +447,8 @@ func TestPBinFoldLoadsSiblingState(t *testing.T) { require.Equal(t, common.Hash(want), pph.grid.root.hash) } -// TestPBinFoldDeleteDropsRecord pins the third dispatch arm: a row that keeps -// nothing takes its stored record with it and reports the absence upwards. +// A row that keeps nothing takes its stored record with it and reports the +// absence upwards. func TestPBinFoldDeleteDropsRecord(t *testing.T) { t.Parallel() diff --git a/execution/commitment/pbin_fuzz_test.go b/execution/commitment/pbin_fuzz_test.go index f66d2a9c61a..8373f15c26b 100644 --- a/execution/commitment/pbin_fuzz_test.go +++ b/execution/commitment/pbin_fuzz_test.go @@ -25,22 +25,22 @@ import ( "github.com/erigontech/erigon/common/length" ) -// pbinFuzzSlots is the slot pool the generator draws from. Entropy is the enemy -// here: 32-byte slots picked at random essentially never share a stem, so a -// fuzzer free to choose them would only ever build shallow trees and would never -// reach sub-index sharing, group boundaries or the account/storage zone split. +// pbinFuzzSlots is the slot pool the generator draws from. Slots picked at +// random essentially never share a stem, so a fuzzer free to choose all 32 bytes +// would only build shallow trees, never reaching sub-index sharing, group +// boundaries or the account/storage zone split. var pbinFuzzSlots = []uint64{0, 1, 2, 63, 64, 65, 66, 127, 128, 255, 256, 257, 258, 511, 512, 1000, 1 << 20, 1<<20 + 1} // pbinFuzzAccountBit is the selector bit choosing an account write over a slot. const pbinFuzzAccountBit = 0x04 -// pbinFuzzCodeSizes is the code pool. The last entry is the only one that spills -// past the account header into the code zone. +// pbinFuzzCodeSizes: the last entry is the only size that spills past the +// account header into the code zone. var pbinFuzzCodeSizes = []int{0, 23, 31, 62, pbinHeaderCodeChunks*pbinChunkDataLen + 62} -// pbinFuzzCode is the code an address carries for a whole run. Keying it on the -// address is what keeps the oracle valid: a redeploy to shorter code leaves its -// high chunks in the tree (H8), and the oracle only knows the final state. +// pbinFuzzCode keys the code on the address so it stays fixed for a whole run, +// which is what keeps the oracle valid: a redeploy to shorter code leaves its +// high chunks in the tree, and the oracle only knows the final state. func pbinFuzzCode(addrSeed, salt byte) []byte { n := pbinFuzzCodeSizes[int(addrSeed+salt)%len(pbinFuzzCodeSizes)] if n == 0 { @@ -49,9 +49,8 @@ func pbinFuzzCode(addrSeed, salt byte) []byte { return pbinTestCode(n) } -// pbinFuzzCorpus reads the input three bytes at a time — what to write, where, -// and with what value — drawing addresses, slots and code lengths from small -// pools so keys collide by construction. +// pbinFuzzCorpus reads the input three bytes at a time: what to write, where, +// and with what value. func pbinFuzzCorpus(data []byte, codeSalt byte) *pbinTestCorpus { c := new(pbinTestCorpus) for i := 0; i+2 < len(data); i += 3 { @@ -91,15 +90,14 @@ func pbinFuzzBatches(data []byte, cut, codeSalt byte) []*pbinTestCorpus { return batches } -// FuzzPBinProcessMatchesOracle is the differential gate: whatever the generator -// produces, the engine's root must equal the reference tree's over the same -// leaves, and the records it left behind must rebuild that root on their own. +// FuzzPBinProcessMatchesOracle: whatever the generator produces, the engine's +// root must equal the reference tree's over the same leaves, and the records it +// left behind must rebuild that root on their own. // // go test ./execution/commitment/ -run=Fuzz -fuzz=FuzzPBinProcessMatchesOracle -fuzztime=60s func FuzzPBinProcessMatchesOracle(f *testing.F) { - // Seeds spell the generator's (selector, slot, value) triples: bit 2 of the - // selector asks for an account, its low bits pick the address, and the slot - // byte indexes the pool. + // Seeds are (selector, slot, value) triples: bit 2 of the selector asks for an + // account, its low bits pick the address, and the slot byte indexes the pool. f.Add([]byte{0x04, 0, 1, 0x05, 0, 2}, byte(0), byte(0)) // two accounts, no code f.Add([]byte{0x00, 10, 1, 0x00, 11, 2, 0x00, 12, 3}, byte(2), byte(0)) // three slots of one group f.Add([]byte{0x00, 3, 1, 0x00, 4, 2, 0x04, 0, 3}, byte(1), byte(0)) // the 63/64 zone boundary plus a header diff --git a/execution/commitment/pbin_hash.go b/execution/commitment/pbin_hash.go index d5a90194810..a5b1cbda5fd 100644 --- a/execution/commitment/pbin_hash.go +++ b/execution/commitment/pbin_hash.go @@ -33,23 +33,20 @@ const ( pbinLeafTag = 0x00 pbinBranchTag = 0x01 - // pbinHashBufLen holds the longest preimage either node shape produces: the - // branch tag, the two-byte prefix bit count, the longest encodable prefix and - // both child hashes. + // pbinHashBufLen is the longest preimage either shape produces: tag, bit count, + // packed prefix, both child hashes. pbinHashBufLen = 1 + 2 + (pbinMaxPathBits+7)/8 + 2*length.Hash ) // pbinEmptyTreeHash is the hash of an absent subtree: 32 zero bytes (eip:208). -// It is not empty.RootHash — that constant is the RLP empty-string MPT root and -// substituting it here would silently produce a different tree. +// Not empty.RootHash — the RLP empty-string MPT root would build a different tree. var pbinEmptyTreeHash common.Hash var errPBinCellHash = errors.New("pbin: cell cannot be hashed") -// pbinHashFn is H. EIP-8297 leaves the hash open and names Keccak-256 among the -// candidates (eip:511-513); the execution-specs reference hashes with BLAKE3. -// Key derivation hashes too, so a suite is only fully swapped when -// pbinDigestCache is swapped with it. +// pbinHashFn is H, which EIP-8297 leaves open (eip:511-513). Tree-key derivation +// hashes with H too, so a suite is only fully swapped when pbinDigestCache is +// swapped with it. type pbinHashFn func([]byte) common.Hash // Names for H, as the --experimental.bin-commitment.hash flag spells them. @@ -59,14 +56,11 @@ const ( ) // pbinSelectedSum is H for every binary-trie engine this process builds; nil is -// Keccak-256. Roots are not comparable across a change, so the datadir persists -// the choice and refuses to reopen under a different one. +// Keccak-256. var pbinSelectedSum pbinHashFn -// SetPBinHashSuite selects H by name. Keccak-256 is the default because the EIP -// names it first, but the clients sharing a binary-trie testnet follow the -// execution-specs reference, which hashes BLAKE3 — interoperating means asking -// for it. Call before the first engine is built. +// SetPBinHashSuite selects H by name. Call it before the first engine is built: +// roots already computed under the previous suite do not match. func SetPBinHashSuite(name string) error { switch name { case "", PBinHashKeccak: @@ -79,8 +73,6 @@ func SetPBinHashSuite(name string) error { return nil } -// PBinHashSuiteName reports the selected suite, for logging and for the value -// the datadir persists. func PBinHashSuiteName() string { if pbinSelectedSum == nil { return PBinHashKeccak @@ -88,9 +80,8 @@ func PBinHashSuiteName() string { return PBinHashBlake3 } -// pbinHasher applies H to node preimages. Every preimage fits its single scratch -// buffer, so each node costs one hash call and no allocation. Its zero value is -// ready and hashes with Keccak-256. +// pbinHasher applies H to node preimages. Its zero value is ready and hashes with +// Keccak-256. type pbinHasher struct { buf [pbinHashBufLen]byte sum pbinHashFn @@ -103,15 +94,14 @@ func (h *pbinHasher) hash(preimage []byte) common.Hash { return keccak.Sum256(preimage) } -// pbinAppendBitPrefix is the spec's encode_bit_prefix (eip:196-201): a two-byte -// big-endian bit count, then the bits MSB-first zero-padded to a byte boundary. -// The count is what keeps a 7-bit prefix distinct from an 8-bit one that agrees +// pbinAppendBitPrefix is the spec's encode_bit_prefix (eip:196-201). The leading +// bit count is what keeps a 7-bit prefix distinct from an 8-bit one that agrees // with it on the pad bit. func pbinAppendBitPrefix(dst []byte, p *pbinBitpath) []byte { return p.appendPackedBits(binary.BigEndian.AppendUint16(dst, uint16(p.bitLen))) } -// branchHash is H(0x01 || encode_bit_prefix(prefix) || left || right). An absent +// branchHash is H(0x01 || encode_bit_prefix(prefix) || left || right); an absent // child passes pbinEmptyTreeHash rather than being omitted. func (h *pbinHasher) branchHash(prefix *pbinBitpath, left, right *common.Hash) common.Hash { buf := pbinAppendBitPrefix(append(h.buf[:0], pbinBranchTag), prefix) @@ -120,11 +110,8 @@ func (h *pbinHasher) branchHash(prefix *pbinBitpath, left, right *common.Hash) c return h.hash(buf) } -// cellHash is the only way a cell becomes a hash. Keeping it single is what -// stops a second hasher drifting from this one. -// -// path is the descent to the cell; a leaf's complete key is path followed by the -// cell's own prefix, which is also what tells the leaf value apart. +// cellHash hashes the cell reached by path; a leaf's complete key is path +// followed by the cell's own prefix. func (h *pbinHasher) cellHash(c *pbinCell, path *pbinBitpath) (common.Hash, error) { switch c.kind { case pbinNodeEmpty: @@ -156,8 +143,8 @@ func (h *pbinHasher) leafCellHash(c *pbinCell, path *pbinBitpath) (common.Hash, buf := full.appendPackedBits(append(h.buf[:0], pbinLeafTag)) key := buf[1:] - // The length is fixed per zone, which is what keeps keys prefix-free: a key of - // another zone's length is not a key at all (eip:284-288). + // Key length is fixed per zone, which is what keeps the key space prefix-free + // (eip:284-288). if want, known := pbinZoneKeyLength(key[0]); !known || len(key) != want { return common.Hash{}, fmt.Errorf("%w: leaf key %x is no key of zone %#x", errPBinCellHash, key, key[0]) } @@ -168,10 +155,6 @@ func (h *pbinHasher) leafCellHash(c *pbinCell, path *pbinBitpath) (common.Hash, return h.hash(append(buf, value[:]...)), nil } -// pbinLeafValue picks the encoding the key's own position names: the zone byte -// separates storage and code from the account header, and within the header the -// sub-index selects between BASIC_DATA, CODE_HASH and a header-resident slot. -// Every other position holds a value the record already carries whole. func pbinLeafValue(key []byte, u *Update) ([pbinValueLength]byte, error) { switch key[0] { case pbinStorageZone: @@ -190,9 +173,9 @@ func pbinLeafValue(key []byte, u *Update) ([pbinValueLength]byte, error) { case subIndex >= pbinHeaderStorageOffset && subIndex < pbinCodeOffset: return pbinEncodeStorageValue(u.Storage[:u.StorageLen]), nil default: - // Code chunks from CODE_OFFSET on, and the sub-indices below - // HEADER_STORAGE_OFFSET the embedding reserves (eip:255-257): neither is - // packed from state, so the value has to be a full 32 bytes already. + // Code chunks from CODE_OFFSET on, plus the sub-indices the embedding + // reserves below HEADER_STORAGE_OFFSET (eip:255-257): neither is packed from + // state, so the value must already be 32 whole bytes. return pbinRecordLeafValue(u) } } diff --git a/execution/commitment/pbin_hash_test.go b/execution/commitment/pbin_hash_test.go index 4f25786d765..ba31a3d9e4f 100644 --- a/execution/commitment/pbin_hash_test.go +++ b/execution/commitment/pbin_hash_test.go @@ -30,8 +30,8 @@ import ( ) // leafHash is H(0x00 || key || value) over the complete tree key. The engine -// builds this preimage from a cell in leafCellHash; spelling it out from a key -// and a value is what lets a test state the expected hash directly. +// builds the same preimage from a cell; taking the key and value directly is +// what lets a test state the expected hash. func (h *pbinHasher) leafHash(key, value []byte) common.Hash { if len(key) != pbinAccountKeyLength && len(key) != pbinStorageKeyLength { panic(fmt.Sprintf("pbin: leaf key of %d bytes is neither zone length", len(key))) @@ -56,8 +56,7 @@ func pbinTestPathFromBits(t *testing.T, bits []byte) pbinBitpath { return p } -// pbinTestBitSpec reads a "1011" style literal into the oracle's one-bit-per-byte -// form, so a test can name a short prefix by writing it out. +// pbinTestBitSpec reads a "1011" literal into the oracle's one-bit-per-byte form. func pbinTestBitSpec(t *testing.T, spec string) []byte { t.Helper() bits := make([]byte, 0, len(spec)) @@ -89,8 +88,8 @@ func pbinTestOracleLeaf(addr, slot uint64) *pbinOracleLeaf { } } -// TestPBinEmptyTreeHash guards H11: EIP-8297's empty subtree is 32 zero bytes -// (eip:208), not the empty-MPT root the rest of erigon reaches for. +// EIP-8297's empty subtree is 32 zero bytes (eip:208), not the empty-MPT root +// the rest of erigon reaches for. func TestPBinEmptyTreeHash(t *testing.T) { t.Parallel() @@ -106,8 +105,7 @@ func TestPBinEmptyTreeHash(t *testing.T) { require.NotEqual(t, empty.RootHash, got) } -// TestPBinAppendBitPrefixMatchesOracle checks the engine's encode_bit_prefix -// against the spec transcription at every length where padding can go wrong. +// The lengths below are the ones where bit-prefix padding can go wrong. func TestPBinAppendBitPrefixMatchesOracle(t *testing.T) { t.Parallel() @@ -174,9 +172,7 @@ func TestPBinBranchHashMatchesOracle(t *testing.T) { } } -// TestPBinNestedBranchHashMatchesOracle folds a two-level shape bottom-up the -// way the engine will, so a branch hash feeding another branch is covered and -// not just a branch over two leaves. +// Covers a branch hash feeding another branch, not just a branch over leaves. func TestPBinNestedBranchHashMatchesOracle(t *testing.T) { t.Parallel() @@ -199,8 +195,8 @@ func TestPBinNestedBranchHashMatchesOracle(t *testing.T) { require.Equal(t, common.Hash(want), h.branchHash(&outerPath, &innerHash, &cHash)) } -// TestPBinBranchHashEmptyChild pins that an absent child contributes the -// empty-subtree constant rather than being skipped. +// An absent child contributes the empty-subtree constant rather than being +// skipped. func TestPBinBranchHashEmptyChild(t *testing.T) { t.Parallel() @@ -319,9 +315,8 @@ func TestPBinCellHashRejectsMalformedLeaf(t *testing.T) { }) } -// TestPBinCellHashBuildsCorpusRoots folds each oracle corpus of two keys by hand -// through the cell hasher, checking the primitives compose into the same root -// the reference tree produces. +// Folds each two-key corpus by hand through the cell hasher, checking the +// primitives compose into the root the reference tree produces. func TestPBinCellHashBuildsCorpusRoots(t *testing.T) { t.Parallel() diff --git a/execution/commitment/pbin_hashsuite_test.go b/execution/commitment/pbin_hashsuite_test.go index e1fbd85da85..e352854144c 100644 --- a/execution/commitment/pbin_hashsuite_test.go +++ b/execution/commitment/pbin_hashsuite_test.go @@ -47,9 +47,8 @@ func TestPBinSetHashSuite(t *testing.T) { require.Equal(t, PBinHashKeccak, PBinHashSuiteName(), "a rejected name must not change the suite") } -// TestPBinInitializeTrieAppliesHashSuite pins that the selection reaches both -// seams through the production constructor: an engine whose node hashing and -// key derivation disagreed would build a tree no one can reproduce. +// The selection has to reach both seams: an engine whose node hashing and key +// derivation disagreed would build a tree no one can reproduce. func TestPBinInitializeTrieAppliesHashSuite(t *testing.T) { pbinRestoreHashSuite(t) @@ -78,10 +77,10 @@ func TestPBinInitializeTrieAppliesHashSuite(t *testing.T) { } } -// TestPBinBlake3SuiteMatchesSpecRoots is the interop check: with BLAKE3 selected -// the way a node selects it, the engine reproduces the reference implementation's -// roots. Under the Keccak default the same vectors must NOT match — otherwise the -// selection is not reaching the engine and the test proves nothing. +// With BLAKE3 selected the way a node selects it, the engine reproduces the +// reference implementation's roots. The Keccak default must NOT match the same +// vectors — otherwise the selection never reached the engine and the positive +// half proves nothing. func TestPBinBlake3SuiteMatchesSpecRoots(t *testing.T) { pbinRestoreHashSuite(t) v := pbinLoadSpecVectors(t) diff --git a/execution/commitment/pbin_hazard_test.go b/execution/commitment/pbin_hazard_test.go index adff8f48789..c4a1995d489 100644 --- a/execution/commitment/pbin_hazard_test.go +++ b/execution/commitment/pbin_hazard_test.go @@ -27,8 +27,8 @@ import ( "github.com/erigontech/erigon/common" ) -// pbinTestBatches runs each corpus through one engine and one state in order, -// the way consecutive blocks reach the trie, and returns the root after the last. +// pbinTestBatches runs the corpora through one engine and one state in order, +// the way consecutive blocks reach the trie. func pbinTestBatches(t *testing.T, batches ...*pbinTestCorpus) (*PBinPatriciaHashed, *MockState, []byte) { t.Helper() pph, ms := pbinTestEngine(t) @@ -41,8 +41,8 @@ func pbinTestBatches(t *testing.T, batches ...*pbinTestCorpus) (*PBinPatriciaHas } // pbinTestUnion is the leaf set the batches leave behind. A key touched twice -// keeps its last value, which is what the oracle's duplicate-key insert does and -// what MockState's update merge does. +// keeps its last value — the same last-write-wins as the oracle's duplicate-key +// insert and MockState's update merge. func pbinTestUnion(batches ...*pbinTestCorpus) *pbinTestCorpus { u := new(pbinTestCorpus) for _, b := range batches { @@ -58,7 +58,6 @@ func pbinTestUnion(batches ...*pbinTestCorpus) *pbinTestCorpus { return u } -// leafCount is how many leaves the corpus stands for once repeated keys collapse. func (c *pbinTestCorpus) leafCount(t *testing.T) int { t.Helper() seen := make(map[string]struct{}) @@ -77,8 +76,8 @@ func (c *pbinTestCorpus) permute(order []int) *pbinTestCorpus { return out } -// TestPBinUntouchedSiblingSurvivesBatch guards H2. At arity 2 a cell's sibling is -// the whole other half of the subtree, so a batch that rewrites a node from the +// TestPBinUntouchedSiblingSurvivesBatch: at arity 2 a cell's sibling is the +// whole other half of the subtree, so a batch that rewrites a node from the // touched child alone loses everything under the other one. func TestPBinUntouchedSiblingSurvivesBatch(t *testing.T) { t.Parallel() @@ -140,10 +139,10 @@ func TestPBinUntouchedSiblingSurvivesBatch(t *testing.T) { } } -// TestPBinSplitInsideStoredPrefix guards H1. A probe diverging inside a stored -// branch's prefix shortens that prefix, and the prefix is inside the node's hash, -// so a hash carried over from the record is stale. The counters are what pin that -// this run actually took that path rather than passing by luck. +// TestPBinSplitInsideStoredPrefix: a probe diverging inside a stored branch's +// prefix shortens that prefix, and the prefix is inside the node's hash, so a +// hash carried over from the record is stale. The counter assertions pin that +// the run took that path rather than passing by luck. func TestPBinSplitInsideStoredPrefix(t *testing.T) { t.Parallel() @@ -173,9 +172,9 @@ func TestPBinSplitInsideStoredPrefix(t *testing.T) { pbinTestVerifyRecords(t, ms, root, union.leafCount(t)) } -// TestPBinDeepSharedPrefixCorpus is H1's other half: a mined cluster whose keys -// agree far past the root, so the splits happen deep instead of at the first -// bits, spread over batches so the survivors come back from records. +// TestPBinDeepSharedPrefixCorpus uses mined keys that agree far past the root, +// so the splits happen deep instead of at the first bits, spread over batches so +// the survivors come back from records. func TestPBinDeepSharedPrefixCorpus(t *testing.T) { t.Parallel() @@ -196,8 +195,6 @@ func TestPBinDeepSharedPrefixCorpus(t *testing.T) { pbinTestVerifyRecords(t, ms, root, union.leafCount(t)) } -// pbinTestOrderings returns the corpus in every arrival order worth trying: as -// written, reversed, both tree-key directions, and one shuffle. func pbinTestOrderings(t *testing.T, c *pbinTestCorpus) map[string]*pbinTestCorpus { t.Helper() @@ -229,8 +226,8 @@ func pbinTestOrderings(t *testing.T, c *pbinTestCorpus) map[string]*pbinTestCorp } } -// pbinTestProcessSeq feeds the corpus one key per Process call, the way -// per-block processing arrives, and returns the root after the last key. +// pbinTestProcessSeq feeds the corpus one key per Process call, where +// pbinTestBatches makes a single call per corpus. func pbinTestProcessSeq(t *testing.T, c *pbinTestCorpus) (*MockState, []byte) { t.Helper() pph, ms := pbinTestEngine(t) @@ -273,9 +270,9 @@ func pbinTestUniqueReprCorpora() []struct { } } -// TestPBinUniqueRepresentation ports Test_HexPatriciaHashed_UniqueRepresentation -// and its variants: the root follows the state the keys leave behind, not the -// order they arrive in nor how many Process calls they are split across. +// TestPBinUniqueRepresentation ports Test_HexPatriciaHashed_UniqueRepresentation: +// the root follows the state the keys leave behind, not the order they arrive in +// nor how many Process calls they are split across. func TestPBinUniqueRepresentation(t *testing.T) { t.Parallel() diff --git a/execution/commitment/pbin_keys.go b/execution/commitment/pbin_keys.go index 88a71109e06..728a6ca99ac 100644 --- a/execution/commitment/pbin_keys.go +++ b/execution/commitment/pbin_keys.go @@ -44,9 +44,9 @@ const ( pbinStorageKeyLength = 66 ) -// pbinZoneKeyLength is the one key length a zone admits, which is what keeps its -// keys prefix-free (eip:284-288). An unknown zone has no length: the embedding -// allocates 0x02..0xFE to nothing yet. +// pbinZoneKeyLength gives the single key length a zone admits, which is what +// keeps that zone's keys prefix-free (eip:284-288). Zones 0x02..0xFE are +// unallocated and have no length. func pbinZoneKeyLength(zone byte) (int, bool) { switch zone { case pbinAccountZone: @@ -60,8 +60,7 @@ func pbinZoneKeyLength(zone byte) (int, bool) { } } -// pbinAddr32 widens a legacy address to the spec's Address32 by left-padding -// with zero bytes (eip:291-296). +// pbinAddr32 widens a legacy address to the spec's Address32 (eip:291-296). func pbinAddr32(addr []byte) [32]byte { if len(addr) > 32 { panic(fmt.Sprintf("pbin: address of %d bytes exceeds 32", len(addr))) @@ -71,9 +70,8 @@ func pbinAddr32(addr []byte) [32]byte { return a32 } -// pbinTreeKey assembles zone || treePosition || subIndex and asserts the length -// fixed for that zone. The assert is load-bearing: one length per zone is what -// keeps keys prefix-free within a zone (eip:283-288). +// pbinTreeKey assembles zone || treePosition || subIndex. The length assert is +// what enforces the prefix-free invariant (see pbinZoneKeyLength). func pbinTreeKey(zone byte, treePosition []byte, subIndex byte) []byte { key := make([]byte, 0, len(treePosition)+2) key = append(key, zone) @@ -96,26 +94,26 @@ func pbinTreeKeyAccount(addr []byte, subIndex byte) []byte { return c.accountKey(addr, subIndex) } -// pbinTreeKeyStorage returns the key for a storage slot, routing slots below 64 -// into the account header and the rest into the storage zone (eip:415-437). -// slot is big-endian and at most 32 bytes. +// pbinTreeKeyStorage returns the key for a storage slot: slots below 64 live in +// the account header, the rest in the storage zone (eip:415-437). slot is +// big-endian and at most 32 bytes. func pbinTreeKeyStorage(addr, slot []byte) []byte { var c pbinDigestCache return c.storageKey(addr, slot) } -// pbinTreeKeyCodeChunk returns the key for a code chunk the account header holds -// (eip:355-367). Those chunks share the account's own stem; higher ones go -// through pbinTreeKeyCodeOverflow. +// pbinTreeKeyCodeChunk returns the key for a code chunk the account header holds, +// sharing the account's own stem (eip:355-367). Higher chunks go through +// pbinTreeKeyCodeOverflow. func pbinTreeKeyCodeChunk(addr []byte, chunkID int) []byte { var c pbinDigestCache return c.codeChunkKey(addr, chunkID) } // pbinTreeKeyCodeOverflow returns the code-zone key for a chunk past the account -// header (eip:355-367). Those chunks are content-addressed by code hash, so -// accounts running the same bytecode name the same leaves and the key cannot be -// derived from an address at all. +// header (eip:355-367). These chunks are content-addressed by code hash, so +// accounts running the same bytecode share the leaves and no address can derive +// the key. func pbinTreeKeyCodeOverflow(codeHash common.Hash, chunkID int) []byte { var c pbinDigestCache return c.codeOverflowKey(codeHash, chunkID) @@ -123,17 +121,17 @@ func pbinTreeKeyCodeOverflow(codeHash common.Hash, chunkID int) []byte { // pbinKeyHasher returns a keyHasher deriving the primary leaf's tree key: // BASIC_DATA for an account, the slot's own leaf for storage. The CODE_HASH -// sibling shares the stem and is written by the engine during the same visit, -// so it needs no key of its own here. -// -// The digest cache is borrowed per call rather than captured: Updates.NewEmpty -// copies the hasher value, so a captured cache would be written by two buffers -// hashing concurrently. Every hit is validated against the address it was built -// from, so borrowing another goroutine's cache stays correct. +// sibling shares the stem and is written by the engine during the same visit, so +// it needs no key of its own here. func pbinKeyHasher() keyHasher { return pbinKeyHasherWith(nil) } // pbinKeyHasherWith derives keys under sum, nil meaning Keccak-256. Callers swap // the hash here and on node hashing together through setHashSuite. +// +// The digest cache is pooled rather than captured because Updates.NewEmpty copies +// the hasher value: a captured cache would be written by two buffers hashing +// concurrently. Every hit is validated against the address it was built from, so +// borrowing another goroutine's cache stays correct. func pbinKeyHasherWith(sum pbinHashFn) keyHasher { var pool sync.Pool return func(plainKey []byte) []byte { @@ -147,11 +145,10 @@ func pbinKeyHasherWith(sum pbinHashFn) keyHasher { } } -// pbinDigestCache memoizes the two hash-derived key components across a run of -// keys: key_hash(addr32) per address and key_hash(addr32||tree_index) per -// 256-slot storage group. Both digests are immutable, so a hit is always -// correct; changing address invalidates the group entry, which is bound to the -// address as well as the index (eip:411-414). +// pbinDigestCache memoizes the two hash-derived key components: key_hash(addr32) +// per address and key_hash(addr32||tree_index) per 256-slot storage group +// (eip:411-414). The group entry is bound to the address as well as the index, so +// a changed address cannot yield a stale hit. type pbinDigestCache struct { sum pbinHashFn @@ -212,11 +209,9 @@ func (c *pbinDigestCache) codeChunkKey(addr []byte, chunkID int) []byte { return c.accountKey(addr, byte(pbinCodeOffset+chunkID)) } -// codeOverflowKey splits the chunk's overflow index into a tree index and a -// sub-index, hashing code_hash ‖ tree_index into the code-zone stem the chunk -// sits under. The digest is not memoized: one contract spans at most a handful -// of tree indexes, and the cache's entries are bound to an address these keys do -// not have. +// codeOverflowKey derives the code-zone key of an overflow chunk. The digest is +// not memoized: one contract spans at most a handful of tree indexes, and the +// cache's entries are bound to an address these keys do not have. func (c *pbinDigestCache) codeOverflowKey(codeHash common.Hash, chunkID int) []byte { if chunkID < pbinHeaderCodeChunks { panic(fmt.Sprintf("pbin: code chunk %d is a header chunk, not a code-zone one", chunkID)) diff --git a/execution/commitment/pbin_keys_test.go b/execution/commitment/pbin_keys_test.go index e8fe2168f29..f8c85914c9a 100644 --- a/execution/commitment/pbin_keys_test.go +++ b/execution/commitment/pbin_keys_test.go @@ -28,8 +28,8 @@ import ( ) // pbinTestKeccak is an independent Keccak-256 (x/crypto, not the fastkeccak the -// engine uses) so the vectors below are pinned against the spec rather than -// against the helper under test. +// engine uses), so the vectors below are pinned against the spec rather than +// against the code under test. func pbinTestKeccak(t *testing.T, parts ...[]byte) []byte { t.Helper() h := sha3.NewLegacyKeccak256() @@ -71,8 +71,7 @@ func pbinTestConcat(parts ...[]byte) []byte { return out } -// TestPBinTreeKeyEIPVectors pins the derivation against the spec's test cases -// (eip:583-630). +// Pins the derivation against the spec's test cases (eip:583-630). func TestPBinTreeKeyEIPVectors(t *testing.T) { t.Parallel() @@ -106,9 +105,8 @@ func TestPBinTreeKeyEIPVectors(t *testing.T) { }) } -// TestPBinStorageZoneRouting walks the header/storage-zone boundary and the -// group boundary, where a mis-route stays internally consistent and so is -// invisible to a root-equality test (guards H8). +// Walks the header/storage-zone boundary and the group boundary. A mis-route +// there stays internally consistent, so a root-equality test cannot see it. func TestPBinStorageZoneRouting(t *testing.T) { t.Parallel() @@ -144,8 +142,6 @@ func TestPBinStorageZoneRouting(t *testing.T) { } } -// TestPBinStorageZoneKeysAreDistinct guards against a routing bug that maps two -// slots onto one key, which a root-equality test cannot see either. func TestPBinStorageZoneKeysAreDistinct(t *testing.T) { t.Parallel() @@ -160,8 +156,8 @@ func TestPBinStorageZoneKeysAreDistinct(t *testing.T) { } } -// TestPBinHighSlotRouting covers slot numbers that do not fit a uint64, where -// the tree index is a 31-byte shift of the slot rather than arithmetic. +// For slots too large for a uint64 the tree index is a 31-byte shift of the +// slot, not arithmetic on it. func TestPBinHighSlotRouting(t *testing.T) { t.Parallel() @@ -181,8 +177,7 @@ func TestPBinHighSlotRouting(t *testing.T) { require.Equal(t, pbinTestConcat([]byte{0xFF}, stem, suffix, []byte{slot[31]}), got) } -// TestPBinAddr32Padding pins that the stem digest covers the 32-byte address, -// not the 20-byte one (guards H8). +// The stem digest covers the 32-byte address, not the 20-byte one. func TestPBinAddr32Padding(t *testing.T) { t.Parallel() @@ -196,8 +191,7 @@ func TestPBinAddr32Padding(t *testing.T) { require.NotEqual(t, pbinTestKeccak(t, addr), key[1:33]) } -// TestPBinKeyHasherPrimaryLeaf pins the keyHasher contract: the primary leaf's -// tree key, sized 34 or 66 by zone. +// The keyHasher contract: the primary leaf's tree key, sized 34 or 66 by zone. func TestPBinKeyHasherPrimaryLeaf(t *testing.T) { t.Parallel() @@ -221,9 +215,8 @@ func TestPBinKeyHasherRejectsMalformedPlainKey(t *testing.T) { require.Panics(t, func() { hasher(nil) }) } -// TestPBinKeyHasherSharedAcrossBuffers hashes through two Updates buffers that -// share one hasher value (Updates.NewEmpty copies it) from two goroutines. Run -// under -race this fails if the hasher keeps a cache the copies can both write. +// Two Updates buffers share one hasher value, since Updates.NewEmpty copies it. +// Under -race this fails if the hasher keeps a cache both copies can write. func TestPBinKeyHasherSharedAcrossBuffers(t *testing.T) { t.Parallel() @@ -254,9 +247,8 @@ func TestPBinKeyHasherSharedAcrossBuffers(t *testing.T) { wg.Wait() } -// TestPBinDigestCacheMatchesFreshDerivation drives one hasher across interleaved -// addresses and slot groups: a cache entry kept past its address or tree index -// would silently place a leaf under the wrong stem. +// Interleaves addresses and slot groups through one hasher: a cache entry kept +// past its address or tree index would place a leaf under the wrong stem. func TestPBinDigestCacheMatchesFreshDerivation(t *testing.T) { t.Parallel() diff --git a/execution/commitment/pbin_oracle_test.go b/execution/commitment/pbin_oracle_test.go index 4feb94757a9..351ea36e2c3 100644 --- a/execution/commitment/pbin_oracle_test.go +++ b/execution/commitment/pbin_oracle_test.go @@ -34,11 +34,11 @@ import ( ) // The reference implementation of EIP-8297's binary tree (eip:112-222), -// transcribed from the spec's Python with no optimisation: no memoised hashes, -// no shared buffers, one bit per byte. It is the ground truth the engine is -// diffed against, so it is written to be recognisably the same algorithm rather -// than a fast one. Its Keccak comes from x/crypto, not the fastkeccak the -// engine uses, so a hasher bug cannot cancel out on both sides. +// transcribed from the spec's Python with no optimisation — no memoised hashes, +// no shared buffers, one bit per byte — because it is the ground truth the +// engine is diffed against and has to stay recognisably the same algorithm. Its +// Keccak comes from x/crypto, not the fastkeccak the engine uses, so a hasher +// bug cannot cancel out on both sides. const ( pbinOracleMaxKeyLength = 8192 @@ -168,8 +168,7 @@ func pbinOracleMerkelize(node pbinOracleNode) [32]byte { } // pbinOracleMerkelizeWith merkelizes under an explicit H. A nil sum means -// Keccak-256; the execution-specs reference uses BLAKE3, so its vectors are -// replayed by passing blake3 here. +// Keccak-256; the reference's vectors are replayed by passing BLAKE3. func pbinOracleMerkelizeWith(node pbinOracleNode, sum func([]byte) [32]byte) [32]byte { var out [32]byte if node == nil { @@ -308,9 +307,9 @@ func pbinOracleCorpusSplitAtLastBit() pbinOracleCorpus { } } -// pbinOracleCorpusSplitInsidePrefix uses synthetic account-zone keys so the -// divergence bit is exact: the first two share 15 bits, the third leaves at bit -// 9, forcing _insert down the survivor path with a non-empty remainder. +// pbinOracleCorpusSplitInsidePrefix uses synthetic account-zone keys, not +// digests, so the divergence bit is exact: the third key leaves the prefix the +// first two share, forcing insert down the survivor path. func pbinOracleCorpusSplitInsidePrefix() pbinOracleCorpus { return pbinOracleCorpus{ name: "split inside prefix", @@ -328,8 +327,8 @@ func pbinOracleSyntheticAccountKey(stemByte byte) []byte { return key } -// pbinOracleCorpusOneAccount is the realistic shape: header leaves, header-zone -// slots and storage-zone slots for a single address, all sharing a stem. +// pbinOracleCorpusOneAccount is the realistic shape: header leaves plus header- +// and storage-zone slots for one address, all sharing a stem. func pbinOracleCorpusOneAccount() pbinOracleCorpus { addr := pbinOracleAddr(4) entries := []pbinOracleEntry{ @@ -365,9 +364,8 @@ var pbinOracleMinedAddrs = sync.OnceValue(func() [][]byte { return pbinOracleMineSharedStems(pbinOracleMinedPrefixBits, pbinOracleMinedCluster) }) -// pbinOracleMineSharedStems searches for addresses whose account keys agree on -// shared leading bits. The stem is a digest, so a deep shared prefix cannot be -// constructed and has to be found by trial. +// pbinOracleMineSharedStems finds addresses whose account keys agree on the +// leading bits by trial: the stem is a digest, so it cannot be constructed. func pbinOracleMineSharedStems(shared, n int) [][]byte { const limit = 1 << 24 var target []byte @@ -389,8 +387,6 @@ func pbinOracleMineSharedStems(shared, n int) [][]byte { return found } -// TestPBinOracleEncodeBitPrefix pins encode_bit_prefix (eip:196-201) against -// hand-written bytes, since every branch hash the oracle produces depends on it. func TestPBinOracleEncodeBitPrefix(t *testing.T) { t.Parallel() @@ -425,8 +421,8 @@ func TestPBinOracleEncodeBitPrefixLongRun(t *testing.T) { require.Equal(t, bytes.Repeat([]byte{0xFF}, 66), got[2:]) } -// TestPBinOracleEmptyTreeHash guards H11 at the oracle: the empty tree is 32 -// zero bytes (eip:208), not the empty-MPT root the rest of erigon uses. +// The empty tree is 32 zero bytes (eip:208), not the empty-MPT root the rest of +// erigon uses. func TestPBinOracleEmptyTreeHash(t *testing.T) { t.Parallel() @@ -476,8 +472,6 @@ func TestPBinOracleTwoKeyRootIsBranchHash(t *testing.T) { require.Equal(t, want, got[:]) } -// TestPBinOracleSplitAtLastBit exercises the deepest split two 528-bit keys can -// have: they agree on all but the final bit. func TestPBinOracleSplitAtLastBit(t *testing.T) { t.Parallel() @@ -502,9 +496,8 @@ func TestPBinOracleSplitAtLastBit(t *testing.T) { require.Equal(t, want, got[:]) } -// TestPBinOracleSplitInsidePrefix pins the shape the split-inside-prefix branch -// of _insert produces (eip:171-182): the survivor keeps prefix[matched+1:], so -// the bit consumed by the new branch must not reappear below it. +// Pins the shape of the split-inside-prefix branch (eip:171-182): the bit the +// new branch consumes must not reappear in the survivor below it. func TestPBinOracleSplitInsidePrefix(t *testing.T) { t.Parallel() @@ -591,9 +584,8 @@ func TestPBinOracleRejectsInvalidInsert(t *testing.T) { }) } -// TestPBinOracleCorporaArePrefixFree checks every corpus satisfies the -// invariant _insert asserts, so a later differential failure is a tree bug and -// not a malformed corpus. +// Every corpus must satisfy the prefix-freedom insert asserts, so that a later +// differential failure is a tree bug and not a malformed corpus. func TestPBinOracleCorporaArePrefixFree(t *testing.T) { t.Parallel() @@ -616,9 +608,8 @@ func TestPBinOracleCorporaArePrefixFree(t *testing.T) { } } -// TestPBinOraclePermutationIndependence is the property that makes the oracle -// usable as ground truth: the root depends on the key/value set, not on the -// order entries arrive in. +// The property that makes the oracle usable as ground truth: the root depends +// on the key/value set, not on the order entries arrive in. func TestPBinOraclePermutationIndependence(t *testing.T) { t.Parallel() @@ -633,9 +624,8 @@ func TestPBinOraclePermutationIndependence(t *testing.T) { } } -// TestPBinOracleDeepSharedPrefixCorpus checks the mined cluster really does -// share a deep prefix — without that, the corpus never exercises a split far -// from the root. +// The mined cluster has to really share a deep prefix — otherwise the corpus +// never exercises a split far from the root. func TestPBinOracleDeepSharedPrefixCorpus(t *testing.T) { t.Parallel() @@ -656,8 +646,8 @@ func TestPBinOracleDeepSharedPrefixCorpus(t *testing.T) { require.GreaterOrEqual(t, len(root.prefix), pbinOracleMinedPrefixBits-1) } -// TestPBinOracleStemSharedCorpus pins that one account's keys land under a -// shared stem: the storage-zone keys agree on the 264 zone+stem bits. +// One account's storage-zone keys must land under a shared stem: they agree on +// the 8+256 zone+stem bits. func TestPBinOracleStemSharedCorpus(t *testing.T) { t.Parallel() diff --git a/execution/commitment/pbin_overflow_test.go b/execution/commitment/pbin_overflow_test.go index 4cd9fb4ae6a..c49e5ac312e 100644 --- a/execution/commitment/pbin_overflow_test.go +++ b/execution/commitment/pbin_overflow_test.go @@ -26,10 +26,9 @@ import ( "github.com/erigontech/erigon/common/length" ) -// pbinTestSpecCodeChunkKey is get_tree_key_for_code_chunk (eip:355-367) -// transcribed from the spec's Python, hashing with the independent Keccak the -// tests use. It is the ground truth the cache-backed derivation is diffed -// against. +// pbinTestSpecCodeChunkKey transcribes get_tree_key_for_code_chunk +// (eip:355-367) from the spec's Python, hashing with the independent Keccak the +// tests use. It is the ground truth for the cache-backed derivation. func pbinTestSpecCodeChunkKey(t *testing.T, addr []byte, codeHash common.Hash, chunkID int) []byte { t.Helper() if chunkID < pbinStemSubtreeWidth-pbinCodeOffset { @@ -43,9 +42,9 @@ func pbinTestSpecCodeChunkKey(t *testing.T, addr []byte, codeHash common.Hash, c return key } -// TestPBinCodeOverflowKeyMatchesSpec pins the second half of the code -// embedding: past the account header a chunk is content-addressed by code hash, -// with the overflow index split into a 32-byte tree index and a sub-index. +// TestPBinCodeOverflowKeyMatchesSpec pins the second half of the code embedding: +// past the account header a chunk is content-addressed by code hash, with the +// overflow index split into a 32-byte tree index and a sub-index. func TestPBinCodeOverflowKeyMatchesSpec(t *testing.T) { t.Parallel() @@ -72,11 +71,10 @@ func TestPBinCodeOverflowKeyMatchesSpec(t *testing.T) { "a header chunk has no code-zone key") } -// TestPBinCodeKeyNeverRoutesToTheStorageZone guards H7. An overflow key is -// derived from code_hash ‖ tree_index, a 64-byte preimage that is not a plain -// key at all: the stream's key hasher only ever sees the two plain-key shapes -// and refuses anything else, so no length can carry a code key into the storage -// zone. +// TestPBinCodeKeyNeverRoutesToTheStorageZone pins that a code key cannot reach +// the storage zone. An overflow key derives from code_hash ‖ tree_index, a +// 64-byte preimage that is not a plain key at all, and the stream's key hasher +// accepts only the two plain-key shapes. func TestPBinCodeKeyNeverRoutesToTheStorageZone(t *testing.T) { t.Parallel() @@ -98,10 +96,9 @@ func TestPBinCodeKeyNeverRoutesToTheStorageZone(t *testing.T) { } } -// TestPBinEngineCommitsOverflowCodeChunks is the code zone end to end: a -// contract whose code outgrows the account header keeps its first 128 chunks on -// the account stem and puts the rest in the code zone, and the whole leaf set -// has to match the reference tree. +// TestPBinEngineCommitsOverflowCodeChunks is the code zone end to end: code +// outgrowing the account header keeps its first 128 chunks on the account stem +// and puts the rest in the code zone. func TestPBinEngineCommitsOverflowCodeChunks(t *testing.T) { t.Parallel() @@ -132,8 +129,7 @@ func TestPBinEngineCommitsOverflowCodeChunks(t *testing.T) { // TestPBinOverflowChunksAreSharedByIdenticalCode pins the point of // content-addressing (eip:352-354): two accounts running the same bytecode name -// the same code-zone leaves, so the zone holds one copy however many accounts -// reach it. +// the same code-zone leaves, so the zone holds one copy of them. func TestPBinOverflowChunksAreSharedByIdenticalCode(t *testing.T) { t.Parallel() @@ -161,9 +157,7 @@ func TestPBinOverflowChunksAreSharedByIdenticalCode(t *testing.T) { // TestPBinOverflowChunksFollowEveryAccountZoneKey pins where the code-zone block // sits in the visit order: the zone byte puts it after every account-header key // and before every storage-zone one, so the chunks of an account visited early -// have to wait for the last account of the run. The grid only walks forward, so -// a block emitted at the wrong point fails loudly rather than rewriting a folded -// row. +// have to wait for the last account of the run. func TestPBinOverflowChunksFollowEveryAccountZoneKey(t *testing.T) { t.Parallel() diff --git a/execution/commitment/pbin_patricia_hashed.go b/execution/commitment/pbin_patricia_hashed.go index d98172816b0..ed2e870c38e 100644 --- a/execution/commitment/pbin_patricia_hashed.go +++ b/execution/commitment/pbin_patricia_hashed.go @@ -16,26 +16,14 @@ // PBinPatriciaHashed — commitment over EIP-8297's partitioned binary tree. // -// The EIP leaves its hash function open and names Keccak-256 as a candidate, -// which is this engine's default for both node hashing and tree-key derivation. -// BLAKE3 is the alternative, selected by SetPBinHashSuite and applied to both -// seams at once. It is what the execution-specs reference and the other clients -// on the shared binary-trie testnets hash with, so a node that has to agree with -// them runs on BLAKE3; a Keccak-keyed tree agrees with no other client. +// The EIP leaves its hash function open; this engine defaults to Keccak-256 for +// both node hashing and tree-key derivation. Interop runs on BLAKE3 +// (SetPBinHashSuite) — what the execution-specs reference and the other clients +// on the shared binary-trie testnets hash with; a Keccak-keyed tree agrees with +// no other client. // -// Scope: Process over all three zones, ModeDirect only. Code is chunked into the -// account header's chunk leaves, overflowing into the code zone where chunks are -// content-addressed by code hash and shared between accounts. Parallel and -// streaming mounting are structurally out — their prefix trie is nibble-shaped -// and the binary key space has no nibbles. The paths that reinterpret commitment -// records outside block execution — witness, eth_getProof, eth_simulateV1, -// receipt regeneration — refuse this variant rather than read bit-path records -// as hex ones. -// -// EIP-8297 has no removal: a zeroed storage slot keeps its leaf at 32 zero -// bytes, an account removal is refused rather than guessed at, and code chunks -// above a shortened redeploy's length stay in the tree — the tree is a function -// of history there, not of current state. +// Parallel and streaming mounting are structurally out: their prefix trie is +// nibble-shaped and the binary key space has no nibbles. package commitment @@ -68,26 +56,25 @@ type PBinPatriciaHashed struct { lastKey [pbinStorageKeyLength]byte // the deepest key visited so far, which the next one must exceed lastKeyLen int16 - traceW io.Writer // nil = disabled + traceW io.Writer - rootChecked bool // whether the root record is known to be absent + rootChecked bool rootTouched bool rootPresent bool rootPrev []byte // root record as last read or written; nil = never read } -// pbinCounters measures what keeping a single hash per branch cell costs. A +// pbinCounters measures what keeping a single hash per branch cell costs: a // probe diverging inside a stored prefix invalidates that hash, and rebuilding -// it needs a branch read the descent itself would not have made. Storing both -// child hashes per cell instead is a wire-format change, so it waits on these -// numbers. +// it needs a branch read the descent itself would not have made. The +// alternative, storing both child hashes per cell, is a wire-format change. type pbinCounters struct { splitsInsidePrefix uint64 materializeReads uint64 } -// pbinPool recycles engines: the grid is the better part of a megabyte, and -// Release leaves a pooled engine in the state a fresh one starts in. +// pbinPool recycles engines: the grid is the better part of a megabyte. Release +// must leave a pooled engine in the state a fresh one starts in. var pbinPool sync.Pool func NewPBinPatriciaHashed(ctx PatriciaContext) *PBinPatriciaHashed { @@ -103,15 +90,12 @@ func (pph *PBinPatriciaHashed) Variant() TrieVariant { return VariantBinPatricia func (pph *PBinPatriciaHashed) ResetContext(ctx PatriciaContext) { pph.ctx = ctx } -// SetTraceWriter enables tracing. M0 traces one line per run: the counters the -// split-rehash decision is waiting on. func (pph *PBinPatriciaHashed) SetTraceWriter(w io.Writer) { pph.traceW = w } -// EnableCsvMetrics is a no-op: the binary engine collects no metrics in M0. +// EnableCsvMetrics is a no-op: the binary engine collects no metrics. func (pph *PBinPatriciaHashed) EnableCsvMetrics(string) {} -// Reset drops the in-memory tree, keeping the context. The next run rebuilds -// what it descends into from stored records, starting at the root cell record. +// Reset drops the in-memory tree, keeping the context. func (pph *PBinPatriciaHashed) Reset() { pph.grid.resetForReuse() pph.currentKey = pbinBitpath{} @@ -121,8 +105,8 @@ func (pph *PBinPatriciaHashed) Reset() { pph.lastKeyLen = 0 } -// setHashSuite swaps H on both seams at once — node hashing on this engine and -// the returned key-derivation hasher — so neither can be configured without the +// setHashSuite swaps the hash on both seams at once — node hashing here and the +// returned key-derivation hasher — so neither can be configured without the // other. nil is Keccak-256 on both. func (pph *PBinPatriciaHashed) setHashSuite(sum pbinHashFn) keyHasher { pph.hasher.sum = sum @@ -149,22 +133,21 @@ var ( ) // ErrPBinUnsupported marks a code path only the hex trie implements. Callers -// wrap it with the path name so the bin variant refuses instead of no-opping. +// wrap it with the path name, so the bin variant refuses instead of no-opping. var ErrPBinUnsupported = errors.New("pbin: unsupported under the bin commitment variant") // pbinRootKey names the record holding the root cell — the one node no descent -// can name: every other node is found by the path that reaches it, while the -// root's own prefix is stored nowhere else. The sentinel cannot collide with a -// node record: every pbinAppendBitPath key ends in a trailing bit-count byte -// ≤ 7. The empty key would not do — domain iteration reads a zero-length key as -// end-of-stream, and the empty key sorts first, truncating the whole table. +// can name, since every other node is found by the path that reaches it. It +// cannot collide with a node record: every pbinAppendBitPath key ends in a +// trailing bit-count byte ≤ 7. The empty key would not do — domain iteration +// reads a zero-length key as end-of-stream, and it sorts first, truncating the +// whole table. var pbinRootKey = []byte{0x08} // Process folds the update stream into the tree and returns the new root. // HashSort hands keys over in tree-key order, which is descent order, so the -// grid only ever walks the path between two consecutive keys. -// -// warmup is ignored: the engine has no parallel read path to pre-warm for. +// grid only ever walks the path between two consecutive keys. warmup is +// ignored: there is no parallel read path to pre-warm. func (pph *PBinPatriciaHashed) Process(ctx context.Context, updates *Updates, logPrefix string, onProgress func(*CommitProgress), warmup WarmupConfig) ([]byte, error) { pph.lastKeyLen = 0 processed, err := pph.updateStream.process(ctx, updates, pph.ctx, pph.followAndUpdate) @@ -190,11 +173,9 @@ func (pph *PBinPatriciaHashed) Process(ctx context.Context, updates *Updates, lo } // followAndUpdate moves the grid onto treeKey and writes the update into the -// cell that lands there. -// -// Visits must ascend. The grid only walks forward: a fold writes the row's record -// outright, so returning to a folded row rewrites it under a touch map that no -// longer names what the first write touched. +// cell that lands there. Visits must ascend: a fold writes the row's record +// outright, so returning to a folded row would rewrite it under a touch map +// that no longer names what the first write touched. func (pph *PBinPatriciaHashed) followAndUpdate(treeKey, plainKey []byte, update *Update) error { if pph.lastKeyLen > 0 && bytes.Compare(treeKey, pph.lastKey[:pph.lastKeyLen]) <= 0 { return fmt.Errorf("%w: %x after %x", errPBinVisitOrder, treeKey, pph.lastKey[:pph.lastKeyLen]) @@ -217,7 +198,7 @@ func (pph *PBinPatriciaHashed) followAndUpdate(treeKey, plainKey []byte, update // updateCell writes one leaf into the deepest open row. Unfolding has already // made the target either empty — a new leaf, whose prefix is the rest of the -// key — or the same leaf touched again. +// key — or the same leaf touched again, never a branch. func (pph *PBinPatriciaHashed) updateCell(plainKey []byte, probe *pbinBitpath, update *Update) error { g := &pph.grid var c *pbinCell @@ -236,9 +217,8 @@ func (pph *PBinPatriciaHashed) updateCell(plainKey []byte, probe *pbinBitpath, u c = &g.rows[row][bit] } - // A key with no state reads back as a delete. Landing on an empty slot means - // there simply is no leaf here; landing on one means the leaf keeps its place - // at a zero value, or the removal is one EIP-8297 does not define. + // A key with no state reads back as a delete. With no leaf here there is + // nothing to remove; over a live one see pbinZeroedLeafUpdate. if update.Deleted() { if c.kind != pbinNodeLeaf { return nil @@ -269,7 +249,7 @@ func (pph *PBinPatriciaHashed) updateCell(plainKey []byte, probe *pbinBitpath, u switch len(plainKey) { case 0: // A code chunk has no plain key: no state domain holds one, so the leaf - // carries its own value and the branch record persists it. + // carries its own value. if _, err := pbinRecordLeafValue(update); err != nil { return err } @@ -291,9 +271,8 @@ func (pph *PBinPatriciaHashed) updateCell(plainKey []byte, probe *pbinBitpath, u // pbinZeroedLeafUpdate reinterprets an absent read over a live leaf. The domain // encodes zero and absent the same way, while EIP-8297 has no removal and holds // a zero value as a present leaf, so a zeroed storage slot keeps its leaf at 32 -// zero bytes. An absent account is a removal the EIP does not describe — its -// encoding is unverified against the reference and would silently change the -// root — so it stays refused. +// zero bytes. An absent account would be a removal the EIP does not define, so +// it stays refused rather than guessed at. func pbinZeroedLeafUpdate(plainKey []byte) (Update, error) { if len(plainKey) != length.Addr+length.Hash { return Update{}, fmt.Errorf("%w: %x", errPBinDeleteUnsupported, plainKey) @@ -301,15 +280,15 @@ func pbinZeroedLeafUpdate(plainKey []byte) (Update, error) { return Update{Flags: StorageUpdate}, nil } -// RootHash hashes whatever the root cell holds. A one-key tree's root is the -// leaf itself (eip:133-135) and an empty tree is 32 zero bytes (eip:208), both -// of which fall out of hashing the cell rather than special-casing the shape. +// RootHash hashes whatever the root cell holds: a one-key tree's root is the +// leaf itself (eip:133-135) and an empty tree is 32 zero bytes (eip:208), so +// neither shape needs a special case. func (pph *PBinPatriciaHashed) RootHash() ([]byte, error) { if pph.grid.activeRows != 0 { return nil, fmt.Errorf("pbin: root hash requested with %d rows still open", pph.grid.activeRows) } - // A run that touches no key never descends, so nothing has pulled the stored - // root in yet and an untouched grid would report the empty tree. + // A run that touches no key never descends, so without this the untouched + // grid would report the empty tree. if !pph.rootChecked { if err := pph.loadRoot(); err != nil { return nil, err @@ -323,10 +302,9 @@ func (pph *PBinPatriciaHashed) RootHash() ([]byte, error) { return hash[:], nil } -// storeRoot persists the root cell so a later engine can find the tree. Without -// it a root sitting under a non-empty prefix — every tree confined to one zone — -// is unreachable, and a run that finds nothing rebuilds from the touched keys -// alone. +// storeRoot persists the root cell so a later engine can find the tree: a root +// sitting under a non-empty prefix — every tree confined to one zone — is +// reachable no other way. func (pph *PBinPatriciaHashed) storeRoot() error { if !pph.rootTouched { return nil @@ -347,8 +325,8 @@ func (pph *PBinPatriciaHashed) storeRoot() error { return nil } -// loadRoot reads the stored root cell into the grid. An absent record means the -// tree is empty; anything below the root is reached from the root's own prefix. +// loadRoot reads the stored root cell into the grid; an absent record is the +// empty tree. func (pph *PBinPatriciaHashed) loadRoot() error { pph.rootChecked = true data, _, err := pph.ctx.Branch(pbinRootKey) @@ -368,8 +346,8 @@ func (pph *PBinPatriciaHashed) loadRoot() error { if pos != len(data) { return fmt.Errorf("%w: %d trailing bytes after the root cell", errPBinMalformedBranch, len(data)-pos) } - // Present but untouched: unfold reads these to decide whether the row it opens - // survives, and a loaded root that reads absent takes the tree with it. + // Present though untouched: unfold reads a touched-but-absent cell as a + // deleted subtree (see unfoldBranchNode). pph.rootPresent = true return nil } @@ -404,7 +382,7 @@ type pbinUnfolding struct { // needUnfolding reports what the grid still needs before probe's slot is in it. // Unlike the hex engine there is no terminator to discount and no account -// boundary to clamp to — one key space, one bit per level. +// boundary to clamp to: one key space, one bit per level. func (pph *PBinPatriciaHashed) needUnfolding(probe *pbinBitpath) pbinUnfolding { var cell *pbinCell var depth int16 @@ -446,8 +424,6 @@ func (pph *PBinPatriciaHashed) needUnfolding(probe *pbinBitpath) pbinUnfolding { return pbinUnfolding{action: pbinUnfoldDescend, matched: matched} } -// unfold opens one more level of the grid along probe, per the plan needUnfolding -// produced. func (pph *PBinPatriciaHashed) unfold(probe *pbinBitpath, u pbinUnfolding) error { if u.action == pbinUnfoldNone { return nil @@ -511,8 +487,8 @@ func (pph *PBinPatriciaHashed) unfold(probe *pbinBitpath, u pbinUnfolding) error } // pbinUnfoldConsumed is how many of the cell's prefix bits this unfold takes: -// all of them when the probe key matched, and one past the divergence when it -// did not — that extra bit is what the new row branches on. +// all of them when the probe key matched, one past the divergence when it did +// not — that extra bit is what the new row branches on. func pbinUnfoldConsumed(u pbinUnfolding, prefix *pbinBitpath) (int16, error) { switch u.action { case pbinUnfoldDescend: @@ -528,8 +504,9 @@ func pbinUnfoldConsumed(u pbinUnfolding, prefix *pbinBitpath) (int16, error) { } // unfoldBranchNode loads the record at the current descent key into a row. The -// key is reconstructed from the parent cell's stored prefix, which is the only -// place the bits between the two nodes exist. +// key is reconstructed from the parent cell's stored prefix, the only place the +// bits between the two nodes exist. deleted marks a parent cell that was touched +// and is now gone, which takes the whole subtree below it with it. func (pph *PBinPatriciaHashed) unfoldBranchNode(row int, depth int16, deleted bool) error { g := &pph.grid key := pbinEncodeBitPath(&pph.currentKey) @@ -548,8 +525,7 @@ func (pph *PBinPatriciaHashed) unfoldBranchNode(row int, depth int16, deleted bo } g.prevRecord[row] = data // The record's own touch map is write-time bookkeeping; nothing in this run - // has touched the row yet. A parent cell that is touched but gone takes the - // whole subtree with it. + // has touched the row yet. if deleted { g.touchMap[row], g.afterMap[row] = afterMap, 0 } else { @@ -562,8 +538,8 @@ func (pph *PBinPatriciaHashed) unfoldBranchNode(row int, depth int16, deleted bo } // fillFromUpperCell moves a cell one level down, dropping the prefix bits the -// descent has taken over. skip counts those bits and includes the one the new -// row branches on. It re-cuts the prefix, so the caller owes the cell a +// descent has taken over; skip counts those and includes the bit the new row +// branches on. The prefix is re-cut, so the caller owes the cell a // rehashAfterPrefixChange. func (c *pbinCell) fillFromUpperCell(up *pbinCell, skip int16) { c.reset() @@ -589,7 +565,7 @@ func (c *pbinCell) fillFromUpperCell(up *pbinCell, skip int16) { } // fillFromLowerCell moves the sole survivor of a collapsed row into the cell -// above, prepending the bits the row consumed: the ones the parent already +// above, prepending the bits the row consumed: those the parent already // descended plus the one the row branched on. func (c *pbinCell) fillFromLowerCell(low *pbinCell, head *pbinBitpath, bit uint64) { prefix := *head @@ -600,8 +576,8 @@ func (c *pbinCell) fillFromLowerCell(low *pbinCell, head *pbinBitpath, bit uint6 } // rehashAfterPrefixChange restores the invariant that a set hashLen means the -// hash covers the prefix the cell holds now. A cell that knows its children -// re-derives; one that does not is marked stale for materializeBranch. +// hash covers the prefix the cell holds now. A cell that cannot re-derive is +// left stale for materializeBranch. func (pph *PBinPatriciaHashed) rehashAfterPrefixChange(c *pbinCell) { if c.kind != pbinNodeBranch { return @@ -659,8 +635,8 @@ func (pph *PBinPatriciaHashed) fold() error { return nil } -// foldBranch hashes a row that keeps both cells and stores it as one record, -// keyed by the bit path down to the branch bit. +// foldBranch stores a row that keeps both cells as one record, keyed by the bit +// path down to the branch bit. func (pph *PBinPatriciaHashed) foldBranch(row int, bit uint64, upDepth, depth int16, upCell *pbinCell) error { g := &pph.grid if n := bits.OnesCount16(g.afterMap[row]); n != 2 { @@ -700,7 +676,7 @@ func (pph *PBinPatriciaHashed) foldBranch(row int, bit uint64, upDepth, depth in } // foldPropagate collapses a row down to its sole survivor. The node moves up -// rather than being rewritten, so no record is written and the bits the row +// rather than being rewritten: no record is written, and the bits the row // consumed are prepended to the survivor's own prefix. func (pph *PBinPatriciaHashed) foldPropagate(row int, bit uint64, upDepth, depth int16, upCell *pbinCell) error { g := &pph.grid @@ -711,8 +687,8 @@ func (pph *PBinPatriciaHashed) foldPropagate(row int, bit uint64, upDepth, depth head := pph.currentKey.slice(upDepth, depth-1) upCell.fillFromLowerCell(child, &head, uint64(childBit)) - // The row's own bit is part of what moves up: dropping it still hashes, and - // still gives the wrong root. + // The row's own branch bit is part of what moves up: dropping it still hashes, + // and still gives the wrong root. if want := depth - upDepth + child.prefix.bitLen; upCell.prefix.bitLen != want { return fmt.Errorf("pbin: propagate at row %d formed a %d-bit prefix, want %d", row, upCell.prefix.bitLen, want) } @@ -743,9 +719,9 @@ func (pph *PBinPatriciaHashed) foldDelete(row int, bit uint64, upCell *pbinCell) return nil } -// propagateTouch carries a modification to the row above. A fold that leaves a -// node behind also marks the root present: without it the next unfold reads -// touched and absent, and drops the whole subtree. +// propagateTouch carries a modification to the row above. A fold at row 0 that +// leaves a node behind must also mark the root present, or the next unfold reads +// it as a deleted subtree. func (pph *PBinPatriciaHashed) propagateTouch(row int, bit uint64) { if pph.grid.touchMap[row] == 0 { return @@ -758,7 +734,7 @@ func (pph *PBinPatriciaHashed) propagateTouch(row int, bit uint64) { } // hashRowCell hashes one cell of a folding row and writes the result back, so -// the record the row produces carries every child hash a later read needs. +// the row's record carries every child hash a later read needs. func (pph *PBinPatriciaHashed) hashRowCell(c *pbinCell, path *pbinBitpath) (common.Hash, error) { h, err := pph.cellHash(c, path) if err != nil { @@ -770,8 +746,6 @@ func (pph *PBinPatriciaHashed) hashRowCell(c *pbinCell, path *pbinBitpath) (comm return h, nil } -// cellHash resolves whatever a cell is missing — a leaf's state, a branch's -// stale hash — and hands it to the one hasher. func (pph *PBinPatriciaHashed) cellHash(c *pbinCell, path *pbinBitpath) (common.Hash, error) { switch c.kind { case pbinNodeLeaf: @@ -790,8 +764,7 @@ func (pph *PBinPatriciaHashed) cellHash(c *pbinCell, path *pbinBitpath) (common. // loadCellState fills a leaf cell whose plain key arrived from a record and // whose value therefore did not. The leaf is already in the tree, so an absent -// read is pbinZeroedLeafUpdate's case: a zero value for storage, a refusal for -// an account. +// read is pbinZeroedLeafUpdate's case. func (pph *PBinPatriciaHashed) loadCellState(c *pbinCell) error { if c.accountAddrLen > 0 && !c.loaded.account() { plainKey := c.accountAddr[:c.accountAddrLen] @@ -826,7 +799,7 @@ func (pph *PBinPatriciaHashed) loadCellState(c *pbinCell) error { // materializeBranch rebuilds a branch cell's hash under the prefix it holds now // by reading its own record. A split shortens a survivor's prefix without moving -// its record, so the key is the cell's path followed by that prefix. +// its record, so the record key is the cell's path followed by that prefix. func (pph *PBinPatriciaHashed) materializeBranch(c *pbinCell, path *pbinBitpath) error { nodeKey := *path nodeKey.append(&c.prefix) diff --git a/execution/commitment/pbin_process_test.go b/execution/commitment/pbin_process_test.go index 7bed830ab87..95d01efbb10 100644 --- a/execution/commitment/pbin_process_test.go +++ b/execution/commitment/pbin_process_test.go @@ -31,9 +31,8 @@ import ( "github.com/erigontech/erigon/db/kv" ) -// pbinTestCorpus collects plain-key updates in the two shapes the engine -// accepts and derives the leaf set they must produce, so a Process run can be -// diffed against the reference tree over the same entries. +// pbinTestCorpus holds plain-key updates and derives the leaf set they must +// produce, so a Process run can be diffed against the reference tree. type pbinTestCorpus struct { plainKeys [][]byte updates []Update @@ -52,8 +51,6 @@ func (c *pbinTestCorpus) accountWithCode(addr []byte, nonce, balance uint64, cod return c } -// accountWithCodeBytes is the code-bearing account with its code behind it: the -// hash and size come from the code, and the tree gains one leaf per chunk. func (c *pbinTestCorpus) accountWithCodeBytes(addr []byte, nonce, balance uint64, code []byte) *pbinTestCorpus { c.accountWithCode(addr, nonce, balance, keccak.Sum256(code), uint64(len(code))) if c.codes == nil { @@ -71,8 +68,8 @@ func (c *pbinTestCorpus) storage(addr, slot []byte, value ...byte) *pbinTestCorp return c } -// entries is the leaf set the corpus stands for. An account is two leaves, so -// this is also where the fan-out is stated independently of the engine. +// entries is the leaf set the corpus stands for. An account is two leaves — +// stated here independently of the engine. func (c *pbinTestCorpus) entries(t *testing.T) []pbinOracleEntry { t.Helper() entries := make([]pbinOracleEntry, 0, len(c.plainKeys)) @@ -105,8 +102,8 @@ func (c *pbinTestCorpus) entries(t *testing.T) []pbinOracleEntry { return entries } -// pbinTestChunkKey is where chunk chunkID of addr's code lives: the account's -// own stem while the header holds it, the content-addressed code zone after. +// pbinTestChunkKey: header chunks live in the account's own stem, the rest in +// the content-addressed code zone. func pbinTestChunkKey(addr []byte, codeHash common.Hash, chunkID int) []byte { if chunkID < pbinHeaderCodeChunks { return pbinTreeKeyCodeChunk(addr, chunkID) @@ -120,9 +117,8 @@ func (c *pbinTestCorpus) oracleRoot(t *testing.T) []byte { return root[:] } -// process applies the corpus to state, then runs it through the engine the way -// the domain layer would: ModeDirect, so every value comes back through the -// context rather than the update stream. +// process runs the corpus the way the domain layer would: ModeDirect, so every +// value comes back through the context rather than the update stream. func (c *pbinTestCorpus) process(t *testing.T) (*PBinPatriciaHashed, []byte) { t.Helper() pph, ms := pbinTestEngine(t) @@ -131,8 +127,8 @@ func (c *pbinTestCorpus) process(t *testing.T) (*PBinPatriciaHashed, []byte) { } // applyTo writes the corpus into state, code included: the engine reads code -// through the context, so a code-bearing account with no code behind it is a -// state the corpus must not produce. +// through the context, so a code-bearing account with no code behind it is an +// invalid corpus. func (c *pbinTestCorpus) applyTo(t *testing.T, ms *MockState) { t.Helper() require.NoError(t, ms.applyPlainUpdates(c.plainKeys, c.updates)) @@ -149,9 +145,8 @@ func pbinTestProcess(t *testing.T, pph *PBinPatriciaHashed, plainKeys [][]byte, return root } -// TestPBinRootHashEmptyEngine guards H11 at the engine boundary: an empty -// EIP-8297 tree is 32 zero bytes (eip:208), not the empty-MPT root the rest of -// erigon reaches for. +// TestPBinRootHashEmptyEngine: an empty EIP-8297 tree is 32 zero bytes +// (eip:208), not the empty-MPT root the rest of erigon reaches for. func TestPBinRootHashEmptyEngine(t *testing.T) { t.Parallel() @@ -179,8 +174,6 @@ func TestPBinProcessSingleKeyRootIsLeaf(t *testing.T) { require.Equal(t, corpus.oracleRoot(t), root) } -// TestPBinProcessTwoKeysRootIsBranch is the other half: a second entry turns the -// root into a branch over the two leaf hashes. func TestPBinProcessTwoKeysRootIsBranch(t *testing.T) { t.Parallel() @@ -192,7 +185,7 @@ func TestPBinProcessTwoKeysRootIsBranch(t *testing.T) { require.Equal(t, pbinNodeBranch, pph.grid.root.kind) // The two sub-indices differ only in their low bit, so the branch prefix is - // every bit of the key but the last and slot 256 takes the left side. + // every bit of the key but the last, and slot 256 takes the left side. left := pbinEncodeStorageValue([]byte{0xAA}) right := pbinEncodeStorageValue([]byte{0xBB}) leftHash := pbinTestKeccak(t, []byte{0x00}, pbinTreeKeyStorage(addr, a), left[:]) @@ -204,8 +197,6 @@ func TestPBinProcessTwoKeysRootIsBranch(t *testing.T) { require.Equal(t, corpus.oracleRoot(t), root) } -// TestPBinProcessMatchesOracle is the M0 gate: for every corpus shape the engine -// must reproduce the reference tree's root. func TestPBinProcessMatchesOracle(t *testing.T) { t.Parallel() @@ -278,8 +269,8 @@ func pbinTestMixedCorpus() *pbinTestCorpus { return c } -// pbinTestDeepSharedPrefixCorpus reuses the mined cluster, so the descent walks -// far past the root before diverging (guards H1's corpus side). +// pbinTestDeepSharedPrefixCorpus uses mined addresses, so the descent walks far +// past the root before diverging. func pbinTestDeepSharedPrefixCorpus() *pbinTestCorpus { c := new(pbinTestCorpus) for i, addr := range pbinOracleMinedAddrs() { @@ -288,9 +279,9 @@ func pbinTestDeepSharedPrefixCorpus() *pbinTestCorpus { return c } -// TestPBinProcessAccountFansOutToCodeHash pins the sibling leaf: one account -// update produces both BASIC_DATA and CODE_HASH, written during the same stem -// visit so the shared keyHasher stays a one-key function. +// TestPBinProcessAccountFansOutToCodeHash: one account update produces both +// BASIC_DATA and CODE_HASH, written during the same stem visit so the shared +// keyHasher stays a one-key function. func TestPBinProcessAccountFansOutToCodeHash(t *testing.T) { t.Parallel() @@ -313,9 +304,9 @@ func TestPBinProcessAccountFansOutToCodeHash(t *testing.T) { require.Equal(t, codeHash[:], code[:]) } -// TestPBinProcessRejectsStreamDelete guards H13: EIP-8297 never removes an -// entry, so a delete arriving on the update stream is an error rather than a -// silently applied removal. +// TestPBinProcessRejectsStreamDelete: EIP-8297 never removes an entry, so a +// delete arriving on the update stream is an error rather than a silently +// applied removal. func TestPBinProcessRejectsStreamDelete(t *testing.T) { t.Parallel() @@ -329,9 +320,9 @@ func TestPBinProcessRejectsStreamDelete(t *testing.T) { require.ErrorIs(t, err, errPBinDeleteUnsupported) } -// TestPBinProcessMissingStateIsAbsent is H13's other half: a context read for a -// key with no state reports DeleteUpdate, which means "no leaf here" and must -// not be mistaken for a removal. +// TestPBinProcessMissingStateIsAbsent: a context read for a key with no state +// reports DeleteUpdate, which means "no leaf here" and must not be mistaken for +// a removal. func TestPBinProcessMissingStateIsAbsent(t *testing.T) { t.Parallel() @@ -353,12 +344,11 @@ func TestPBinProcessMissingStateIsAbsent(t *testing.T) { require.Equal(t, present.oracleRoot(t), root, "keys with no state contribute no leaf") } -// The absent read over a live leaf — the case this one must not be confused -// with — is pbin_zerovalue_test.go's: storage keeps the leaf at a zero value, -// an account removal stays refused. +// The neighbouring case — an absent read over a live leaf — is in +// pbin_zerovalue_test.go. -// TestPBinProcessRepeatedKeyKeepsOneLeaf checks a stem touched twice in one run -// still holds a single leaf, so the second visit updates rather than splits. +// TestPBinProcessRepeatedKeyKeepsOneLeaf: a key rewritten by a later batch +// updates its leaf instead of splitting the stem. func TestPBinProcessRepeatedKeyKeepsOneLeaf(t *testing.T) { t.Parallel() @@ -377,8 +367,6 @@ func TestPBinProcessRepeatedKeyKeepsOneLeaf(t *testing.T) { require.Equal(t, second.oracleRoot(t), root) } -// TestPBinProcessEmptyUpdatesKeepsEmptyRoot checks the drive loop over nothing: -// the root stays the empty-tree constant instead of picking up a shape. func TestPBinProcessEmptyUpdatesKeepsEmptyRoot(t *testing.T) { t.Parallel() @@ -389,9 +377,9 @@ func TestPBinProcessEmptyUpdatesKeepsEmptyRoot(t *testing.T) { var errPBinTestContext = errors.New("pbin test: context failure") -// pbinFailingContext fails one context call, letting a chosen number through -// first, so each read and write the engine makes can be checked to reach the -// caller instead of being swallowed. +// pbinFailingContext fails one context call after letting skip of them through, +// so a single read or write can be checked to reach the caller instead of being +// swallowed. type pbinFailingContext struct { PatriciaContext method string @@ -438,9 +426,9 @@ func (c *pbinFailingContext) Storage(plainKey []byte) (*Update, error) { return c.PatriciaContext.Storage(plainKey) } -// TestPBinProcessSurfacesContextErrors runs a second batch over a stored tree — -// the path that reads the root cell, a node record, a leaf's state and a branch -// it has to rebuild — and fails one call at a time. +// TestPBinProcessSurfacesContextErrors runs a second batch over a stored tree, +// because only that path reads the root cell, a node record and a leaf's state, +// and fails one call at a time. func TestPBinProcessSurfacesContextErrors(t *testing.T) { t.Parallel() diff --git a/execution/commitment/pbin_rootkey_test.go b/execution/commitment/pbin_rootkey_test.go index dd36f85bd8e..ab55cf99593 100644 --- a/execution/commitment/pbin_rootkey_test.go +++ b/execution/commitment/pbin_rootkey_test.go @@ -29,7 +29,7 @@ import ( ) // pbinTestStoredTree runs a small corpus through the engine and returns the -// backing state with every record the run persisted, plus the root it computed. +// state it persisted plus the root it computed. func pbinTestStoredTree(t *testing.T) (*MockState, []byte) { t.Helper() corpus := new(pbinTestCorpus). @@ -40,11 +40,11 @@ func pbinTestStoredTree(t *testing.T) (*MockState, []byte) { return ms, pbinTestProcess(t, pph, corpus.plainKeys, corpus.updates) } -// TestPBinRootRecordRealTableIteration guards H2: every record a Process run -// writes, the root record included, must survive a round-trip through the real -// TblCommitmentVals table. Domain iteration treats a zero-length key as -// end-of-stream, and the empty key sorts first — a root record stored under it -// truncates the whole iteration and the datadir reads back as fresh. +// Every record a Process run writes, the root record included, must survive a +// round-trip through the real TblCommitmentVals table. Domain iteration treats a +// zero-length key as end-of-stream and the empty key sorts first, so a root +// record stored under it truncates the iteration and the datadir reads back as +// fresh. func TestPBinRootRecordRealTableIteration(t *testing.T) { t.Parallel() @@ -76,10 +76,9 @@ func TestPBinRootRecordRealTableIteration(t *testing.T) { require.Equal(t, rootRecord, gotRoot, "root record lost or damaged by the table round-trip") } -// TestPBinRootKeySentinelNotABitPath pins the root key to a shape no bit-path -// key can take. Every pbinAppendBitPath encoding ends in a trailing bit-count -// byte ≤ 7, so a single byte ≥ 0x08 cannot collide with any encoded path, and -// pbinDecodeBitPath must reject it outright. +// Every pbinAppendBitPath encoding ends in a trailing bit-count byte ≤ 7, so a +// single byte ≥ 0x08 cannot collide with any encoded path, and pbinDecodeBitPath +// must reject it outright. func TestPBinRootKeySentinelNotABitPath(t *testing.T) { t.Parallel() @@ -99,9 +98,8 @@ func TestPBinRootKeySentinelNotABitPath(t *testing.T) { } } -// TestPBinLoadRootNoRecordVersusStoredTree asserts loadRoot tells a fresh -// datadir from a persisted tree: no record reads back as the empty tree, while -// a stored record must reproduce the stored root, never a fresh one. +// loadRoot must tell a fresh datadir from a persisted tree: no record reads back +// as the empty tree, a stored record as the root it was built with. func TestPBinLoadRootNoRecordVersusStoredTree(t *testing.T) { t.Parallel() diff --git a/execution/commitment/pbin_specengine_test.go b/execution/commitment/pbin_specengine_test.go index d3df88fd346..34729cd5712 100644 --- a/execution/commitment/pbin_specengine_test.go +++ b/execution/commitment/pbin_specengine_test.go @@ -13,14 +13,7 @@ import ( "github.com/erigontech/erigon/common/length" ) -// Drives the engine itself over the reference's root vectors, rather than the -// oracle. The vectors carry raw tree keys and raw 32-byte values, while the -// engine rebuilds a leaf's value from an Update according to where the key sits, -// so each value has to be mapped back onto the field the engine will read. -// -// Every position the embedding defines is expressible, so the exclusion list is -// asserted empty: a vector the mapping cannot express fails this test rather -// than being skipped. +// Drives the engine itself over the reference's root vectors, rather than the oracle. type pbinEngineLeaf struct { treeKey []byte @@ -28,8 +21,9 @@ type pbinEngineLeaf struct { update Update } -// pbinLeafFromVector maps a raw (key, value) pair onto the Update the engine -// reads for that key's position. +// pbinLeafFromVector maps a raw (key, value) vector onto the Update the engine +// reads for that key's position: the engine rebuilds a leaf's value from Update +// fields, so a raw 32-byte value has to land on the field it will be read from. func pbinLeafFromVector(key, value []byte, seq int) pbinEngineLeaf { var l pbinEngineLeaf l.treeKey = key @@ -47,7 +41,7 @@ func pbinLeafFromVector(key, value []byte, seq int) pbinEngineLeaf { l.update.StorageLen = int8(copy(l.update.Storage[:], value)) } // A leaf carrying its own 32 bytes has no plain key: a code chunk, or a - // sub-index the embedding has reserved and defined no packing for. + // reserved sub-index with no defined packing. recordLeaf := func() { l.plainKey = nil l.update.Flags = StorageUpdate @@ -107,9 +101,7 @@ func pbinSpecEngineRoot(t *testing.T, pph *PBinPatriciaHashed, tc pbinSpecTrieVe return hex.EncodeToString(got) } -// TestPBinReleaseClearsHashSuite pins pooling hygiene: a released engine must -// come back on the Keccak default, not carrying a previous user's BLAKE3. -// Not parallel — it inspects a pooled object. +// Not parallel: it inspects engines coming out of the shared pool. func TestPBinReleaseClearsHashSuite(t *testing.T) { pph := NewPBinPatriciaHashed(NewMockState(t)) pph.setHashSuite(pbinBlake3Hash) diff --git a/execution/commitment/pbin_specroots_test.go b/execution/commitment/pbin_specroots_test.go index 744ea014ca5..adbdd10c50a 100644 --- a/execution/commitment/pbin_specroots_test.go +++ b/execution/commitment/pbin_specroots_test.go @@ -11,26 +11,18 @@ import ( "github.com/erigontech/erigon/common" ) -// Root vectors exported from the EIP-8297 reference implementation in -// ethereum/execution-specs (branch projects/binary-trie), which hashes with -// BLAKE3. Replaying them under BLAKE3 checks this package's oracle against an -// implementation that was written independently and, more importantly, builds -// the tree by a different algorithm: the reference rebuilds canonically, the -// oracle inserts incrementally as the EIP's pseudocode does. Agreement across -// that difference is what rules out a shared misreading of the spec. -// -// The engine itself is tied to this oracle by the differential tests, so the -// chain reaches the engine even though the engine hashes with Keccak-256. +// Replays the reference's root vectors (see pbinSpecVectors) against the oracle +// under BLAKE3. The reference rebuilds the tree canonically while the oracle +// inserts incrementally as the EIP's pseudocode does, so agreement across the +// two algorithms is what rules out a shared misreading of the spec. func pbinBlake3Sum(b []byte) [32]byte { return blake3.Sum256(b) } -// pbinBlake3Hash adapts pbinBlake3Sum to the engine's injectable hash seam. var pbinBlake3Hash pbinHashFn = func(b []byte) common.Hash { return common.Hash(blake3.Sum256(b)) } -// pbinOracleRootOf builds the oracle trie from a whole key set and merkelizes it -// under BLAKE3. Building from the surviving set is also how a delete is applied: -// the EIP's insert has no removal, and the reference's removal semantics are -// still open, so nothing here depends on a delete algorithm. +// pbinOracleRootOf rebuilds the oracle trie from the whole key set. A delete is +// applied the same way — the EIP's insert has no removal — so nothing here +// depends on a delete algorithm. func pbinOracleRootOf(t *testing.T, entries map[string][]byte) [32]byte { t.Helper() keys := make([]string, 0, len(entries)) @@ -67,9 +59,8 @@ func TestPBinOracleMatchesSpecTrieRoots(t *testing.T) { } } -// TestPBinOracleMatchesSpecSequenceRoots replays the reference's op sequences, -// checking the root after every operation rather than only at the end, so a -// divergence is pinned to the op that caused it. +// Checks the root after every op in a reference sequence, not only at the end, +// so a divergence pins to the op that caused it. func TestPBinOracleMatchesSpecSequenceRoots(t *testing.T) { t.Parallel() v := pbinLoadSpecVectors(t) diff --git a/execution/commitment/pbin_specvectors_test.go b/execution/commitment/pbin_specvectors_test.go index eaf01b7fce6..f91f876e596 100644 --- a/execution/commitment/pbin_specvectors_test.go +++ b/execution/commitment/pbin_specvectors_test.go @@ -12,9 +12,8 @@ import ( // Vectors exported from the EIP-8297 reference implementation in // ethereum/execution-specs (branch projects/binary-trie), which hashes with -// BLAKE3. The BASIC_DATA packing involves no hash and is compared as-is; key -// derivation is replayed under BLAKE3 through the injectable seam, so the -// tree-key bodies compare in full. +// BLAKE3. Comparisons against them either involve no hash (BASIC_DATA packing) +// or replay derivation under BLAKE3 through the injectable seam. type pbinSpecVectors struct { Meta map[string]string `json:"meta"` BasicData []struct { @@ -78,8 +77,8 @@ func pbinMustHex(t *testing.T, s string) []byte { return b } -// TestPBinSpecBasicDataVectors is the one fully external check available under a -// different hash: BASIC_DATA packing is pure byte layout. +// BASIC_DATA packing is pure byte layout, so it compares against the reference +// directly despite the differing hash. func TestPBinSpecBasicDataVectors(t *testing.T) { t.Parallel() v := pbinLoadSpecVectors(t) @@ -96,11 +95,10 @@ func TestPBinSpecBasicDataVectors(t *testing.T) { } } -// TestPBinSpecKeyRouting compares full tree keys against the reference under -// the BLAKE3 the vectors were generated with — zone, digest bodies and -// sub-index alike. Full equality through the production keyHasher seam is what -// proves no derivation step hashes outside it: a hardcoded Keccak site would -// diverge here (guards H3). +// Compares full tree keys — zone, digest body and sub-index — against the +// reference under BLAKE3. Going through the production key hasher is what +// catches a derivation step that hashes outside the seam: a hardcoded Keccak +// site would diverge here. func TestPBinSpecKeyRouting(t *testing.T) { t.Parallel() v := pbinLoadSpecVectors(t) diff --git a/execution/commitment/pbin_state.go b/execution/commitment/pbin_state.go index 9d1d5a47d8b..78e808f2c75 100644 --- a/execution/commitment/pbin_state.go +++ b/execution/commitment/pbin_state.go @@ -23,10 +23,8 @@ import ( ) // The pbin state blob is the root cell plus the three root flags — nothing per -// row. State is only encoded with every row folded, and unfold fully -// initializes a row before anything reads it, so the grid arrays restore as -// zero. Depths in particular are never serialized, and no depth ever meets a -// one-byte encoding. +// row. State is only encoded with every row folded, and unfold fully initializes +// a row before anything reads it, so the grid arrays restore as zero. const ( // pbinStateMarker opens every pbin blob. A hex blob opens with a root-flags // byte ≤ 0x07, so the marker also refuses a cross-variant restore outright. @@ -72,8 +70,7 @@ func (pph *PBinPatriciaHashed) EncodeCurrentState(buf []byte) ([]byte, error) { return buf, nil } -// SetState is the inverse of EncodeCurrentState; nil or empty resets the engine, -// and the tree is then found again through the stored root record. +// SetState is the inverse of EncodeCurrentState; an empty blob resets the engine. func (pph *PBinPatriciaHashed) SetState(buf []byte) error { if pph.grid.activeRows != 0 { return fmt.Errorf("%w: cannot restore over %d rows", errPBinStateOpen, pph.grid.activeRows) diff --git a/execution/commitment/pbin_state_test.go b/execution/commitment/pbin_state_test.go index d2baf0467b5..ab7a83f3987 100644 --- a/execution/commitment/pbin_state_test.go +++ b/execution/commitment/pbin_state_test.go @@ -24,11 +24,9 @@ import ( "github.com/erigontech/erigon/common/length" ) -// TestPBinRestartRoundTripDeepPath guards H6: two same-group storage slots share -// the first 520 bits of their tree keys, so the tree's one branch sits deeper -// than any depth a single byte can hold. The engine must encode its state after -// a full fold, restore it in a fresh engine, and keep folding correctly past -// the restart. +// Two same-group storage slots share the first 520 bits of their tree keys, so +// the tree's one branch sits deeper than any depth a single byte can hold. The +// encoded state has to carry that depth across a restart. func TestPBinRestartRoundTripDeepPath(t *testing.T) { t.Parallel() @@ -64,9 +62,8 @@ func TestPBinRestartRoundTripDeepPath(t *testing.T) { require.Equal(t, full.oracleRoot(t), rootContinued, "the restored engine must keep folding correctly") } -// TestPBinStateBlobRoundTripsFlags checks the three root flags survive the blob: -// they are the only engine state beside the root cell, so losing one changes how -// the next run treats the stored tree. +// The three root flags are the only engine state beside the root cell, so losing +// one to the blob changes how the next run treats the stored tree. func TestPBinStateBlobRoundTripsFlags(t *testing.T) { t.Parallel() @@ -88,9 +85,8 @@ func TestPBinStateBlobRoundTripsFlags(t *testing.T) { require.Equal(t, storedRoot, root) } -// TestPBinSetStateEmptyResetsToStored pins the hex convention: no state blob -// resets the engine, and the tree is then found again through the stored root -// record rather than being lost. +// Following the hex convention, no state blob resets the engine; the tree is +// then found again through the stored root record rather than lost. func TestPBinSetStateEmptyResetsToStored(t *testing.T) { t.Parallel() @@ -104,9 +100,9 @@ func TestPBinSetStateEmptyResetsToStored(t *testing.T) { require.Equal(t, storedRoot, root) } -// TestPBinSetStateRejectsForeignBlob: the blob is read back by whatever engine -// the datadir opens with, so a pbin engine handed a hex blob (or a damaged pbin -// one) must refuse it instead of decoding garbage into the root cell. +// The blob is read back by whatever engine the datadir opens with, so a pbin +// engine handed a hex blob (or a damaged pbin one) must refuse it instead of +// decoding garbage into the root cell. func TestPBinSetStateRejectsForeignBlob(t *testing.T) { t.Parallel() @@ -132,8 +128,7 @@ func TestPBinSetStateRejectsForeignBlob(t *testing.T) { } } -// TestPBinStateRefusesOpenRows pins the precondition the root-cell blob rests -// on: with a row still open, part of the tree lives in the grid arrays and a +// With a row still open, part of the tree lives in the grid arrays and a // root-cell snapshot would silently drop it. func TestPBinStateRefusesOpenRows(t *testing.T) { t.Parallel() diff --git a/execution/commitment/pbin_unfold_test.go b/execution/commitment/pbin_unfold_test.go index 2a83b7decd6..3a598f942a7 100644 --- a/execution/commitment/pbin_unfold_test.go +++ b/execution/commitment/pbin_unfold_test.go @@ -33,8 +33,8 @@ func pbinTestEngine(t *testing.T) (*PBinPatriciaHashed, *MockState) { return NewPBinPatriciaHashed(ms), ms } -// pbinTestSpecCell builds a cell whose prefix is spelled out bit by bit, so a -// test can name a divergence point instead of deriving one. +// pbinTestSpecCell spells a cell prefix out bit by bit, so a test can name a +// divergence point instead of deriving one. func pbinTestSpecCell(t *testing.T, kind pbinNodeKind, spec string) pbinCell { t.Helper() c := pbinTestEmptyCell() @@ -67,16 +67,14 @@ func pbinTestPutRootCell(t *testing.T, ms *MockState, c pbinCell) { require.NoError(t, ms.PutBranch(pbinRootKey, rec, nil)) } -// pbinTestPutTopRecord seeds a node record at the empty path together with the -// root cell that names it — the pair a stored tree always writes. +// pbinTestPutTopRecord seeds a node record at the empty path plus the root cell +// naming it — the pair a stored tree always writes. func pbinTestPutTopRecord(t *testing.T, ms *MockState, cells [2]pbinCell) { t.Helper() pbinTestPutRecord(t, ms, pbinBitpath{}, cells) pbinTestPutRootCell(t, ms, pbinTestSpecCell(t, pbinNodeBranch, "")) } -// pbinTestUnfoldStep opens one more row, loading the root cell first when the -// grid is still empty. func pbinTestUnfoldStep(t *testing.T, pph *PBinPatriciaHashed, probe *pbinBitpath) { t.Helper() u := pph.needUnfolding(probe) @@ -87,11 +85,10 @@ func pbinTestUnfoldStep(t *testing.T, pph *PBinPatriciaHashed, probe *pbinBitpat require.NoError(t, pph.unfold(probe, u)) } -// TestPBinNeedUnfolding guards H9: the hex engine's cpl+1 hides a terminator -// nibble, so the binary engine states each outcome instead. What matters is that -// "the probe agrees with the whole prefix" and "the probe leaves the prefix -// partway" are different answers — only the second shortens a stored prefix, -// which is inside that node's hash. +// "The probe agrees with the whole prefix" and "the probe leaves the prefix +// partway" must be different answers: only the second shortens a stored prefix, +// which is inside that node's hash. The hex engine's cpl+1 conflates them +// because it has a terminator nibble to hide behind. func TestPBinNeedUnfolding(t *testing.T) { t.Parallel() @@ -165,9 +162,6 @@ func TestPBinNeedUnfolding(t *testing.T) { } } -// TestPBinNeedUnfoldingSelectsCellByBranchBit checks the row case picks the cell -// with the bit the row branches on, the arity-2 stand-in for the hex engine's -// nibble. func TestPBinNeedUnfoldingSelectsCellByBranchBit(t *testing.T) { t.Parallel() @@ -199,9 +193,9 @@ func TestPBinNeedUnfoldingSelectsCellByBranchBit(t *testing.T) { } } -// TestPBinUnfoldEmptyPrefixBranchRecord guards H7: EIP-8297 admits a branch node -// with no prefix, so a zero-length prefix cannot double as "this cell is not a -// stored branch". The engine must read the record below and descend into it. +// EIP-8297 admits a branch node with no prefix, so a zero-length prefix cannot +// double as "this cell is not a stored branch". The engine must read the record +// below and descend into it. func TestPBinUnfoldEmptyPrefixBranchRecord(t *testing.T) { t.Parallel() @@ -237,8 +231,7 @@ func TestPBinUnfoldEmptyPrefixBranchRecord(t *testing.T) { require.Equal(t, uint16(0), pph.grid.touchMap[1]) } -// A missing record below such a cell is an inconsistency, not an empty subtree — -// the other half of H7's failure mode. +// A missing record below such a cell is an inconsistency, not an empty subtree. func TestPBinUnfoldEmptyPrefixBranchRecordMissing(t *testing.T) { t.Parallel() @@ -268,10 +261,9 @@ func TestPBinUnfoldEmptyRoot(t *testing.T) { require.Equal(t, pbinUnfolding{}, pph.needUnfolding(&probe), "a checked empty root does not unfold again") } -// TestPBinUnfoldSplitsInsidePrefix walks the divergence bit across both word -// boundaries of the [9]uint64 path and both zone lengths. A split moves the node -// below one level down and re-cuts its prefix, dropping the bit the new row -// branches on (eip:174-176). +// The divergence bit walks both word boundaries of the [9]uint64 path. A split +// moves the node below one level down and re-cuts its prefix, dropping the bit +// the new row branches on (eip:174-176). func TestPBinUnfoldSplitsInsidePrefix(t *testing.T) { t.Parallel() @@ -312,9 +304,9 @@ func TestPBinUnfoldSplitsInsidePrefix(t *testing.T) { } } -// TestPBinUnfoldDescendsThroughPrefix pins the two-step descent: consuming a -// branch cell's prefix leaves a row whose cell has none, and only then is the -// record read — at a key the parent's stored prefix is what reconstructs. +// The descent takes two steps: consuming a branch cell's prefix leaves a row +// whose cell has none, and only then is the record read — at a key the parent's +// stored prefix is what reconstructs. func TestPBinUnfoldDescendsThroughPrefix(t *testing.T) { t.Parallel() diff --git a/execution/commitment/pbin_values.go b/execution/commitment/pbin_values.go index 35928bdd0d3..e0b5bc22164 100644 --- a/execution/commitment/pbin_values.go +++ b/execution/commitment/pbin_values.go @@ -31,8 +31,8 @@ import ( // pbinValueLength is the one leaf value size EIP-8297 admits (eip:132). const pbinValueLength = 32 -// BASIC_DATA field offsets within the leaf value (eip:332-339). Bytes 1..3 are -// reserved and version is always zero, since writing any header field resets it. +// BASIC_DATA field offsets within the leaf value (eip:332-339). Byte 0 (version) +// and the reserved bytes 1..3 stay zero. const ( pbinBasicDataCodeSizeOffset = 4 pbinBasicDataNonceOffset = 8 @@ -44,9 +44,9 @@ var ( errPBinCodeSizeOverflow = errors.New("pbin: code size does not fit the 4-byte BASIC_DATA field") ) -// pbinEncodeBasicData packs version, code_size, nonce and balance big-endian -// into the BASIC_DATA leaf value. A balance the 16-byte field cannot hold is an -// error rather than a silent truncation, which would commit a wrong root. +// pbinEncodeBasicData packs code_size, nonce and balance big-endian into the +// BASIC_DATA leaf value. A value the field cannot hold is an error rather than a +// silent truncation, which would commit a wrong root. func pbinEncodeBasicData(nonce uint64, balance *uint256.Int, codeSize uint64) ([pbinValueLength]byte, error) { var v [pbinValueLength]byte if balance.BitLen() > 128 { @@ -63,7 +63,7 @@ func pbinEncodeBasicData(nonce uint64, balance *uint256.Int, codeSize uint64) ([ } // pbinCodeHashValue returns the CODE_HASH leaf value, mapping an unset hash to -// the hash of empty bytecode as the spec requires for a codeless account +// the empty-bytecode hash as the spec requires for a codeless account // (eip:345-347). func pbinCodeHashValue(codeHash common.Hash) [pbinValueLength]byte { if codeHash == (common.Hash{}) { @@ -72,7 +72,6 @@ func pbinCodeHashValue(codeHash common.Hash) [pbinValueLength]byte { return codeHash } -// pbinEncodeStorageValue left-pads a storage value to the fixed leaf width. func pbinEncodeStorageValue(value []byte) [pbinValueLength]byte { if len(value) > length.Hash { panic(fmt.Sprintf("pbin: storage value of %d bytes exceeds %d", len(value), length.Hash)) diff --git a/execution/commitment/pbin_values_test.go b/execution/commitment/pbin_values_test.go index a0ff8bab9d6..913f5eb1be9 100644 --- a/execution/commitment/pbin_values_test.go +++ b/execution/commitment/pbin_values_test.go @@ -26,9 +26,9 @@ import ( "github.com/erigontech/erigon/common" ) -// The expectations here are hand-written hex, never the encoder's own output: -// the Task 4 oracle consumes this same encoder, so a differential root test -// cannot see a value-encoding bug. +// The expectations are hand-written hex, never the encoder's own output: the +// oracle consumes this same encoder, so a differential root test would not see +// a value-encoding bug. func TestPBinEncodeBasicData(t *testing.T) { t.Parallel() diff --git a/execution/commitment/pbin_variant_test.go b/execution/commitment/pbin_variant_test.go index 5186cc27910..05c420eefec 100644 --- a/execution/commitment/pbin_variant_test.go +++ b/execution/commitment/pbin_variant_test.go @@ -26,9 +26,9 @@ import ( "github.com/erigontech/erigon/common" ) -// TestInitializeTrieAndUpdates_BinVariant pins the registration. M0 runs the -// binary engine in ModeDirect whatever mode the caller asks for: ModeParallel's -// prefix trie is a hex-nibble structure with no meaning at arity 2. +// The binary engine runs in ModeDirect whatever mode the caller asks for: +// ModeParallel's prefix trie is a hex-nibble structure with no meaning at +// arity 2. func TestInitializeTrieAndUpdates_BinVariant(t *testing.T) { t.Parallel() @@ -53,9 +53,9 @@ func TestParseTrieVariantBin(t *testing.T) { require.Equal(t, VariantParallelHexPatricia, ParseTrieVariant("parallel")) } -// TestPBinResetReuse checks that a run over a populated state depends only on -// what the context holds: an engine that dropped its in-memory root, and one -// that never had it, must both reproduce the root of the run that built it. +// A run over a populated state depends only on what the context holds: an engine +// that dropped its in-memory root and one that never had it must both reproduce +// the root of the run that built it. func TestPBinResetReuse(t *testing.T) { t.Parallel() @@ -73,10 +73,10 @@ func TestPBinResetReuse(t *testing.T) { require.Equal(t, want, pbinTestProcess(t, fresh, corpus.plainKeys, corpus.updates), "fresh engine over the same state agrees") } -// TestPBinResetReuseTouchingOneKey is the reuse case a re-run of the whole -// corpus hides: after Reset the engine must find the leaves it is not told about -// again. A tree confined to one zone has a non-empty root prefix, so its top -// record is not at the zero-bit key and only the root cell record names it. +// Re-running the whole corpus hides this case: after Reset the engine must find +// the leaves it is not told about again. A tree confined to one zone has a +// non-empty root prefix, so its top record is not at the zero-bit key and only +// the root cell record names it. func TestPBinResetReuseTouchingOneKey(t *testing.T) { t.Parallel() @@ -100,8 +100,8 @@ func TestPBinResetReuseTouchingOneKey(t *testing.T) { require.Equal(t, want, pbinTestProcess(t, fresh, touchOne.plainKeys, touchOne.updates)) } -// TestPBinResetReuseSingleLeaf covers the shape that writes no node record at -// all: a one-leaf tree lives entirely in the root cell record. +// A one-leaf tree writes no node record at all: it lives entirely in the root +// cell record. func TestPBinResetReuseSingleLeaf(t *testing.T) { t.Parallel() @@ -123,7 +123,6 @@ func TestPBinResetReuseSingleLeaf(t *testing.T) { "the leaf that was the whole tree must survive a reset") } -// TestPBinResetClearsTrieState is the state-level half of the reuse contract: // Reset leaves the engine indistinguishable from a new one but keeps the // context, which the Trie interface hands over separately. func TestPBinResetClearsTrieState(t *testing.T) { @@ -148,9 +147,8 @@ func TestPBinResetClearsTrieState(t *testing.T) { require.Same(t, ms, pph.ctx) } -// TestPBinRootHashAfterResetLoadsStoredRoot pins the zero-update path the domain -// layer takes: it asks for the root without processing anything, so RootHash has -// to reach the stored tree rather than report the empty-tree hash. +// The domain layer asks for the root without processing anything, so RootHash +// has to reach the stored tree rather than report the empty-tree hash. func TestPBinRootHashAfterResetLoadsStoredRoot(t *testing.T) { t.Parallel() @@ -174,7 +172,6 @@ func TestPBinRootHashAfterResetLoadsStoredRoot(t *testing.T) { require.Equal(t, want, empty, "a run with no updates must not shrink the tree to empty") } -// TestPBinResetContext swaps the state under a released-and-reused engine. func TestPBinResetContext(t *testing.T) { t.Parallel() @@ -189,8 +186,8 @@ func TestPBinResetContext(t *testing.T) { require.Equal(t, corpus.oracleRoot(t), pbinTestProcess(t, pph, corpus.plainKeys, corpus.updates)) } -// TestPBinReleaseReuse guards the pool: a released engine carries no state into -// its next life, so the next run over a different context matches a fresh one. +// A released engine goes back to the pool, so it must carry no state into the +// next run over a different context. func TestPBinReleaseReuse(t *testing.T) { t.Parallel() @@ -206,8 +203,6 @@ func TestPBinReleaseReuse(t *testing.T) { require.Equal(t, corpus.oracleRoot(t), pbinTestProcess(t, reused, corpus.plainKeys, corpus.updates)) } -// TestPBinSetTraceWriter pins what the engine traces: the two counters the -// split-rehash decision is waiting on. func TestPBinSetTraceWriter(t *testing.T) { t.Parallel() diff --git a/execution/commitment/pbin_verify_test.go b/execution/commitment/pbin_verify_test.go index 64275214b78..825809fe325 100644 --- a/execution/commitment/pbin_verify_test.go +++ b/execution/commitment/pbin_verify_test.go @@ -28,12 +28,11 @@ import ( ) // pbinVerifier rebuilds the tree from the records the engine wrote, reading -// nothing out of the engine's own cells. The Task 4 oracle answers "is this the -// right root for these leaves"; this answers the other half — "is what landed in -// the database the tree that root came from". It walks records top down, resolves -// every child, and hashes back up with the independent Keccak the tests use. +// nothing out of the engine's own cells. Where the oracle answers "is this the +// right root for these leaves", this answers "is what landed in the database the +// tree that root came from". // -// It reports errors rather than failing the test directly, so a test can also +// It returns errors rather than failing the test directly, so a test can also // pin that a corrupted record is caught. type pbinVerifier struct { t *testing.T @@ -45,8 +44,8 @@ var ( errPBinVerifyPosition = errors.New("pbin verify: leaf sits where its key does not") ) -// recordPaths decodes the key of every live node record. A record put with no -// data is a deletion and names no node; the root cell record is keyed outside the +// recordPaths decodes the key of every live node record. A record with no data +// is a deletion and names no node; the root cell record is keyed outside the // bit-path space and is read through rootCell. func (v *pbinVerifier) recordPaths() ([]pbinBitpath, error) { paths := make([]pbinBitpath, 0, len(v.ms.cm)) @@ -93,8 +92,6 @@ func (v *pbinVerifier) rootPath() (pbinBitpath, error) { return roots[0], nil } -// rootCell decodes the record holding the root cell, the entry point the rest of -// the record set hangs off. func (v *pbinVerifier) rootCell() (pbinCell, error) { var c pbinCell data, _, err := v.ms.Branch(pbinRootKey) @@ -114,8 +111,8 @@ func (v *pbinVerifier) rootCell() (pbinCell, error) { return c, nil } -// recomputeRoot hashes the whole record set bottom up, entering at the stored -// root cell rather than guessing which record has no ancestor. +// recomputeRoot hashes the record set bottom up, entering at the stored root +// cell rather than guessing which record has no ancestor. func (v *pbinVerifier) recomputeRoot() ([]byte, error) { c, err := v.rootCell() if err != nil { @@ -218,7 +215,7 @@ func (v *pbinVerifier) recordAt(nodePath *pbinBitpath) ([2]pbinCell, error) { // checkPlainKeys asserts every stored leaf sits where its own key derivation puts // it: the record's path, the child bit and the cell's prefix must spell exactly // treeKey(plainKey). A slot routed into the wrong zone still builds a tree that -// hashes consistently, so position against derivation is what catches it (H8). +// hashes consistently, so position against derivation is what catches it. func (v *pbinVerifier) checkPlainKeys() (int, error) { root, err := v.rootCell() if err != nil { @@ -293,7 +290,7 @@ func pbinVerifyDerivedKey(c *pbinCell, key []byte) ([]byte, error) { // A record-resident leaf holds no plain key to re-derive from, so what is // checked is where it may sit: only a code chunk carries its own value, and // a chunk is either at the top of an account stem or in the code zone — - // never in the storage zone (guards H7). + // never in the storage zone. switch { case len(key) == pbinCodeKeyLength && key[0] == pbinCodeZone: case len(key) == pbinAccountKeyLength && key[0] == pbinAccountZone && key[pbinAccountKeyLength-1] >= pbinCodeOffset: @@ -322,9 +319,8 @@ func pbinVerifyPackBits(bits []byte) []byte { return out } -// pbinTestVerifyRecords is the check every multi-leaf corpus gets: the records -// rebuild the root the engine returned, and every leaf they hold sits at its own -// key. +// pbinTestVerifyRecords requires the records to rebuild the root the engine +// returned, with every leaf they hold sitting at its own key. func pbinTestVerifyRecords(t *testing.T, ms *MockState, root []byte, wantLeaves int) { t.Helper() v := &pbinVerifier{t: t, ms: ms} @@ -338,8 +334,8 @@ func pbinTestVerifyRecords(t *testing.T, ms *MockState, root []byte, wantLeaves require.Equal(t, wantLeaves, leaves) } -// TestPBinVerifyRootRecordIsUnique pins the shape the recompute relies on: one -// record has no ancestor, and its path is the root node's prefix. +// The recompute relies on this shape: one record has no ancestor, and its path +// is the root node's prefix. func TestPBinVerifyRootRecordIsUnique(t *testing.T) { t.Parallel() @@ -357,9 +353,8 @@ func TestPBinVerifyRootRecordIsUnique(t *testing.T) { require.Equal(t, pph.grid.root.prefix, root, "the root record's path is the root node's prefix") } -// TestPBinVerifySingleLeafRoot checks the one shape that writes no node record: -// a root that is a bare leaf is still recoverable, because the root cell record -// carries it. +// A bare-leaf root writes no node record and is still recoverable, because the +// root cell record carries it. func TestPBinVerifySingleLeafRoot(t *testing.T) { t.Parallel() @@ -376,8 +371,6 @@ func TestPBinVerifySingleLeafRoot(t *testing.T) { pbinTestVerifyRecords(t, ms, root, 1) } -// TestPBinVerifyEmptyStateHasNoRecords checks the recompute refuses to invent a -// tree out of a state nothing was ever written to. func TestPBinVerifyEmptyStateHasNoRecords(t *testing.T) { t.Parallel() @@ -386,9 +379,9 @@ func TestPBinVerifyEmptyStateHasNoRecords(t *testing.T) { require.ErrorIs(t, err, errPBinVerifyNoRecords) } -// TestPBinVerifyCatchesSwappedCells gives both checks teeth: swapping a record's -// two children moves each leaf to a position its key does not spell, which the -// plain-key check must reject and the recompute must no longer reproduce. +// Swapping a record's two children moves each leaf to a position its key does +// not spell, which the plain-key check must reject and the recompute must no +// longer reproduce. Without it both checks could be vacuous. func TestPBinVerifyCatchesSwappedCells(t *testing.T) { t.Parallel() diff --git a/execution/commitment/pbin_vs_hex_compare_test.go b/execution/commitment/pbin_vs_hex_compare_test.go index afe981dc044..fe6ce0065f4 100644 --- a/execution/commitment/pbin_vs_hex_compare_test.go +++ b/execution/commitment/pbin_vs_hex_compare_test.go @@ -38,7 +38,7 @@ func (s pbinEngineShape) depthStats() (maxD, p50, mean int) { } // pbinHexPathBits converts a HexToCompact-encoded branch key to a path length in -// key bits so the two radices are comparable: one nibble is four bits. +// key bits, so the two radices are comparable. func pbinHexPathBits(compact string) int { if len(compact) == 0 { return 0 @@ -100,9 +100,9 @@ func pbinRunBin(t *testing.T, plainKeys [][]byte, updates []Update) (pbinEngineS return s, pph.counters } -// pbinClusteredCorpus gives every contract slots that share a storage group, which -// is what EIP-8297's raw sub-index co-locates. pbinScatteredCorpus spreads slots so -// no two share a group — the mapping-style access random corpora produce. +// pbinClusteredCorpus gives every contract slots that share a storage group, +// which is what EIP-8297's raw sub-index co-locates. pbinScatteredCorpus spreads +// them so no two share a group — the mapping-style access random corpora produce. func pbinClusteredCorpus(contracts, slotsPer int) ([][]byte, []Update) { ub := NewUpdateBuilder() for c := range contracts { @@ -164,10 +164,9 @@ func TestPBinVsHexStructure(t *testing.T) { } } -// TestPBinStemCoLocation pins the storage behaviour that distinguishes -// EIP-8297: slots sharing a tree_index differ only in the last key byte, so -// they hang off one stem. Random 32-byte slots never collide in a group, so -// without a deliberate corpus this path goes untested. +// Slots sharing a tree_index differ only in the last key byte, so they hang off +// one stem. Random 32-byte slots never collide in a group, so without a +// deliberate corpus this path goes untested. func TestPBinStemCoLocation(t *testing.T) { t.Parallel() @@ -193,11 +192,9 @@ func TestPBinStemCoLocation(t *testing.T) { require.Equal(t, byte(n%256), k[pbinStorageKeyLength-1], "sub-index is the raw low byte") } - // crossing into the next group must change the second digest next := c.storageKey(addr, slotOf(512)) require.NotEqual(t, base[33:65], next[33:65], "a new tree_index must move the group digest") - // a co-located pair shares a long prefix; a cross-group pair does not sharedBits := pbinCommonPrefixBitsOfKeys(base, c.storageKey(addr, slotOf(257))) crossBits := pbinCommonPrefixBitsOfKeys(base, next) require.Greater(t, sharedBits, crossBits, diff --git a/execution/commitment/pbin_zerovalue_test.go b/execution/commitment/pbin_zerovalue_test.go index 84f66568512..b279a6d69f3 100644 --- a/execution/commitment/pbin_zerovalue_test.go +++ b/execution/commitment/pbin_zerovalue_test.go @@ -27,16 +27,11 @@ import ( "github.com/erigontech/erigon/common/length" ) -// Zero-vs-absent. The domain has one encoding for both — an absent read — while -// EIP-8297 has no removal and commits a zero value as a present leaf (the -// reference's zero_value_present vector). The engine holds the presence bit the -// domain lacks: a zeroed slot under a live leaf keeps the leaf and commits 32 -// zero bytes, an absent key with no leaf contributes nothing, and an absent -// account over a live leaf is a removal the EIP does not describe (Q1) and stays -// refused. -// -// The expected roots come from the oracle, which the reference's own root -// vectors — zero_value_present among them — pin in pbin_specroots_test.go. +// Zero-vs-absent. The domain encodes both as an absent read, while EIP-8297 has +// no removal and commits a zero value as a present leaf. The engine supplies the +// presence bit the domain lacks: a zeroed slot under a live leaf keeps the leaf +// and commits 32 zero bytes, an absent key with no leaf of its own contributes +// nothing, and an absent account over a live leaf stays refused. // TestPBinStorageDeleteKeepsLeafAsPresentZero covers the update-stream side: the // zeroed slot is touched, so its leaf is in the grid when the absent read lands. @@ -111,9 +106,6 @@ func TestPBinStorageDeleteOnUntouchedSiblingIsPresentZero(t *testing.T) { require.Equal(t, want.oracleRoot(t), root) } -// TestPBinLoadCellStateAbsentRead pins the two arms apart at the site they share: -// an absent storage read fills the leaf with 32 zero bytes, an absent account -// read refuses. func TestPBinLoadCellStateAbsentRead(t *testing.T) { t.Parallel() @@ -146,9 +138,10 @@ func TestPBinLoadCellStateAbsentRead(t *testing.T) { }) } -// TestPBinAccountRemovalStillRefused holds Q1 open: turning an absent account -// into a zero-valued BASIC_DATA leaf is consistent with eip:345-347 but is not -// verified against the reference, and it would silently change the root. +// TestPBinAccountRemovalStillRefused keeps the refusal in place: turning an +// absent account into a zero-valued BASIC_DATA leaf would be consistent with +// eip:345-347, but it is not verified against the reference and would silently +// change the root. func TestPBinAccountRemovalStillRefused(t *testing.T) { t.Parallel() @@ -166,11 +159,11 @@ func TestPBinAccountRemovalStillRefused(t *testing.T) { require.ErrorIs(t, err, errPBinDeleteUnsupported) } -// TestPBinFoldDeleteUnreachableFromProcess guards H12: foldDelete collapses -// nodes the reference leaves in place, and nothing on the Process path may -// reach it. Its only observable is the zero-length record it writes at a -// bit-path key — storeRoot is the sole other zero-length write, and only at the -// root key — so a run that zeroes every leaf it stored must produce none. +// TestPBinFoldDeleteUnreachableFromProcess pins that foldDelete stays off the +// Process path, since it collapses nodes the reference leaves in place. Its only +// observable is the zero-length record it writes at a bit-path key — storeRoot +// makes the sole other zero-length write, and only at the root key — so a run +// that zeroes every leaf it stored must produce none. func TestPBinFoldDeleteUnreachableFromProcess(t *testing.T) { t.Parallel() @@ -191,8 +184,8 @@ func TestPBinFoldDeleteUnreachableFromProcess(t *testing.T) { zeroed.storage(addr, pbinOracleSlot(slot)) want.storage(addr, pbinOracleSlot(slot)) } - // An absent key with no leaf of its own is the case a zero write must not be - // confused with: it contributes nothing and leaves no empty row behind. + // An absent key with no leaf of its own contributes nothing and leaves no + // empty row behind — the case a zero write must not be confused with. zeroed.storage(addr, pbinOracleSlot(1<<20)) want.account(pbinOracleAddr(47), 1, 2, common.Hash{0x47}) diff --git a/execution/stagedsync/pbin_defer_test.go b/execution/stagedsync/pbin_defer_test.go index 6a031ed54e3..5377a2366c4 100644 --- a/execution/stagedsync/pbin_defer_test.go +++ b/execution/stagedsync/pbin_defer_test.go @@ -24,10 +24,8 @@ import ( "github.com/erigontech/erigon/execution/commitment" ) -// TestPBinDeferCommitmentUpdatesExcludesBin pins the exec3 side of the deferral -// decision. The commitment context panics on a deferral request under the bin -// variant, so ExecV3 must never make one — while every hex-family variant keeps -// deferring for fork validation and the parallel apply path. +// The commitment context panics on a deferral request under the bin variant, so +// ExecV3 must never make one. func TestPBinDeferCommitmentUpdatesExcludesBin(t *testing.T) { t.Parallel() diff --git a/execution/stagedsync/pbin_parallel_exec_test.go b/execution/stagedsync/pbin_parallel_exec_test.go index f027e43214b..ec4eba53201 100644 --- a/execution/stagedsync/pbin_parallel_exec_test.go +++ b/execution/stagedsync/pbin_parallel_exec_test.go @@ -24,10 +24,8 @@ import ( "github.com/erigontech/erigon/execution/commitment" ) -// TestPBinExecuteInParallelExcludesBin pins the executor choice. The parallel -// executor's normalized write set roots differently under the bin trie than the -// state the same block produces serially, so bin runs the serial executor -// whatever the parallel toggles say; every other variant is left alone. +// The parallel executor's normalized write set roots differently under the bin trie +// than the serial path, so bin must run serially whatever the parallel toggles say. func TestPBinExecuteInParallelExcludesBin(t *testing.T) { t.Parallel() diff --git a/execution/state/genesiswrite/pbin_genesis_test.go b/execution/state/genesiswrite/pbin_genesis_test.go index 11e366db48a..029ee5a0ea6 100644 --- a/execution/state/genesiswrite/pbin_genesis_test.go +++ b/execution/state/genesiswrite/pbin_genesis_test.go @@ -41,7 +41,6 @@ func withBinCommitment(t *testing.T, on bool) { statecfg.ExperimentalBinCommitment = on } -// Code-free alloc: code chunking into the tree is not part of this task. func pbinTestGenesis() *types.Genesis { return &types.Genesis{ Config: chain.AllProtocolChanges, @@ -52,8 +51,8 @@ func pbinTestGenesis() *types.Genesis { } } -// Genesis is the block-0 state root the executor is later checked against, so it -// must be computed on the variant the datadir uses, not always on the hex trie. +// Genesis produces the block-0 root the executor is later checked against, so it must +// use the variant the datadir uses, not always the hex trie. func TestPBinGenesisComputesBinaryRoot(t *testing.T) { // No t.Parallel: mutates process-global statecfg flags. logger := log.New() @@ -71,8 +70,7 @@ func TestPBinGenesisComputesBinaryRoot(t *testing.T) { require.Equal(t, common.BytesToHash(pbinGenesisRoot(t, g)), binBlock.Root()) } -// pbinGenesisRoot computes the genesis root over a SharedDomains explicitly -// running the bin trie, as an oracle for what GenesisToBlock must return. +// Oracle for GenesisToBlock: the same root computed through SharedDomains on bin. func pbinGenesisRoot(t *testing.T, g *types.Genesis) []byte { t.Helper() db := temporaltest.NewTestDB(t, datadir.New(t.TempDir())) diff --git a/rpc/jsonrpc/pbin_hex_only_test.go b/rpc/jsonrpc/pbin_hex_only_test.go index 39d729bfce2..fbf9ae2a3cb 100644 --- a/rpc/jsonrpc/pbin_hex_only_test.go +++ b/rpc/jsonrpc/pbin_hex_only_test.go @@ -33,8 +33,8 @@ import ( "github.com/erigontech/erigon/rpc/rpccfg" ) -// eth_getProof rebuilds proofs with the hex trie, so it must refuse a bin datadir -// instead of reading bit-path branch records as hex ones. +// Proof and simulation both recompute state with the hex trie, so they must refuse a +// bin datadir instead of reading its bit-path branch records as hex ones. func TestPBinGetProofRefusesBin(t *testing.T) { // No t.Parallel: mutates process-global statecfg flags. m, _, _ := rpcdaemontest.CreateTestExecModule(t) From 7b7ef582e3b039915491483b357333086133ffb1 Mon Sep 17 00:00:00 2001 From: awskii Date: Sat, 1 Aug 2026 22:49:31 +0700 Subject: [PATCH 47/56] execution/tests: run the EIP-8297 fixtures against the bin trie MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The reference names its fork BinaryTree — Amsterdam rules, state committed through the binary tree — so the fixtures were unrunnable: the fork registry had no such network. Selecting it needs the statecfg hash field, not just SetPBinHashSuite. The settings resolver reads that field to persist trie_hash, and re-applies what it persisted; a suite set before the datadir is resolved is otherwise overwritten by the keccak default. 67 of 70 blockchain fixtures pass. The three failures are an account deletion, which the engine refuses, and two multi-block state-root divergences. --- execution/tests/testforks/forks.go | 10 ++++++++++ execution/tests/testutil/block_test_util.go | 14 ++++++++++++++ 2 files changed, 24 insertions(+) diff --git a/execution/tests/testforks/forks.go b/execution/tests/testforks/forks.go index 8cb4780a167..c66a3173cb5 100644 --- a/execution/tests/testforks/forks.go +++ b/execution/tests/testforks/forks.go @@ -57,6 +57,11 @@ var blobSchedule = map[string]*params.BlobConfig{ } // Forks table defines supported forks and their chain config. +// BinaryTree names the experimental EIP-8297 fork. Selecting it switches the +// commitment engine process-wide, so a run covering it must not also cover a +// Merkle-Patricia fork. +const BinaryTree = "BinaryTree" + var Forks = map[string]*chain.Config{} func init() { @@ -219,6 +224,11 @@ func init() { cAms.AmsterdamTime = common.NewUint64(0) Forks["Amsterdam"] = cAms + // BinaryTree is Amsterdam with state committed through EIP-8297's binary tree + // instead of the MPT. The fork rules are identical; only the commitment engine + // differs, and the runner selects it from the network name. + Forks[BinaryTree] = configCopy(cAms) + // BPO3/BPO4 continue from BPO2 as a separate chain c = configCopy(c) c.Bpo3Time = common.NewUint64(15_000) diff --git a/execution/tests/testutil/block_test_util.go b/execution/tests/testutil/block_test_util.go index 6684b5308ba..7c75d2fa2de 100644 --- a/execution/tests/testutil/block_test_util.go +++ b/execution/tests/testutil/block_test_util.go @@ -38,7 +38,9 @@ import ( "github.com/erigontech/erigon/db/dbservices" "github.com/erigontech/erigon/db/kv" "github.com/erigontech/erigon/db/rawdb" + "github.com/erigontech/erigon/db/state/statecfg" "github.com/erigontech/erigon/execution/chain" + "github.com/erigontech/erigon/execution/commitment" "github.com/erigontech/erigon/execution/execmodule" "github.com/erigontech/erigon/execution/execmodule/execmoduletester" "github.com/erigontech/erigon/execution/rlp" @@ -226,6 +228,18 @@ func (bt *BlockTest) newTester(tb testing.TB) (*execmoduletester.ExecModuleTeste if !ok { return nil, testforks.UnsupportedForkError{Name: bt.json.Network} } + if bt.json.Network == testforks.BinaryTree { + // The commitment variant and its hash are datadir properties resolved + // process-wide, not per-tester options, so they are set here rather than + // passed through mOpts. Setting the statecfg field is what makes the + // settings resolver persist blake3 and re-apply it; calling + // SetPBinHashSuite alone would be undone by the resolver's keccak default. + statecfg.ExperimentalBinCommitment = true + statecfg.BinCommitmentHash = commitment.PBinHashBlake3 + if err := commitment.SetPBinHashSuite(commitment.PBinHashBlake3); err != nil { + return nil, err + } + } engine := rulesconfig.CreateRulesEngineBareBones(context.Background(), config, log.New()) mOpts := []execmoduletester.Option{ execmoduletester.WithGenesisSpec(bt.genesis(config)), From ff808c2334aa65be3cc7c7ec29579e6063e5017b Mon Sep 17 00:00:00 2001 From: awskii Date: Sat, 1 Aug 2026 23:04:57 +0700 Subject: [PATCH 48/56] execution/commitment: trim the pbin test surface The EEST fixtures on execution-specs projects/binary-trie are the external conformance check now, so the in-repo vectors only need enough sequence coverage to keep the oracle honest: 20 ops per seed rather than 120, stored minified. pbin_vs_hex_compare_test.go asserted only that two engines disagree on a root, which is true by construction; the rest was measurement that has served its purpose. The M1b smoke record was a working document. --- docs/pbin-m1b-smoke.md | 105 - .../commitment/pbin_vs_hex_compare_test.go | 213 - .../commitment/testdata/eip8297_vectors.json | 4730 +---------------- 3 files changed, 1 insertion(+), 5047 deletions(-) delete mode 100644 docs/pbin-m1b-smoke.md delete mode 100644 execution/commitment/pbin_vs_hex_compare_test.go diff --git a/docs/pbin-m1b-smoke.md b/docs/pbin-m1b-smoke.md deleted file mode 100644 index 8a6eb572ca4..00000000000 --- a/docs/pbin-m1b-smoke.md +++ /dev/null @@ -1,105 +0,0 @@ -# PBin M1b smoke run — `--chain=dev` on the binary commitment trie - -Record of the M1b gate: a local dev chain booted from genesis on the EIP-8297 binary -trie, produced blocks, deployed and called contracts, and resumed after a restart. - -Binary: `awskii/pbin-patricia`, erigon `v3.7.0-dev`, darwin/arm64. -Hash: Keccak-256, the default. Roots below agree with no other client; reproducing -them elsewhere needs the same flags. `--experimental.bin-commitment.hash=blake3` -selects the hash the execution-specs reference and the other binary-trie clients use. - -## Command line - -```bash -make erigon -./build/bin/erigon \ - --chain=dev \ - --datadir=/tmp/pbin-m1b/gate \ - --experimental.bin-commitment \ - --beacon.api=beacon,validator,node,config \ - --dev.slot-time=2 \ - --http.api=eth,erigon,web3,net,debug,trace,txpool \ - --http.port=8545 --beacon.api.port=5555 --private.api.addr=127.0.0.1:9090 -``` - -The header state-root check is on (its default), so every block below had its executed -root compared against the header the builder produced. - -Restart uses the same line **without** `--experimental.bin-commitment`: the variant is -persisted in `snapshots/erigondb.toml` (`trie_variant = 'bin'`) and re-adopted, logged as -`datadir uses the bin commitment trie; enabling it for this process`. - -## Genesis - -| | root | block hash | -|---|---|---| -| bin | `0xa314dd2e35d820afa60105d356faeae5beb379796fe3bf691a39df6e7bc9a331` | `0xa6d15d434deb7f19f5ac9655b7bf4918c056ecaacc25769bf5f6b3c242a9f538` | -| hex | `0xeed1da9777066ae75039e23f5d0ccc4ae5efae81b9314afcc87af0e714179b4c` | `0x3aa9a433bdbbf19493a237861e62e6c4a66ad676da6d1978dd8039228f64e2c0` | - -The dev beacon takes `Eth1Data` from the EL genesis hash, accepted it, and produced from -slot 1 on. The alloc's deposit contract is 6358 bytes = 206 code chunks (128 header + 78 -CODE_ZONE overflow), so block 0 already exercises Task 13. - -## Blocks, contracts - -Deployed from the dev signer `0x78eF752367584ee389aCB8824Ceec734456402b6` -(key = `sha256("signer:devnet")`). - -- **A** `0x55d8f9693a57f932cde89739f93d4a271d56a156` — init `0x600680600b6000396000f3600035600055`, - runtime stores calldata word 0 into slot 0. -- **B** `0x02dcc6fdd01d75a5bda67e4e7c074cfddc204111` — 4983-byte runtime (151 × `PUSH32`), - 161 chunks, so 33 land in CODE_ZONE overflow at runtime rather than at genesis. - -| block | event | root | -|---|---|---| -| 0 | genesis | `0xa314dd2e35d820afa60105d356faeae5beb379796fe3bf691a39df6e7bc9a331` | -| 15 | deploy A | `0x4cf9eb8a276c1dc5f7debf3d70f50228de7d5b28bf21e8d3b34d88df47426aef` | -| 16 | call A, slot 0 := `0x2a` | `0xa399537a22b085b5df15ffb5fc855870d67225817e0d18c74583db1009b2182a` | -| 17 | deploy B | `0xd513691489314ab5c754b18e3e51db092918725decbc87d475d1151174a3f773` | - -`eth_getStorageAt(A, 0)` = `0x…2a`, `eth_getCode(B)` = 4983 bytes. - -## Restart - -Stopped at head 21 (SIGTERM), restarted flagless on the same datadir. Roots at blocks -0/15/16/17 identical, head preserved, zero `Wrong trie root`. - -A longer run reached head 241 and repeated the restart over a datadir that had already -collated and merged state files (`v2.2-commitment.0-4.kv`, `4-6`, `6-7`): roots at blocks -0/15/16/17/100/200 identical across the restart. Before it, an earlier run resumed and -produced ~90 further blocks (79 → 171) with the root check on. - -Block *production* does not always resume after a restart: Caplin's forward sync stalls -("could not find sync committee for epoch"). Reproduced identically on hex, so it is a -dev-mode CL limitation, not a trie one. The EL side always resumed. - -## Collation and merge - -With `step_size = 64` and `MAX_REORG_DEPTH=8` (defaults never freeze on a chain this -short) the chain built and merged pbin commitment files while running, with no root -mismatch. - -## `integration commitment rebuild` - -Runs to completion under bin: adopts the persisted variant, rebuilds all three shards from -the pbin state files (497 / 256 / 160 keys, blocks 126 / 190 / 222). - -It does **not** confirm the chain's roots here. The per-shard roots the tool prints are -partial — each shard folds only its own key range — and the documented follow-up -(`integration stage_exec --reset`) cannot run on this datadir at all: `readGenesis` has no -`dev` entry and panics with `unknown chain spec with name dev`. Without it, DB remnants -past the rebuilt range make the first post-rebuild block report a wrong root — **on hex -exactly as on bin**, so the check as available is not variant-discriminating. - -The real forward-run-vs-rebuild oracle for pbin is the M1a gate -(`execution/commitment/backtester/pbin_m1a_test.go`), which does that comparison over a -real MDBX datadir with real `.kv` files. - -## Limitations hit during the run - -- **Parallel execution is off under bin.** The parallel executor's normalized write set - produces a different bin root than the same block executed serially (block 0: - `e557bca8…` vs the genesis root `a314dd2e…`; hex agrees on both executors). Rather than - leave a wrong-root path reachable, `executeInParallel` keeps bin on the serial executor. - Unresolved — the divergence itself still needs a root cause. -- Dev-mode CL cannot reliably resume block production after a restart (above). diff --git a/execution/commitment/pbin_vs_hex_compare_test.go b/execution/commitment/pbin_vs_hex_compare_test.go deleted file mode 100644 index fe6ce0065f4..00000000000 --- a/execution/commitment/pbin_vs_hex_compare_test.go +++ /dev/null @@ -1,213 +0,0 @@ -package commitment - -import ( - "context" - "fmt" - "math/bits" - "sort" - "testing" - - "github.com/stretchr/testify/require" - - "github.com/erigontech/erigon/common/length" -) - -// Structural comparison of the hex and binary commitment engines over one -// corpus. Roots differ by construction — the trees, keys and node preimages all -// differ — so this measures shape and footprint, not equality. - -type pbinEngineShape struct { - name string - root []byte - records int - recordByte int - depthBits []int // path length to each stored branch, in key bits -} - -func (s pbinEngineShape) depthStats() (maxD, p50, mean int) { - if len(s.depthBits) == 0 { - return 0, 0, 0 - } - d := append([]int(nil), s.depthBits...) - sort.Ints(d) - sum := 0 - for _, v := range d { - sum += v - } - return d[len(d)-1], d[len(d)/2], sum / len(d) -} - -// pbinHexPathBits converts a HexToCompact-encoded branch key to a path length in -// key bits, so the two radices are comparable. -func pbinHexPathBits(compact string) int { - if len(compact) == 0 { - return 0 - } - nibbles := (len(compact)-1)*2 + 1 - if compact[0]&0x10 == 0 { - nibbles-- - } - return nibbles * 4 -} - -func pbinPathBits(key string) int { - p, err := pbinDecodeBitPath([]byte(key)) - if err != nil { - return -1 - } - return int(p.bitLen) -} - -func pbinRunHex(t *testing.T, plainKeys [][]byte, updates []Update) pbinEngineShape { - t.Helper() - ms := NewMockState(t) - // PBin derives its zone from the plain-key length, so a comparison corpus - // must use real 20-byte addresses; the hex engine has to be told the same. - hph := NewHexPatriciaHashed(length.Addr, ms, DefaultTrieConfig()) - upds := WrapKeyUpdates(t, ModeDirect, KeyToHexNibbleHash, plainKeys, updates) - defer upds.Close() - require.NoError(t, ms.applyPlainUpdates(plainKeys, updates)) - - root, err := hph.Process(context.Background(), upds, "", nil, WarmupConfig{}) - require.NoError(t, err) - - s := pbinEngineShape{name: "hex", root: root} - for k, v := range ms.cm { - s.records++ - s.recordByte += len(v) - s.depthBits = append(s.depthBits, pbinHexPathBits(k)) - } - return s -} - -func pbinRunBin(t *testing.T, plainKeys [][]byte, updates []Update) (pbinEngineShape, pbinCounters) { - t.Helper() - ms := NewMockState(t) - pph := NewPBinPatriciaHashed(ms) - upds := WrapKeyUpdates(t, ModeDirect, pbinKeyHasher(), plainKeys, updates) - defer upds.Close() - require.NoError(t, ms.applyPlainUpdates(plainKeys, updates)) - - root, err := pph.Process(context.Background(), upds, "", nil, WarmupConfig{}) - require.NoError(t, err) - - s := pbinEngineShape{name: "bin", root: root} - for k, v := range ms.cm { - s.records++ - s.recordByte += len(v) - s.depthBits = append(s.depthBits, pbinPathBits(k)) - } - return s, pph.counters -} - -// pbinClusteredCorpus gives every contract slots that share a storage group, -// which is what EIP-8297's raw sub-index co-locates. pbinScatteredCorpus spreads -// them so no two share a group — the mapping-style access random corpora produce. -func pbinClusteredCorpus(contracts, slotsPer int) ([][]byte, []Update) { - ub := NewUpdateBuilder() - for c := range contracts { - addr := fmt.Sprintf("%040x", c+1) - ub.Balance(addr, uint64(c+1)) - for s := range slotsPer { - ub.Storage(addr, fmt.Sprintf("%064x", 0x100+s), fmt.Sprintf("%064x", s+1)) - } - } - return ub.Build() -} - -func pbinScatteredCorpus(contracts, slotsPer int) ([][]byte, []Update) { - ub := NewUpdateBuilder() - for c := range contracts { - addr := fmt.Sprintf("%040x", c+1) - ub.Balance(addr, uint64(c+1)) - for s := range slotsPer { - // one slot per group: step by STEM_SUBTREE_WIDTH - ub.Storage(addr, fmt.Sprintf("%064x", (s+1)*256), fmt.Sprintf("%064x", s+1)) - } - } - return ub.Build() -} - -func TestPBinVsHexStructure(t *testing.T) { - t.Parallel() - - for _, tc := range []struct { - name string - build func(int, int) ([][]byte, []Update) - }{ - {"clustered", pbinClusteredCorpus}, - {"scattered", pbinScatteredCorpus}, - } { - t.Run(tc.name, func(t *testing.T) { - plainKeys, updates := tc.build(16, 16) - - hex := pbinRunHex(t, plainKeys, updates) - bin, counters := pbinRunBin(t, plainKeys, updates) - - require.NotEqual(t, hex.root, bin.root, - "hex and binary trees must not agree on a root; equality means one of them is not the tree it claims to be") - - hMax, hP50, hMean := hex.depthStats() - bMax, bP50, bMean := bin.depthStats() - - t.Logf("corpus=%s accounts=%d storage=%d", tc.name, 16, 16*16) - t.Logf(" %-4s records=%4d bytes=%7d depthBits max=%3d p50=%3d mean=%3d", - hex.name, hex.records, hex.recordByte, hMax, hP50, hMean) - t.Logf(" %-4s records=%4d bytes=%7d depthBits max=%3d p50=%3d mean=%3d", - bin.name, bin.records, bin.recordByte, bMax, bP50, bMean) - t.Logf(" bin/hex records=%.2fx bytes=%.2fx", - float64(bin.records)/float64(hex.records), - float64(bin.recordByte)/float64(hex.recordByte)) - t.Logf(" bin splitsInsidePrefix=%d materializeReads=%d", - counters.splitsInsidePrefix, counters.materializeReads) - }) - } -} - -// Slots sharing a tree_index differ only in the last key byte, so they hang off -// one stem. Random 32-byte slots never collide in a group, so without a -// deliberate corpus this path goes untested. -func TestPBinStemCoLocation(t *testing.T) { - t.Parallel() - - addr := make([]byte, 20) - addr[19] = 0xAB - - slotOf := func(n uint64) []byte { - s := make([]byte, 32) - s[31] = byte(n) - s[30] = byte(n >> 8) - return s - } - - var c pbinDigestCache - // slots 256..511 share tree_index 1 and differ only in sub_index. - base := c.storageKey(addr, slotOf(256)) - require.Len(t, base, pbinStorageKeyLength) - - for _, n := range []uint64{257, 300, 511} { - k := c.storageKey(addr, slotOf(n)) - require.Equal(t, base[:pbinStorageKeyLength-1], k[:pbinStorageKeyLength-1], - "slots in one group must share every byte but the sub-index") - require.Equal(t, byte(n%256), k[pbinStorageKeyLength-1], "sub-index is the raw low byte") - } - - next := c.storageKey(addr, slotOf(512)) - require.NotEqual(t, base[33:65], next[33:65], "a new tree_index must move the group digest") - - sharedBits := pbinCommonPrefixBitsOfKeys(base, c.storageKey(addr, slotOf(257))) - crossBits := pbinCommonPrefixBitsOfKeys(base, next) - require.Greater(t, sharedBits, crossBits, - "co-located slots must share a longer key prefix than cross-group slots") - t.Logf("co-located slots share %d bits; cross-group share %d bits", sharedBits, crossBits) -} - -func pbinCommonPrefixBitsOfKeys(a, b []byte) int { - n := min(len(a), len(b)) - for i := range n { - if a[i] != b[i] { - return i*8 + bits.LeadingZeros8(a[i]^b[i]) - } - } - return n * 8 -} diff --git a/execution/commitment/testdata/eip8297_vectors.json b/execution/commitment/testdata/eip8297_vectors.json index 93f68d1c7db..aed6098cff0 100644 --- a/execution/commitment/testdata/eip8297_vectors.json +++ b/execution/commitment/testdata/eip8297_vectors.json @@ -1,4729 +1 @@ -{ - "meta": { - "source": "execution-specs@ec412acfd (branch eip-8297-tests)", - "hasher": "blake3", - "generator": "export_vectors.py" - }, - "empty_root": "0x0000000000000000000000000000000000000000000000000000000000000000", - "trie_vectors": [ - { - "name": "empty", - "entries": [], - "root": "0x0000000000000000000000000000000000000000000000000000000000000000" - }, - { - "name": "single_account_leaf", - "entries": [ - { - "key": "0x0055bbfb5b05f80543bd662a7b7a74980a2992612798087fa24e1fc86b8e8508e400", - "value": "0x0000000000000000000000000000000000000000000000000000000000000007" - } - ], - "root": "0x22da077ee89a0e6e6259303169fb6c7a3133fafa3033515a355af9c4f9ed5ea0" - }, - { - "name": "one_header_stem_two_leaves", - "entries": [ - { - "key": "0x0055bbfb5b05f80543bd662a7b7a74980a2992612798087fa24e1fc86b8e8508e400", - "value": "0x0000000000000000000000000000000000000000000000000000000000000007" - }, - { - "key": "0x0055bbfb5b05f80543bd662a7b7a74980a2992612798087fa24e1fc86b8e8508e401", - "value": "0x0000000000000000000000000000000000000000000000000000000000000009" - } - ], - "root": "0x3599d1dd23fef634f3845fd935375be8a42169ecc90906632aa8afa2fa380812" - }, - { - "name": "two_accounts", - "entries": [ - { - "key": "0x0055bbfb5b05f80543bd662a7b7a74980a2992612798087fa24e1fc86b8e8508e400", - "value": "0x0000000000000000000000000000000000000000000000000000000000000007" - }, - { - "key": "0x0034bb4f3468340d5f72275da92420aed94db48bbc61823e92f29f14f51590373a00", - "value": "0x0000000000000000000000000000000000000000000000000000000000000008" - } - ], - "root": "0xbcca03773c89779e136f983eba516339660c732d5b0a1ccfc2c725dca29eefbc" - }, - { - "name": "cross_zone_small", - "entries": [ - { - "key": "0x0055bbfb5b05f80543bd662a7b7a74980a2992612798087fa24e1fc86b8e8508e400", - "value": "0x0000000000000000000000000000000000000000000000000000000000000001" - }, - { - "key": "0x0055bbfb5b05f80543bd662a7b7a74980a2992612798087fa24e1fc86b8e8508e401", - "value": "0x0000000000000000000000000000000000000000000000000000000000000002" - }, - { - "key": "0xff55bbfb5b05f80543bd662a7b7a74980a2992612798087fa24e1fc86b8e8508e406f5e5117ba26652e3cbef5ea24cc46f42709eb47be3c46f3a923b68a67f44c264", - "value": "0x0000000000000000000000000000000000000000000000000000000000000003" - }, - { - "key": "0xff55bbfb5b05f80543bd662a7b7a74980a2992612798087fa24e1fc86b8e8508e4f1ce240e3efd0b0855a335ac6b58ea33be1b2afb193608d6d21fd91308dd74bc64", - "value": "0x0000000000000000000000000000000000000000000000000000000000000004" - }, - { - "key": "0x0034bb4f3468340d5f72275da92420aed94db48bbc61823e92f29f14f51590373a00", - "value": "0x0000000000000000000000000000000000000000000000000000000000000005" - } - ], - "root": "0x3fc0f35560e81b2b1bc349decef42eb48a614d8497646e401e7b85abc0b16a30" - }, - { - "name": "zero_value_present", - "entries": [ - { - "key": "0x00a43fb2d7beb12b34786a837467f62b7d6c5dab4ab7eecc89c0b67db76a68292700", - "value": "0x0000000000000000000000000000000000000000000000000000000000000000" - } - ], - "root": "0x343a84978f71225f27f6dbdd2e0dd603a2ae3b83028a907ae0f8f4db262c9d13" - }, - { - "name": "full_header_stem", - "entries": [ - { - "key": "0x004c3a8541d9ec369a9fd4bcdad885cc3b166da70ed6a83066924727d9ff147fce00", - "value": "0x0000000000000000000000000000000000000000000000000000000000000001" - }, - { - "key": "0x004c3a8541d9ec369a9fd4bcdad885cc3b166da70ed6a83066924727d9ff147fce01", - "value": "0x0000000000000000000000000000000000000000000000000000000000000002" - }, - { - "key": "0x004c3a8541d9ec369a9fd4bcdad885cc3b166da70ed6a83066924727d9ff147fce02", - "value": "0x0000000000000000000000000000000000000000000000000000000000000003" - }, - { - "key": "0x004c3a8541d9ec369a9fd4bcdad885cc3b166da70ed6a83066924727d9ff147fce03", - "value": "0x0000000000000000000000000000000000000000000000000000000000000004" - }, - { - "key": "0x004c3a8541d9ec369a9fd4bcdad885cc3b166da70ed6a83066924727d9ff147fce04", - "value": "0x0000000000000000000000000000000000000000000000000000000000000005" - }, - { - "key": "0x004c3a8541d9ec369a9fd4bcdad885cc3b166da70ed6a83066924727d9ff147fce05", - "value": "0x0000000000000000000000000000000000000000000000000000000000000006" - }, - { - "key": "0x004c3a8541d9ec369a9fd4bcdad885cc3b166da70ed6a83066924727d9ff147fce06", - "value": "0x0000000000000000000000000000000000000000000000000000000000000007" - }, - { - "key": "0x004c3a8541d9ec369a9fd4bcdad885cc3b166da70ed6a83066924727d9ff147fce07", - "value": "0x0000000000000000000000000000000000000000000000000000000000000008" - }, - { - "key": "0x004c3a8541d9ec369a9fd4bcdad885cc3b166da70ed6a83066924727d9ff147fce08", - "value": "0x0000000000000000000000000000000000000000000000000000000000000009" - }, - { - "key": "0x004c3a8541d9ec369a9fd4bcdad885cc3b166da70ed6a83066924727d9ff147fce09", - "value": "0x000000000000000000000000000000000000000000000000000000000000000a" - }, - { - "key": "0x004c3a8541d9ec369a9fd4bcdad885cc3b166da70ed6a83066924727d9ff147fce0a", - "value": "0x000000000000000000000000000000000000000000000000000000000000000b" - }, - { - "key": "0x004c3a8541d9ec369a9fd4bcdad885cc3b166da70ed6a83066924727d9ff147fce0b", - "value": "0x000000000000000000000000000000000000000000000000000000000000000c" - }, - { - "key": "0x004c3a8541d9ec369a9fd4bcdad885cc3b166da70ed6a83066924727d9ff147fce0c", - "value": "0x000000000000000000000000000000000000000000000000000000000000000d" - }, - { - "key": "0x004c3a8541d9ec369a9fd4bcdad885cc3b166da70ed6a83066924727d9ff147fce0d", - "value": "0x000000000000000000000000000000000000000000000000000000000000000e" - }, - { - "key": "0x004c3a8541d9ec369a9fd4bcdad885cc3b166da70ed6a83066924727d9ff147fce0e", - "value": "0x000000000000000000000000000000000000000000000000000000000000000f" - }, - { - "key": "0x004c3a8541d9ec369a9fd4bcdad885cc3b166da70ed6a83066924727d9ff147fce0f", - "value": "0x0000000000000000000000000000000000000000000000000000000000000010" - }, - { - "key": "0x004c3a8541d9ec369a9fd4bcdad885cc3b166da70ed6a83066924727d9ff147fce10", - "value": "0x0000000000000000000000000000000000000000000000000000000000000011" - }, - { - "key": "0x004c3a8541d9ec369a9fd4bcdad885cc3b166da70ed6a83066924727d9ff147fce11", - "value": "0x0000000000000000000000000000000000000000000000000000000000000012" - }, - { - "key": "0x004c3a8541d9ec369a9fd4bcdad885cc3b166da70ed6a83066924727d9ff147fce12", - "value": "0x0000000000000000000000000000000000000000000000000000000000000013" - }, - { - "key": "0x004c3a8541d9ec369a9fd4bcdad885cc3b166da70ed6a83066924727d9ff147fce13", - "value": "0x0000000000000000000000000000000000000000000000000000000000000014" - }, - { - "key": "0x004c3a8541d9ec369a9fd4bcdad885cc3b166da70ed6a83066924727d9ff147fce14", - "value": "0x0000000000000000000000000000000000000000000000000000000000000015" - }, - { - "key": "0x004c3a8541d9ec369a9fd4bcdad885cc3b166da70ed6a83066924727d9ff147fce15", - "value": "0x0000000000000000000000000000000000000000000000000000000000000016" - }, - { - "key": "0x004c3a8541d9ec369a9fd4bcdad885cc3b166da70ed6a83066924727d9ff147fce16", - "value": "0x0000000000000000000000000000000000000000000000000000000000000017" - }, - { - "key": "0x004c3a8541d9ec369a9fd4bcdad885cc3b166da70ed6a83066924727d9ff147fce17", - "value": "0x0000000000000000000000000000000000000000000000000000000000000018" - }, - { - "key": "0x004c3a8541d9ec369a9fd4bcdad885cc3b166da70ed6a83066924727d9ff147fce18", - "value": "0x0000000000000000000000000000000000000000000000000000000000000019" - }, - { - "key": "0x004c3a8541d9ec369a9fd4bcdad885cc3b166da70ed6a83066924727d9ff147fce19", - "value": "0x000000000000000000000000000000000000000000000000000000000000001a" - }, - { - "key": "0x004c3a8541d9ec369a9fd4bcdad885cc3b166da70ed6a83066924727d9ff147fce1a", - "value": "0x000000000000000000000000000000000000000000000000000000000000001b" - }, - { - "key": "0x004c3a8541d9ec369a9fd4bcdad885cc3b166da70ed6a83066924727d9ff147fce1b", - "value": "0x000000000000000000000000000000000000000000000000000000000000001c" - }, - { - "key": "0x004c3a8541d9ec369a9fd4bcdad885cc3b166da70ed6a83066924727d9ff147fce1c", - "value": "0x000000000000000000000000000000000000000000000000000000000000001d" - }, - { - "key": "0x004c3a8541d9ec369a9fd4bcdad885cc3b166da70ed6a83066924727d9ff147fce1d", - "value": "0x000000000000000000000000000000000000000000000000000000000000001e" - }, - { - "key": "0x004c3a8541d9ec369a9fd4bcdad885cc3b166da70ed6a83066924727d9ff147fce1e", - "value": "0x000000000000000000000000000000000000000000000000000000000000001f" - }, - { - "key": "0x004c3a8541d9ec369a9fd4bcdad885cc3b166da70ed6a83066924727d9ff147fce1f", - "value": "0x0000000000000000000000000000000000000000000000000000000000000020" - }, - { - "key": "0x004c3a8541d9ec369a9fd4bcdad885cc3b166da70ed6a83066924727d9ff147fce20", - "value": "0x0000000000000000000000000000000000000000000000000000000000000021" - }, - { - "key": "0x004c3a8541d9ec369a9fd4bcdad885cc3b166da70ed6a83066924727d9ff147fce21", - "value": "0x0000000000000000000000000000000000000000000000000000000000000022" - }, - { - "key": "0x004c3a8541d9ec369a9fd4bcdad885cc3b166da70ed6a83066924727d9ff147fce22", - "value": "0x0000000000000000000000000000000000000000000000000000000000000023" - }, - { - "key": "0x004c3a8541d9ec369a9fd4bcdad885cc3b166da70ed6a83066924727d9ff147fce23", - "value": "0x0000000000000000000000000000000000000000000000000000000000000024" - }, - { - "key": "0x004c3a8541d9ec369a9fd4bcdad885cc3b166da70ed6a83066924727d9ff147fce24", - "value": "0x0000000000000000000000000000000000000000000000000000000000000025" - }, - { - "key": "0x004c3a8541d9ec369a9fd4bcdad885cc3b166da70ed6a83066924727d9ff147fce25", - "value": "0x0000000000000000000000000000000000000000000000000000000000000026" - }, - { - "key": "0x004c3a8541d9ec369a9fd4bcdad885cc3b166da70ed6a83066924727d9ff147fce26", - "value": "0x0000000000000000000000000000000000000000000000000000000000000027" - }, - { - "key": "0x004c3a8541d9ec369a9fd4bcdad885cc3b166da70ed6a83066924727d9ff147fce27", - "value": "0x0000000000000000000000000000000000000000000000000000000000000028" - }, - { - "key": "0x004c3a8541d9ec369a9fd4bcdad885cc3b166da70ed6a83066924727d9ff147fce28", - "value": "0x0000000000000000000000000000000000000000000000000000000000000029" - }, - { - "key": "0x004c3a8541d9ec369a9fd4bcdad885cc3b166da70ed6a83066924727d9ff147fce29", - "value": "0x000000000000000000000000000000000000000000000000000000000000002a" - }, - { - "key": "0x004c3a8541d9ec369a9fd4bcdad885cc3b166da70ed6a83066924727d9ff147fce2a", - "value": "0x000000000000000000000000000000000000000000000000000000000000002b" - }, - { - "key": "0x004c3a8541d9ec369a9fd4bcdad885cc3b166da70ed6a83066924727d9ff147fce2b", - "value": "0x000000000000000000000000000000000000000000000000000000000000002c" - }, - { - "key": "0x004c3a8541d9ec369a9fd4bcdad885cc3b166da70ed6a83066924727d9ff147fce2c", - "value": "0x000000000000000000000000000000000000000000000000000000000000002d" - }, - { - "key": "0x004c3a8541d9ec369a9fd4bcdad885cc3b166da70ed6a83066924727d9ff147fce2d", - "value": "0x000000000000000000000000000000000000000000000000000000000000002e" - }, - { - "key": "0x004c3a8541d9ec369a9fd4bcdad885cc3b166da70ed6a83066924727d9ff147fce2e", - "value": "0x000000000000000000000000000000000000000000000000000000000000002f" - }, - { - "key": "0x004c3a8541d9ec369a9fd4bcdad885cc3b166da70ed6a83066924727d9ff147fce2f", - "value": "0x0000000000000000000000000000000000000000000000000000000000000030" - }, - { - "key": "0x004c3a8541d9ec369a9fd4bcdad885cc3b166da70ed6a83066924727d9ff147fce30", - "value": "0x0000000000000000000000000000000000000000000000000000000000000031" - }, - { - "key": "0x004c3a8541d9ec369a9fd4bcdad885cc3b166da70ed6a83066924727d9ff147fce31", - "value": "0x0000000000000000000000000000000000000000000000000000000000000032" - }, - { - "key": "0x004c3a8541d9ec369a9fd4bcdad885cc3b166da70ed6a83066924727d9ff147fce32", - "value": "0x0000000000000000000000000000000000000000000000000000000000000033" - }, - { - "key": "0x004c3a8541d9ec369a9fd4bcdad885cc3b166da70ed6a83066924727d9ff147fce33", - "value": "0x0000000000000000000000000000000000000000000000000000000000000034" - }, - { - "key": "0x004c3a8541d9ec369a9fd4bcdad885cc3b166da70ed6a83066924727d9ff147fce34", - "value": "0x0000000000000000000000000000000000000000000000000000000000000035" - }, - { - "key": "0x004c3a8541d9ec369a9fd4bcdad885cc3b166da70ed6a83066924727d9ff147fce35", - "value": "0x0000000000000000000000000000000000000000000000000000000000000036" - }, - { - "key": "0x004c3a8541d9ec369a9fd4bcdad885cc3b166da70ed6a83066924727d9ff147fce36", - "value": "0x0000000000000000000000000000000000000000000000000000000000000037" - }, - { - "key": "0x004c3a8541d9ec369a9fd4bcdad885cc3b166da70ed6a83066924727d9ff147fce37", - "value": "0x0000000000000000000000000000000000000000000000000000000000000038" - }, - { - "key": "0x004c3a8541d9ec369a9fd4bcdad885cc3b166da70ed6a83066924727d9ff147fce38", - "value": "0x0000000000000000000000000000000000000000000000000000000000000039" - }, - { - "key": "0x004c3a8541d9ec369a9fd4bcdad885cc3b166da70ed6a83066924727d9ff147fce39", - "value": "0x000000000000000000000000000000000000000000000000000000000000003a" - }, - { - "key": "0x004c3a8541d9ec369a9fd4bcdad885cc3b166da70ed6a83066924727d9ff147fce3a", - "value": "0x000000000000000000000000000000000000000000000000000000000000003b" - }, - { - "key": "0x004c3a8541d9ec369a9fd4bcdad885cc3b166da70ed6a83066924727d9ff147fce3b", - "value": "0x000000000000000000000000000000000000000000000000000000000000003c" - }, - { - "key": "0x004c3a8541d9ec369a9fd4bcdad885cc3b166da70ed6a83066924727d9ff147fce3c", - "value": "0x000000000000000000000000000000000000000000000000000000000000003d" - }, - { - "key": "0x004c3a8541d9ec369a9fd4bcdad885cc3b166da70ed6a83066924727d9ff147fce3d", - "value": "0x000000000000000000000000000000000000000000000000000000000000003e" - }, - { - "key": "0x004c3a8541d9ec369a9fd4bcdad885cc3b166da70ed6a83066924727d9ff147fce3e", - "value": "0x000000000000000000000000000000000000000000000000000000000000003f" - }, - { - "key": "0x004c3a8541d9ec369a9fd4bcdad885cc3b166da70ed6a83066924727d9ff147fce3f", - "value": "0x0000000000000000000000000000000000000000000000000000000000000040" - }, - { - "key": "0x004c3a8541d9ec369a9fd4bcdad885cc3b166da70ed6a83066924727d9ff147fce40", - "value": "0x0000000000000000000000000000000000000000000000000000000000000041" - }, - { - "key": "0x004c3a8541d9ec369a9fd4bcdad885cc3b166da70ed6a83066924727d9ff147fce41", - "value": "0x0000000000000000000000000000000000000000000000000000000000000042" - }, - { - "key": "0x004c3a8541d9ec369a9fd4bcdad885cc3b166da70ed6a83066924727d9ff147fce42", - "value": "0x0000000000000000000000000000000000000000000000000000000000000043" - }, - { - "key": "0x004c3a8541d9ec369a9fd4bcdad885cc3b166da70ed6a83066924727d9ff147fce43", - "value": "0x0000000000000000000000000000000000000000000000000000000000000044" - }, - { - "key": "0x004c3a8541d9ec369a9fd4bcdad885cc3b166da70ed6a83066924727d9ff147fce44", - "value": "0x0000000000000000000000000000000000000000000000000000000000000045" - }, - { - "key": "0x004c3a8541d9ec369a9fd4bcdad885cc3b166da70ed6a83066924727d9ff147fce45", - "value": "0x0000000000000000000000000000000000000000000000000000000000000046" - }, - { - "key": "0x004c3a8541d9ec369a9fd4bcdad885cc3b166da70ed6a83066924727d9ff147fce46", - "value": "0x0000000000000000000000000000000000000000000000000000000000000047" - }, - { - "key": "0x004c3a8541d9ec369a9fd4bcdad885cc3b166da70ed6a83066924727d9ff147fce47", - "value": "0x0000000000000000000000000000000000000000000000000000000000000048" - }, - { - "key": "0x004c3a8541d9ec369a9fd4bcdad885cc3b166da70ed6a83066924727d9ff147fce48", - "value": "0x0000000000000000000000000000000000000000000000000000000000000049" - }, - { - "key": "0x004c3a8541d9ec369a9fd4bcdad885cc3b166da70ed6a83066924727d9ff147fce49", - "value": "0x000000000000000000000000000000000000000000000000000000000000004a" - }, - { - "key": "0x004c3a8541d9ec369a9fd4bcdad885cc3b166da70ed6a83066924727d9ff147fce4a", - "value": "0x000000000000000000000000000000000000000000000000000000000000004b" - }, - { - "key": "0x004c3a8541d9ec369a9fd4bcdad885cc3b166da70ed6a83066924727d9ff147fce4b", - "value": "0x000000000000000000000000000000000000000000000000000000000000004c" - }, - { - "key": "0x004c3a8541d9ec369a9fd4bcdad885cc3b166da70ed6a83066924727d9ff147fce4c", - "value": "0x000000000000000000000000000000000000000000000000000000000000004d" - }, - { - "key": "0x004c3a8541d9ec369a9fd4bcdad885cc3b166da70ed6a83066924727d9ff147fce4d", - "value": "0x000000000000000000000000000000000000000000000000000000000000004e" - }, - { - "key": "0x004c3a8541d9ec369a9fd4bcdad885cc3b166da70ed6a83066924727d9ff147fce4e", - "value": "0x000000000000000000000000000000000000000000000000000000000000004f" - }, - { - "key": "0x004c3a8541d9ec369a9fd4bcdad885cc3b166da70ed6a83066924727d9ff147fce4f", - "value": "0x0000000000000000000000000000000000000000000000000000000000000050" - }, - { - "key": "0x004c3a8541d9ec369a9fd4bcdad885cc3b166da70ed6a83066924727d9ff147fce50", - "value": "0x0000000000000000000000000000000000000000000000000000000000000051" - }, - { - "key": "0x004c3a8541d9ec369a9fd4bcdad885cc3b166da70ed6a83066924727d9ff147fce51", - "value": "0x0000000000000000000000000000000000000000000000000000000000000052" - }, - { - "key": "0x004c3a8541d9ec369a9fd4bcdad885cc3b166da70ed6a83066924727d9ff147fce52", - "value": "0x0000000000000000000000000000000000000000000000000000000000000053" - }, - { - "key": "0x004c3a8541d9ec369a9fd4bcdad885cc3b166da70ed6a83066924727d9ff147fce53", - "value": "0x0000000000000000000000000000000000000000000000000000000000000054" - }, - { - "key": "0x004c3a8541d9ec369a9fd4bcdad885cc3b166da70ed6a83066924727d9ff147fce54", - "value": "0x0000000000000000000000000000000000000000000000000000000000000055" - }, - { - "key": "0x004c3a8541d9ec369a9fd4bcdad885cc3b166da70ed6a83066924727d9ff147fce55", - "value": "0x0000000000000000000000000000000000000000000000000000000000000056" - }, - { - "key": "0x004c3a8541d9ec369a9fd4bcdad885cc3b166da70ed6a83066924727d9ff147fce56", - "value": "0x0000000000000000000000000000000000000000000000000000000000000057" - }, - { - "key": "0x004c3a8541d9ec369a9fd4bcdad885cc3b166da70ed6a83066924727d9ff147fce57", - "value": "0x0000000000000000000000000000000000000000000000000000000000000058" - }, - { - "key": "0x004c3a8541d9ec369a9fd4bcdad885cc3b166da70ed6a83066924727d9ff147fce58", - "value": "0x0000000000000000000000000000000000000000000000000000000000000059" - }, - { - "key": "0x004c3a8541d9ec369a9fd4bcdad885cc3b166da70ed6a83066924727d9ff147fce59", - "value": "0x000000000000000000000000000000000000000000000000000000000000005a" - }, - { - "key": "0x004c3a8541d9ec369a9fd4bcdad885cc3b166da70ed6a83066924727d9ff147fce5a", - "value": "0x000000000000000000000000000000000000000000000000000000000000005b" - }, - { - "key": "0x004c3a8541d9ec369a9fd4bcdad885cc3b166da70ed6a83066924727d9ff147fce5b", - "value": "0x000000000000000000000000000000000000000000000000000000000000005c" - }, - { - "key": "0x004c3a8541d9ec369a9fd4bcdad885cc3b166da70ed6a83066924727d9ff147fce5c", - "value": "0x000000000000000000000000000000000000000000000000000000000000005d" - }, - { - "key": "0x004c3a8541d9ec369a9fd4bcdad885cc3b166da70ed6a83066924727d9ff147fce5d", - "value": "0x000000000000000000000000000000000000000000000000000000000000005e" - }, - { - "key": "0x004c3a8541d9ec369a9fd4bcdad885cc3b166da70ed6a83066924727d9ff147fce5e", - "value": "0x000000000000000000000000000000000000000000000000000000000000005f" - }, - { - "key": "0x004c3a8541d9ec369a9fd4bcdad885cc3b166da70ed6a83066924727d9ff147fce5f", - "value": "0x0000000000000000000000000000000000000000000000000000000000000060" - }, - { - "key": "0x004c3a8541d9ec369a9fd4bcdad885cc3b166da70ed6a83066924727d9ff147fce60", - "value": "0x0000000000000000000000000000000000000000000000000000000000000061" - }, - { - "key": "0x004c3a8541d9ec369a9fd4bcdad885cc3b166da70ed6a83066924727d9ff147fce61", - "value": "0x0000000000000000000000000000000000000000000000000000000000000062" - }, - { - "key": "0x004c3a8541d9ec369a9fd4bcdad885cc3b166da70ed6a83066924727d9ff147fce62", - "value": "0x0000000000000000000000000000000000000000000000000000000000000063" - }, - { - "key": "0x004c3a8541d9ec369a9fd4bcdad885cc3b166da70ed6a83066924727d9ff147fce63", - "value": "0x0000000000000000000000000000000000000000000000000000000000000064" - }, - { - "key": "0x004c3a8541d9ec369a9fd4bcdad885cc3b166da70ed6a83066924727d9ff147fce64", - "value": "0x0000000000000000000000000000000000000000000000000000000000000065" - }, - { - "key": "0x004c3a8541d9ec369a9fd4bcdad885cc3b166da70ed6a83066924727d9ff147fce65", - "value": "0x0000000000000000000000000000000000000000000000000000000000000066" - }, - { - "key": "0x004c3a8541d9ec369a9fd4bcdad885cc3b166da70ed6a83066924727d9ff147fce66", - "value": "0x0000000000000000000000000000000000000000000000000000000000000067" - }, - { - "key": "0x004c3a8541d9ec369a9fd4bcdad885cc3b166da70ed6a83066924727d9ff147fce67", - "value": "0x0000000000000000000000000000000000000000000000000000000000000068" - }, - { - "key": "0x004c3a8541d9ec369a9fd4bcdad885cc3b166da70ed6a83066924727d9ff147fce68", - "value": "0x0000000000000000000000000000000000000000000000000000000000000069" - }, - { - "key": "0x004c3a8541d9ec369a9fd4bcdad885cc3b166da70ed6a83066924727d9ff147fce69", - "value": "0x000000000000000000000000000000000000000000000000000000000000006a" - }, - { - "key": "0x004c3a8541d9ec369a9fd4bcdad885cc3b166da70ed6a83066924727d9ff147fce6a", - "value": "0x000000000000000000000000000000000000000000000000000000000000006b" - }, - { - "key": "0x004c3a8541d9ec369a9fd4bcdad885cc3b166da70ed6a83066924727d9ff147fce6b", - "value": "0x000000000000000000000000000000000000000000000000000000000000006c" - }, - { - "key": "0x004c3a8541d9ec369a9fd4bcdad885cc3b166da70ed6a83066924727d9ff147fce6c", - "value": "0x000000000000000000000000000000000000000000000000000000000000006d" - }, - { - "key": "0x004c3a8541d9ec369a9fd4bcdad885cc3b166da70ed6a83066924727d9ff147fce6d", - "value": "0x000000000000000000000000000000000000000000000000000000000000006e" - }, - { - "key": "0x004c3a8541d9ec369a9fd4bcdad885cc3b166da70ed6a83066924727d9ff147fce6e", - "value": "0x000000000000000000000000000000000000000000000000000000000000006f" - }, - { - "key": "0x004c3a8541d9ec369a9fd4bcdad885cc3b166da70ed6a83066924727d9ff147fce6f", - "value": "0x0000000000000000000000000000000000000000000000000000000000000070" - }, - { - "key": "0x004c3a8541d9ec369a9fd4bcdad885cc3b166da70ed6a83066924727d9ff147fce70", - "value": "0x0000000000000000000000000000000000000000000000000000000000000071" - }, - { - "key": "0x004c3a8541d9ec369a9fd4bcdad885cc3b166da70ed6a83066924727d9ff147fce71", - "value": "0x0000000000000000000000000000000000000000000000000000000000000072" - }, - { - "key": "0x004c3a8541d9ec369a9fd4bcdad885cc3b166da70ed6a83066924727d9ff147fce72", - "value": "0x0000000000000000000000000000000000000000000000000000000000000073" - }, - { - "key": "0x004c3a8541d9ec369a9fd4bcdad885cc3b166da70ed6a83066924727d9ff147fce73", - "value": "0x0000000000000000000000000000000000000000000000000000000000000074" - }, - { - "key": "0x004c3a8541d9ec369a9fd4bcdad885cc3b166da70ed6a83066924727d9ff147fce74", - "value": "0x0000000000000000000000000000000000000000000000000000000000000075" - }, - { - "key": "0x004c3a8541d9ec369a9fd4bcdad885cc3b166da70ed6a83066924727d9ff147fce75", - "value": "0x0000000000000000000000000000000000000000000000000000000000000076" - }, - { - "key": "0x004c3a8541d9ec369a9fd4bcdad885cc3b166da70ed6a83066924727d9ff147fce76", - "value": "0x0000000000000000000000000000000000000000000000000000000000000077" - }, - { - "key": "0x004c3a8541d9ec369a9fd4bcdad885cc3b166da70ed6a83066924727d9ff147fce77", - "value": "0x0000000000000000000000000000000000000000000000000000000000000078" - }, - { - "key": "0x004c3a8541d9ec369a9fd4bcdad885cc3b166da70ed6a83066924727d9ff147fce78", - "value": "0x0000000000000000000000000000000000000000000000000000000000000079" - }, - { - "key": "0x004c3a8541d9ec369a9fd4bcdad885cc3b166da70ed6a83066924727d9ff147fce79", - "value": "0x000000000000000000000000000000000000000000000000000000000000007a" - }, - { - "key": "0x004c3a8541d9ec369a9fd4bcdad885cc3b166da70ed6a83066924727d9ff147fce7a", - "value": "0x000000000000000000000000000000000000000000000000000000000000007b" - }, - { - "key": "0x004c3a8541d9ec369a9fd4bcdad885cc3b166da70ed6a83066924727d9ff147fce7b", - "value": "0x000000000000000000000000000000000000000000000000000000000000007c" - }, - { - "key": "0x004c3a8541d9ec369a9fd4bcdad885cc3b166da70ed6a83066924727d9ff147fce7c", - "value": "0x000000000000000000000000000000000000000000000000000000000000007d" - }, - { - "key": "0x004c3a8541d9ec369a9fd4bcdad885cc3b166da70ed6a83066924727d9ff147fce7d", - "value": "0x000000000000000000000000000000000000000000000000000000000000007e" - }, - { - "key": "0x004c3a8541d9ec369a9fd4bcdad885cc3b166da70ed6a83066924727d9ff147fce7e", - "value": "0x000000000000000000000000000000000000000000000000000000000000007f" - }, - { - "key": "0x004c3a8541d9ec369a9fd4bcdad885cc3b166da70ed6a83066924727d9ff147fce7f", - "value": "0x0000000000000000000000000000000000000000000000000000000000000080" - }, - { - "key": "0x004c3a8541d9ec369a9fd4bcdad885cc3b166da70ed6a83066924727d9ff147fce80", - "value": "0x0000000000000000000000000000000000000000000000000000000000000081" - }, - { - "key": "0x004c3a8541d9ec369a9fd4bcdad885cc3b166da70ed6a83066924727d9ff147fce81", - "value": "0x0000000000000000000000000000000000000000000000000000000000000082" - }, - { - "key": "0x004c3a8541d9ec369a9fd4bcdad885cc3b166da70ed6a83066924727d9ff147fce82", - "value": "0x0000000000000000000000000000000000000000000000000000000000000083" - }, - { - "key": "0x004c3a8541d9ec369a9fd4bcdad885cc3b166da70ed6a83066924727d9ff147fce83", - "value": "0x0000000000000000000000000000000000000000000000000000000000000084" - }, - { - "key": "0x004c3a8541d9ec369a9fd4bcdad885cc3b166da70ed6a83066924727d9ff147fce84", - "value": "0x0000000000000000000000000000000000000000000000000000000000000085" - }, - { - "key": "0x004c3a8541d9ec369a9fd4bcdad885cc3b166da70ed6a83066924727d9ff147fce85", - "value": "0x0000000000000000000000000000000000000000000000000000000000000086" - }, - { - "key": "0x004c3a8541d9ec369a9fd4bcdad885cc3b166da70ed6a83066924727d9ff147fce86", - "value": "0x0000000000000000000000000000000000000000000000000000000000000087" - }, - { - "key": "0x004c3a8541d9ec369a9fd4bcdad885cc3b166da70ed6a83066924727d9ff147fce87", - "value": "0x0000000000000000000000000000000000000000000000000000000000000088" - }, - { - "key": "0x004c3a8541d9ec369a9fd4bcdad885cc3b166da70ed6a83066924727d9ff147fce88", - "value": "0x0000000000000000000000000000000000000000000000000000000000000089" - }, - { - "key": "0x004c3a8541d9ec369a9fd4bcdad885cc3b166da70ed6a83066924727d9ff147fce89", - "value": "0x000000000000000000000000000000000000000000000000000000000000008a" - }, - { - "key": "0x004c3a8541d9ec369a9fd4bcdad885cc3b166da70ed6a83066924727d9ff147fce8a", - "value": "0x000000000000000000000000000000000000000000000000000000000000008b" - }, - { - "key": "0x004c3a8541d9ec369a9fd4bcdad885cc3b166da70ed6a83066924727d9ff147fce8b", - "value": "0x000000000000000000000000000000000000000000000000000000000000008c" - }, - { - "key": "0x004c3a8541d9ec369a9fd4bcdad885cc3b166da70ed6a83066924727d9ff147fce8c", - "value": "0x000000000000000000000000000000000000000000000000000000000000008d" - }, - { - "key": "0x004c3a8541d9ec369a9fd4bcdad885cc3b166da70ed6a83066924727d9ff147fce8d", - "value": "0x000000000000000000000000000000000000000000000000000000000000008e" - }, - { - "key": "0x004c3a8541d9ec369a9fd4bcdad885cc3b166da70ed6a83066924727d9ff147fce8e", - "value": "0x000000000000000000000000000000000000000000000000000000000000008f" - }, - { - "key": "0x004c3a8541d9ec369a9fd4bcdad885cc3b166da70ed6a83066924727d9ff147fce8f", - "value": "0x0000000000000000000000000000000000000000000000000000000000000090" - }, - { - "key": "0x004c3a8541d9ec369a9fd4bcdad885cc3b166da70ed6a83066924727d9ff147fce90", - "value": "0x0000000000000000000000000000000000000000000000000000000000000091" - }, - { - "key": "0x004c3a8541d9ec369a9fd4bcdad885cc3b166da70ed6a83066924727d9ff147fce91", - "value": "0x0000000000000000000000000000000000000000000000000000000000000092" - }, - { - "key": "0x004c3a8541d9ec369a9fd4bcdad885cc3b166da70ed6a83066924727d9ff147fce92", - "value": "0x0000000000000000000000000000000000000000000000000000000000000093" - }, - { - "key": "0x004c3a8541d9ec369a9fd4bcdad885cc3b166da70ed6a83066924727d9ff147fce93", - "value": "0x0000000000000000000000000000000000000000000000000000000000000094" - }, - { - "key": "0x004c3a8541d9ec369a9fd4bcdad885cc3b166da70ed6a83066924727d9ff147fce94", - "value": "0x0000000000000000000000000000000000000000000000000000000000000095" - }, - { - "key": "0x004c3a8541d9ec369a9fd4bcdad885cc3b166da70ed6a83066924727d9ff147fce95", - "value": "0x0000000000000000000000000000000000000000000000000000000000000096" - }, - { - "key": "0x004c3a8541d9ec369a9fd4bcdad885cc3b166da70ed6a83066924727d9ff147fce96", - "value": "0x0000000000000000000000000000000000000000000000000000000000000097" - }, - { - "key": "0x004c3a8541d9ec369a9fd4bcdad885cc3b166da70ed6a83066924727d9ff147fce97", - "value": "0x0000000000000000000000000000000000000000000000000000000000000098" - }, - { - "key": "0x004c3a8541d9ec369a9fd4bcdad885cc3b166da70ed6a83066924727d9ff147fce98", - "value": "0x0000000000000000000000000000000000000000000000000000000000000099" - }, - { - "key": "0x004c3a8541d9ec369a9fd4bcdad885cc3b166da70ed6a83066924727d9ff147fce99", - "value": "0x000000000000000000000000000000000000000000000000000000000000009a" - }, - { - "key": "0x004c3a8541d9ec369a9fd4bcdad885cc3b166da70ed6a83066924727d9ff147fce9a", - "value": "0x000000000000000000000000000000000000000000000000000000000000009b" - }, - { - "key": "0x004c3a8541d9ec369a9fd4bcdad885cc3b166da70ed6a83066924727d9ff147fce9b", - "value": "0x000000000000000000000000000000000000000000000000000000000000009c" - }, - { - "key": "0x004c3a8541d9ec369a9fd4bcdad885cc3b166da70ed6a83066924727d9ff147fce9c", - "value": "0x000000000000000000000000000000000000000000000000000000000000009d" - }, - { - "key": "0x004c3a8541d9ec369a9fd4bcdad885cc3b166da70ed6a83066924727d9ff147fce9d", - "value": "0x000000000000000000000000000000000000000000000000000000000000009e" - }, - { - "key": "0x004c3a8541d9ec369a9fd4bcdad885cc3b166da70ed6a83066924727d9ff147fce9e", - "value": "0x000000000000000000000000000000000000000000000000000000000000009f" - }, - { - "key": "0x004c3a8541d9ec369a9fd4bcdad885cc3b166da70ed6a83066924727d9ff147fce9f", - "value": "0x00000000000000000000000000000000000000000000000000000000000000a0" - }, - { - "key": "0x004c3a8541d9ec369a9fd4bcdad885cc3b166da70ed6a83066924727d9ff147fcea0", - "value": "0x00000000000000000000000000000000000000000000000000000000000000a1" - }, - { - "key": "0x004c3a8541d9ec369a9fd4bcdad885cc3b166da70ed6a83066924727d9ff147fcea1", - "value": "0x00000000000000000000000000000000000000000000000000000000000000a2" - }, - { - "key": "0x004c3a8541d9ec369a9fd4bcdad885cc3b166da70ed6a83066924727d9ff147fcea2", - "value": "0x00000000000000000000000000000000000000000000000000000000000000a3" - }, - { - "key": "0x004c3a8541d9ec369a9fd4bcdad885cc3b166da70ed6a83066924727d9ff147fcea3", - "value": "0x00000000000000000000000000000000000000000000000000000000000000a4" - }, - { - "key": "0x004c3a8541d9ec369a9fd4bcdad885cc3b166da70ed6a83066924727d9ff147fcea4", - "value": "0x00000000000000000000000000000000000000000000000000000000000000a5" - }, - { - "key": "0x004c3a8541d9ec369a9fd4bcdad885cc3b166da70ed6a83066924727d9ff147fcea5", - "value": "0x00000000000000000000000000000000000000000000000000000000000000a6" - }, - { - "key": "0x004c3a8541d9ec369a9fd4bcdad885cc3b166da70ed6a83066924727d9ff147fcea6", - "value": "0x00000000000000000000000000000000000000000000000000000000000000a7" - }, - { - "key": "0x004c3a8541d9ec369a9fd4bcdad885cc3b166da70ed6a83066924727d9ff147fcea7", - "value": "0x00000000000000000000000000000000000000000000000000000000000000a8" - }, - { - "key": "0x004c3a8541d9ec369a9fd4bcdad885cc3b166da70ed6a83066924727d9ff147fcea8", - "value": "0x00000000000000000000000000000000000000000000000000000000000000a9" - }, - { - "key": "0x004c3a8541d9ec369a9fd4bcdad885cc3b166da70ed6a83066924727d9ff147fcea9", - "value": "0x00000000000000000000000000000000000000000000000000000000000000aa" - }, - { - "key": "0x004c3a8541d9ec369a9fd4bcdad885cc3b166da70ed6a83066924727d9ff147fceaa", - "value": "0x00000000000000000000000000000000000000000000000000000000000000ab" - }, - { - "key": "0x004c3a8541d9ec369a9fd4bcdad885cc3b166da70ed6a83066924727d9ff147fceab", - "value": "0x00000000000000000000000000000000000000000000000000000000000000ac" - }, - { - "key": "0x004c3a8541d9ec369a9fd4bcdad885cc3b166da70ed6a83066924727d9ff147fceac", - "value": "0x00000000000000000000000000000000000000000000000000000000000000ad" - }, - { - "key": "0x004c3a8541d9ec369a9fd4bcdad885cc3b166da70ed6a83066924727d9ff147fcead", - "value": "0x00000000000000000000000000000000000000000000000000000000000000ae" - }, - { - "key": "0x004c3a8541d9ec369a9fd4bcdad885cc3b166da70ed6a83066924727d9ff147fceae", - "value": "0x00000000000000000000000000000000000000000000000000000000000000af" - }, - { - "key": "0x004c3a8541d9ec369a9fd4bcdad885cc3b166da70ed6a83066924727d9ff147fceaf", - "value": "0x00000000000000000000000000000000000000000000000000000000000000b0" - }, - { - "key": "0x004c3a8541d9ec369a9fd4bcdad885cc3b166da70ed6a83066924727d9ff147fceb0", - "value": "0x00000000000000000000000000000000000000000000000000000000000000b1" - }, - { - "key": "0x004c3a8541d9ec369a9fd4bcdad885cc3b166da70ed6a83066924727d9ff147fceb1", - "value": "0x00000000000000000000000000000000000000000000000000000000000000b2" - }, - { - "key": "0x004c3a8541d9ec369a9fd4bcdad885cc3b166da70ed6a83066924727d9ff147fceb2", - "value": "0x00000000000000000000000000000000000000000000000000000000000000b3" - }, - { - "key": "0x004c3a8541d9ec369a9fd4bcdad885cc3b166da70ed6a83066924727d9ff147fceb3", - "value": "0x00000000000000000000000000000000000000000000000000000000000000b4" - }, - { - "key": "0x004c3a8541d9ec369a9fd4bcdad885cc3b166da70ed6a83066924727d9ff147fceb4", - "value": "0x00000000000000000000000000000000000000000000000000000000000000b5" - }, - { - "key": "0x004c3a8541d9ec369a9fd4bcdad885cc3b166da70ed6a83066924727d9ff147fceb5", - "value": "0x00000000000000000000000000000000000000000000000000000000000000b6" - }, - { - "key": "0x004c3a8541d9ec369a9fd4bcdad885cc3b166da70ed6a83066924727d9ff147fceb6", - "value": "0x00000000000000000000000000000000000000000000000000000000000000b7" - }, - { - "key": "0x004c3a8541d9ec369a9fd4bcdad885cc3b166da70ed6a83066924727d9ff147fceb7", - "value": "0x00000000000000000000000000000000000000000000000000000000000000b8" - }, - { - "key": "0x004c3a8541d9ec369a9fd4bcdad885cc3b166da70ed6a83066924727d9ff147fceb8", - "value": "0x00000000000000000000000000000000000000000000000000000000000000b9" - }, - { - "key": "0x004c3a8541d9ec369a9fd4bcdad885cc3b166da70ed6a83066924727d9ff147fceb9", - "value": "0x00000000000000000000000000000000000000000000000000000000000000ba" - }, - { - "key": "0x004c3a8541d9ec369a9fd4bcdad885cc3b166da70ed6a83066924727d9ff147fceba", - "value": "0x00000000000000000000000000000000000000000000000000000000000000bb" - }, - { - "key": "0x004c3a8541d9ec369a9fd4bcdad885cc3b166da70ed6a83066924727d9ff147fcebb", - "value": "0x00000000000000000000000000000000000000000000000000000000000000bc" - }, - { - "key": "0x004c3a8541d9ec369a9fd4bcdad885cc3b166da70ed6a83066924727d9ff147fcebc", - "value": "0x00000000000000000000000000000000000000000000000000000000000000bd" - }, - { - "key": "0x004c3a8541d9ec369a9fd4bcdad885cc3b166da70ed6a83066924727d9ff147fcebd", - "value": "0x00000000000000000000000000000000000000000000000000000000000000be" - }, - { - "key": "0x004c3a8541d9ec369a9fd4bcdad885cc3b166da70ed6a83066924727d9ff147fcebe", - "value": "0x00000000000000000000000000000000000000000000000000000000000000bf" - }, - { - "key": "0x004c3a8541d9ec369a9fd4bcdad885cc3b166da70ed6a83066924727d9ff147fcebf", - "value": "0x00000000000000000000000000000000000000000000000000000000000000c0" - }, - { - "key": "0x004c3a8541d9ec369a9fd4bcdad885cc3b166da70ed6a83066924727d9ff147fcec0", - "value": "0x00000000000000000000000000000000000000000000000000000000000000c1" - }, - { - "key": "0x004c3a8541d9ec369a9fd4bcdad885cc3b166da70ed6a83066924727d9ff147fcec1", - "value": "0x00000000000000000000000000000000000000000000000000000000000000c2" - }, - { - "key": "0x004c3a8541d9ec369a9fd4bcdad885cc3b166da70ed6a83066924727d9ff147fcec2", - "value": "0x00000000000000000000000000000000000000000000000000000000000000c3" - }, - { - "key": "0x004c3a8541d9ec369a9fd4bcdad885cc3b166da70ed6a83066924727d9ff147fcec3", - "value": "0x00000000000000000000000000000000000000000000000000000000000000c4" - }, - { - "key": "0x004c3a8541d9ec369a9fd4bcdad885cc3b166da70ed6a83066924727d9ff147fcec4", - "value": "0x00000000000000000000000000000000000000000000000000000000000000c5" - }, - { - "key": "0x004c3a8541d9ec369a9fd4bcdad885cc3b166da70ed6a83066924727d9ff147fcec5", - "value": "0x00000000000000000000000000000000000000000000000000000000000000c6" - }, - { - "key": "0x004c3a8541d9ec369a9fd4bcdad885cc3b166da70ed6a83066924727d9ff147fcec6", - "value": "0x00000000000000000000000000000000000000000000000000000000000000c7" - }, - { - "key": "0x004c3a8541d9ec369a9fd4bcdad885cc3b166da70ed6a83066924727d9ff147fcec7", - "value": "0x00000000000000000000000000000000000000000000000000000000000000c8" - }, - { - "key": "0x004c3a8541d9ec369a9fd4bcdad885cc3b166da70ed6a83066924727d9ff147fcec8", - "value": "0x00000000000000000000000000000000000000000000000000000000000000c9" - }, - { - "key": "0x004c3a8541d9ec369a9fd4bcdad885cc3b166da70ed6a83066924727d9ff147fcec9", - "value": "0x00000000000000000000000000000000000000000000000000000000000000ca" - }, - { - "key": "0x004c3a8541d9ec369a9fd4bcdad885cc3b166da70ed6a83066924727d9ff147fceca", - "value": "0x00000000000000000000000000000000000000000000000000000000000000cb" - }, - { - "key": "0x004c3a8541d9ec369a9fd4bcdad885cc3b166da70ed6a83066924727d9ff147fcecb", - "value": "0x00000000000000000000000000000000000000000000000000000000000000cc" - }, - { - "key": "0x004c3a8541d9ec369a9fd4bcdad885cc3b166da70ed6a83066924727d9ff147fcecc", - "value": "0x00000000000000000000000000000000000000000000000000000000000000cd" - }, - { - "key": "0x004c3a8541d9ec369a9fd4bcdad885cc3b166da70ed6a83066924727d9ff147fcecd", - "value": "0x00000000000000000000000000000000000000000000000000000000000000ce" - }, - { - "key": "0x004c3a8541d9ec369a9fd4bcdad885cc3b166da70ed6a83066924727d9ff147fcece", - "value": "0x00000000000000000000000000000000000000000000000000000000000000cf" - }, - { - "key": "0x004c3a8541d9ec369a9fd4bcdad885cc3b166da70ed6a83066924727d9ff147fcecf", - "value": "0x00000000000000000000000000000000000000000000000000000000000000d0" - }, - { - "key": "0x004c3a8541d9ec369a9fd4bcdad885cc3b166da70ed6a83066924727d9ff147fced0", - "value": "0x00000000000000000000000000000000000000000000000000000000000000d1" - }, - { - "key": "0x004c3a8541d9ec369a9fd4bcdad885cc3b166da70ed6a83066924727d9ff147fced1", - "value": "0x00000000000000000000000000000000000000000000000000000000000000d2" - }, - { - "key": "0x004c3a8541d9ec369a9fd4bcdad885cc3b166da70ed6a83066924727d9ff147fced2", - "value": "0x00000000000000000000000000000000000000000000000000000000000000d3" - }, - { - "key": "0x004c3a8541d9ec369a9fd4bcdad885cc3b166da70ed6a83066924727d9ff147fced3", - "value": "0x00000000000000000000000000000000000000000000000000000000000000d4" - }, - { - "key": "0x004c3a8541d9ec369a9fd4bcdad885cc3b166da70ed6a83066924727d9ff147fced4", - "value": "0x00000000000000000000000000000000000000000000000000000000000000d5" - }, - { - "key": "0x004c3a8541d9ec369a9fd4bcdad885cc3b166da70ed6a83066924727d9ff147fced5", - "value": "0x00000000000000000000000000000000000000000000000000000000000000d6" - }, - { - "key": "0x004c3a8541d9ec369a9fd4bcdad885cc3b166da70ed6a83066924727d9ff147fced6", - "value": "0x00000000000000000000000000000000000000000000000000000000000000d7" - }, - { - "key": "0x004c3a8541d9ec369a9fd4bcdad885cc3b166da70ed6a83066924727d9ff147fced7", - "value": "0x00000000000000000000000000000000000000000000000000000000000000d8" - }, - { - "key": "0x004c3a8541d9ec369a9fd4bcdad885cc3b166da70ed6a83066924727d9ff147fced8", - "value": "0x00000000000000000000000000000000000000000000000000000000000000d9" - }, - { - "key": "0x004c3a8541d9ec369a9fd4bcdad885cc3b166da70ed6a83066924727d9ff147fced9", - "value": "0x00000000000000000000000000000000000000000000000000000000000000da" - }, - { - "key": "0x004c3a8541d9ec369a9fd4bcdad885cc3b166da70ed6a83066924727d9ff147fceda", - "value": "0x00000000000000000000000000000000000000000000000000000000000000db" - }, - { - "key": "0x004c3a8541d9ec369a9fd4bcdad885cc3b166da70ed6a83066924727d9ff147fcedb", - "value": "0x00000000000000000000000000000000000000000000000000000000000000dc" - }, - { - "key": "0x004c3a8541d9ec369a9fd4bcdad885cc3b166da70ed6a83066924727d9ff147fcedc", - "value": "0x00000000000000000000000000000000000000000000000000000000000000dd" - }, - { - "key": "0x004c3a8541d9ec369a9fd4bcdad885cc3b166da70ed6a83066924727d9ff147fcedd", - "value": "0x00000000000000000000000000000000000000000000000000000000000000de" - }, - { - "key": "0x004c3a8541d9ec369a9fd4bcdad885cc3b166da70ed6a83066924727d9ff147fcede", - "value": "0x00000000000000000000000000000000000000000000000000000000000000df" - }, - { - "key": "0x004c3a8541d9ec369a9fd4bcdad885cc3b166da70ed6a83066924727d9ff147fcedf", - "value": "0x00000000000000000000000000000000000000000000000000000000000000e0" - }, - { - "key": "0x004c3a8541d9ec369a9fd4bcdad885cc3b166da70ed6a83066924727d9ff147fcee0", - "value": "0x00000000000000000000000000000000000000000000000000000000000000e1" - }, - { - "key": "0x004c3a8541d9ec369a9fd4bcdad885cc3b166da70ed6a83066924727d9ff147fcee1", - "value": "0x00000000000000000000000000000000000000000000000000000000000000e2" - }, - { - "key": "0x004c3a8541d9ec369a9fd4bcdad885cc3b166da70ed6a83066924727d9ff147fcee2", - "value": "0x00000000000000000000000000000000000000000000000000000000000000e3" - }, - { - "key": "0x004c3a8541d9ec369a9fd4bcdad885cc3b166da70ed6a83066924727d9ff147fcee3", - "value": "0x00000000000000000000000000000000000000000000000000000000000000e4" - }, - { - "key": "0x004c3a8541d9ec369a9fd4bcdad885cc3b166da70ed6a83066924727d9ff147fcee4", - "value": "0x00000000000000000000000000000000000000000000000000000000000000e5" - }, - { - "key": "0x004c3a8541d9ec369a9fd4bcdad885cc3b166da70ed6a83066924727d9ff147fcee5", - "value": "0x00000000000000000000000000000000000000000000000000000000000000e6" - }, - { - "key": "0x004c3a8541d9ec369a9fd4bcdad885cc3b166da70ed6a83066924727d9ff147fcee6", - "value": "0x00000000000000000000000000000000000000000000000000000000000000e7" - }, - { - "key": "0x004c3a8541d9ec369a9fd4bcdad885cc3b166da70ed6a83066924727d9ff147fcee7", - "value": "0x00000000000000000000000000000000000000000000000000000000000000e8" - }, - { - "key": "0x004c3a8541d9ec369a9fd4bcdad885cc3b166da70ed6a83066924727d9ff147fcee8", - "value": "0x00000000000000000000000000000000000000000000000000000000000000e9" - }, - { - "key": "0x004c3a8541d9ec369a9fd4bcdad885cc3b166da70ed6a83066924727d9ff147fcee9", - "value": "0x00000000000000000000000000000000000000000000000000000000000000ea" - }, - { - "key": "0x004c3a8541d9ec369a9fd4bcdad885cc3b166da70ed6a83066924727d9ff147fceea", - "value": "0x00000000000000000000000000000000000000000000000000000000000000eb" - }, - { - "key": "0x004c3a8541d9ec369a9fd4bcdad885cc3b166da70ed6a83066924727d9ff147fceeb", - "value": "0x00000000000000000000000000000000000000000000000000000000000000ec" - }, - { - "key": "0x004c3a8541d9ec369a9fd4bcdad885cc3b166da70ed6a83066924727d9ff147fceec", - "value": "0x00000000000000000000000000000000000000000000000000000000000000ed" - }, - { - "key": "0x004c3a8541d9ec369a9fd4bcdad885cc3b166da70ed6a83066924727d9ff147fceed", - "value": "0x00000000000000000000000000000000000000000000000000000000000000ee" - }, - { - "key": "0x004c3a8541d9ec369a9fd4bcdad885cc3b166da70ed6a83066924727d9ff147fceee", - "value": "0x00000000000000000000000000000000000000000000000000000000000000ef" - }, - { - "key": "0x004c3a8541d9ec369a9fd4bcdad885cc3b166da70ed6a83066924727d9ff147fceef", - "value": "0x00000000000000000000000000000000000000000000000000000000000000f0" - }, - { - "key": "0x004c3a8541d9ec369a9fd4bcdad885cc3b166da70ed6a83066924727d9ff147fcef0", - "value": "0x00000000000000000000000000000000000000000000000000000000000000f1" - }, - { - "key": "0x004c3a8541d9ec369a9fd4bcdad885cc3b166da70ed6a83066924727d9ff147fcef1", - "value": "0x00000000000000000000000000000000000000000000000000000000000000f2" - }, - { - "key": "0x004c3a8541d9ec369a9fd4bcdad885cc3b166da70ed6a83066924727d9ff147fcef2", - "value": "0x00000000000000000000000000000000000000000000000000000000000000f3" - }, - { - "key": "0x004c3a8541d9ec369a9fd4bcdad885cc3b166da70ed6a83066924727d9ff147fcef3", - "value": "0x00000000000000000000000000000000000000000000000000000000000000f4" - }, - { - "key": "0x004c3a8541d9ec369a9fd4bcdad885cc3b166da70ed6a83066924727d9ff147fcef4", - "value": "0x00000000000000000000000000000000000000000000000000000000000000f5" - }, - { - "key": "0x004c3a8541d9ec369a9fd4bcdad885cc3b166da70ed6a83066924727d9ff147fcef5", - "value": "0x00000000000000000000000000000000000000000000000000000000000000f6" - }, - { - "key": "0x004c3a8541d9ec369a9fd4bcdad885cc3b166da70ed6a83066924727d9ff147fcef6", - "value": "0x00000000000000000000000000000000000000000000000000000000000000f7" - }, - { - "key": "0x004c3a8541d9ec369a9fd4bcdad885cc3b166da70ed6a83066924727d9ff147fcef7", - "value": "0x00000000000000000000000000000000000000000000000000000000000000f8" - }, - { - "key": "0x004c3a8541d9ec369a9fd4bcdad885cc3b166da70ed6a83066924727d9ff147fcef8", - "value": "0x00000000000000000000000000000000000000000000000000000000000000f9" - }, - { - "key": "0x004c3a8541d9ec369a9fd4bcdad885cc3b166da70ed6a83066924727d9ff147fcef9", - "value": "0x00000000000000000000000000000000000000000000000000000000000000fa" - }, - { - "key": "0x004c3a8541d9ec369a9fd4bcdad885cc3b166da70ed6a83066924727d9ff147fcefa", - "value": "0x00000000000000000000000000000000000000000000000000000000000000fb" - }, - { - "key": "0x004c3a8541d9ec369a9fd4bcdad885cc3b166da70ed6a83066924727d9ff147fcefb", - "value": "0x00000000000000000000000000000000000000000000000000000000000000fc" - }, - { - "key": "0x004c3a8541d9ec369a9fd4bcdad885cc3b166da70ed6a83066924727d9ff147fcefc", - "value": "0x00000000000000000000000000000000000000000000000000000000000000fd" - }, - { - "key": "0x004c3a8541d9ec369a9fd4bcdad885cc3b166da70ed6a83066924727d9ff147fcefd", - "value": "0x00000000000000000000000000000000000000000000000000000000000000fe" - }, - { - "key": "0x004c3a8541d9ec369a9fd4bcdad885cc3b166da70ed6a83066924727d9ff147fcefe", - "value": "0x00000000000000000000000000000000000000000000000000000000000000ff" - }, - { - "key": "0x004c3a8541d9ec369a9fd4bcdad885cc3b166da70ed6a83066924727d9ff147fceff", - "value": "0x0000000000000000000000000000000000000000000000000000000000000100" - } - ], - "root": "0x090ef773023b99997f3c8c72e3fa1fbd9b0b62833ffb662e457b60530b358721" - } - ], - "sequence_vectors": [ - { - "seed": 8297, - "ops": [ - { - "op": "set", - "key": "0x0015c4813bb5b1499e0a7e0d46daab8e10c70940c008c566d5c68557b1523d1f2999", - "value": "0x00000000000000000000000000000000000000000000000000000000362952bd" - }, - { - "op": "set", - "key": "0xff2eb90959edcd0b6a8a7922b8b8d37cd9664001c217b4c8e6d2df4b8ede329a7191c71427661efcc7f991940e68aae22bede52be877c5e2b9b4e0c22051e2c61706", - "value": "0x000000000000000000000000000000000000000000000000000000005912e971" - }, - { - "op": "delete", - "key": "0xff2eb90959edcd0b6a8a7922b8b8d37cd9664001c217b4c8e6d2df4b8ede329a7191c71427661efcc7f991940e68aae22bede52be877c5e2b9b4e0c22051e2c61706" - }, - { - "op": "set", - "key": "0xff9c847c439d6431d36ddeb326cb553da2e967778a24afbd1d23a62412186faf016d21858cec8b9c323663e513206217295d0693bb4eb042fd7e5e863612ec45fe5c", - "value": "0x000000000000000000000000000000000000000000000000000000009e92aea6" - }, - { - "op": "set", - "key": "0x00998f08d7054351e5fa3ccadda32ab8d6c0c86a825dd360d57c0fe6b46612297c21", - "value": "0x0000000000000000000000000000000000000000000000000000000037f3974d" - }, - { - "op": "set", - "key": "0x00e61994d31ceae39cae5feb02303eb90e0d4ae90f5b0755c3c26adc81f360ec8516", - "value": "0x0000000000000000000000000000000000000000000000000000000091546180" - }, - { - "op": "set", - "key": "0xffd1afd6427b13f2ce7beb82a2cd6bbfc58eac8b2d46bc87bfbc78fbf440e18a635bd91a1bb3fea09cd07c1f6f0b5b0928ec465c3d5409c3dc4d9f3b384fd276b0df", - "value": "0x00000000000000000000000000000000000000000000000000000000d560d2d0" - }, - { - "op": "set", - "key": "0x002eb90959edcd0b6a8a7922b8b8d37cd9664001c217b4c8e6d2df4b8ede329a717c", - "value": "0x00000000000000000000000000000000000000000000000000000000b7e649ff" - }, - { - "op": "delete", - "key": "0xff9c847c439d6431d36ddeb326cb553da2e967778a24afbd1d23a62412186faf016d21858cec8b9c323663e513206217295d0693bb4eb042fd7e5e863612ec45fe5c" - }, - { - "op": "set", - "key": "0xffe60d91d1d11837912311124089c6b73c9d726c99f4df797eb48c2b7fec6e5e2cedb6b011186812600d2c38950eb3aa1e9e39d11456e96ac38d3ec9cb37e4816783", - "value": "0x0000000000000000000000000000000000000000000000000000000015716296" - }, - { - "op": "set", - "key": "0x0078a2812688114caf4452a5611322977e4b1000e59ef408d4c8e2c90028370bcbf3", - "value": "0x00000000000000000000000000000000000000000000000000000000d566656c" - }, - { - "op": "set", - "key": "0x000861030791246737d706c0d0017de01a26278bbe4ce8a134f5e0cd6f7eb69bcf6b", - "value": "0x00000000000000000000000000000000000000000000000000000000c6f30fd3" - }, - { - "op": "set", - "key": "0x0068e867f5f899fc01d641a00cea2c42d58f3cc061797135198a85301d1e2903062a", - "value": "0x00000000000000000000000000000000000000000000000000000000308a8072" - }, - { - "op": "set", - "key": "0xff46aeb3243f905d4e0eac7f60ee8962848c87e79dfbd019ece14fe45bd720ea6bc7d37b238b059b3f39fb34f03f189fc8c596ad3bc45f7231d1a8e723510074014d", - "value": "0x00000000000000000000000000000000000000000000000000000000fd6c27f9" - }, - { - "op": "set", - "key": "0x00a43fb2d7beb12b34786a837467f62b7d6c5dab4ab7eecc89c0b67db76a682927c1", - "value": "0x000000000000000000000000000000000000000000000000000000000b5daa14" - }, - { - "op": "set", - "key": "0x00543907253acb230f5dc96bbafd8ea0f37dd63b30e8a4a3675bb007673a31fbc5d7", - "value": "0x00000000000000000000000000000000000000000000000000000000ef86c437" - }, - { - "op": "set", - "key": "0xff0fb23a930b05e3bdfbdd0e6694fa9df9c5059ceabc60618bc40931ddb379f1f64c1111b166c73d061477381d77db7818b678c5a5595df51af307fccfda674238c3", - "value": "0x000000000000000000000000000000000000000000000000000000008687ece2" - }, - { - "op": "set", - "key": "0x0046aeb3243f905d4e0eac7f60ee8962848c87e79dfbd019ece14fe45bd720ea6b69", - "value": "0x000000000000000000000000000000000000000000000000000000008d81d15d" - }, - { - "op": "set", - "key": "0xff998f08d7054351e5fa3ccadda32ab8d6c0c86a825dd360d57c0fe6b46612297c216fd3a73b07a45409a19b5e6d545779e55b6900fddc4443dcb06e3d97e25e1908", - "value": "0x000000000000000000000000000000000000000000000000000000008f91e546" - }, - { - "op": "set", - "key": "0x002cebacb183e2f51a3b6cb9f7fe2bd11c399bf1bdb8ce95ed457d3e46190197a0fd", - "value": "0x00000000000000000000000000000000000000000000000000000000b638fa76" - }, - { - "op": "set", - "key": "0xffe60d91d1d11837912311124089c6b73c9d726c99f4df797eb48c2b7fec6e5e2c3b9034b692f2597d8c9c9ad6921ecd112d2f69cc50a6e065034e2ace540e89288e", - "value": "0x0000000000000000000000000000000000000000000000000000000091e7fc09" - }, - { - "op": "set", - "key": "0x002eb90959edcd0b6a8a7922b8b8d37cd9664001c217b4c8e6d2df4b8ede329a71bf", - "value": "0x00000000000000000000000000000000000000000000000000000000cea86aa1" - }, - { - "op": "delete", - "key": "0xffd1afd6427b13f2ce7beb82a2cd6bbfc58eac8b2d46bc87bfbc78fbf440e18a635bd91a1bb3fea09cd07c1f6f0b5b0928ec465c3d5409c3dc4d9f3b384fd276b0df" - }, - { - "op": "set", - "key": "0x00a43fb2d7beb12b34786a837467f62b7d6c5dab4ab7eecc89c0b67db76a68292735", - "value": "0x0000000000000000000000000000000000000000000000000000000049742ebd" - }, - { - "op": "delete", - "key": "0x0078a2812688114caf4452a5611322977e4b1000e59ef408d4c8e2c90028370bcbf3" - }, - { - "op": "set", - "key": "0x007293ce391f5cd652216ca8ea31069a06a82f393028dfc8786aa5efd7be734b2c59", - "value": "0x0000000000000000000000000000000000000000000000000000000006c1a51d" - }, - { - "op": "delete", - "key": "0xff46aeb3243f905d4e0eac7f60ee8962848c87e79dfbd019ece14fe45bd720ea6bc7d37b238b059b3f39fb34f03f189fc8c596ad3bc45f7231d1a8e723510074014d" - }, - { - "op": "set", - "key": "0xff4b83f061efb2f4708114e46b5ef50a255528c28c80bbe5fc6ad88e892bd4c4ea37491f84eb77d352d7d8fca0484b856ceab7236e2f773d157b3fbd6d0586de7fb5", - "value": "0x0000000000000000000000000000000000000000000000000000000097b8536d" - }, - { - "op": "set", - "key": "0x000ed33feae99b046218c56bec56750a98beb5fd6a51ec13218024cb2455586b4323", - "value": "0x0000000000000000000000000000000000000000000000000000000038e606b4" - }, - { - "op": "delete", - "key": "0x0046aeb3243f905d4e0eac7f60ee8962848c87e79dfbd019ece14fe45bd720ea6b69" - }, - { - "op": "delete", - "key": "0xffe60d91d1d11837912311124089c6b73c9d726c99f4df797eb48c2b7fec6e5e2cedb6b011186812600d2c38950eb3aa1e9e39d11456e96ac38d3ec9cb37e4816783" - }, - { - "op": "set", - "key": "0xff34bb4f3468340d5f72275da92420aed94db48bbc61823e92f29f14f51590373aef2032e9c5c80ba9048f874aaea79ab7ce9e0f910b0e98955e60542e3a7f4464d1", - "value": "0x00000000000000000000000000000000000000000000000000000000417abfdc" - }, - { - "op": "set", - "key": "0x00e6b6d8d98be54d4c00cee378bedf964f755b5257bf1eabf52cf39aa987686fa7b0", - "value": "0x0000000000000000000000000000000000000000000000000000000082455405" - }, - { - "op": "set", - "key": "0xffc28c21982a56a6cecf595bfeb78297c7432abfb487cc75c09db2856a118f441d4d68c9b03da1dec117fa853416c0249fb7b863e57d51f6ac8e86e023c336f4686c", - "value": "0x0000000000000000000000000000000000000000000000000000000058e72fcf" - }, - { - "op": "set", - "key": "0xff4b83f061efb2f4708114e46b5ef50a255528c28c80bbe5fc6ad88e892bd4c4ea37491f84eb77d352d7d8fca0484b856ceab7236e2f773d157b3fbd6d0586de7f9d", - "value": "0x00000000000000000000000000000000000000000000000000000000713293e5" - }, - { - "op": "delete", - "key": "0x002eb90959edcd0b6a8a7922b8b8d37cd9664001c217b4c8e6d2df4b8ede329a71bf" - }, - { - "op": "delete", - "key": "0xff998f08d7054351e5fa3ccadda32ab8d6c0c86a825dd360d57c0fe6b46612297c216fd3a73b07a45409a19b5e6d545779e55b6900fddc4443dcb06e3d97e25e1908" - }, - { - "op": "delete", - "key": "0x00543907253acb230f5dc96bbafd8ea0f37dd63b30e8a4a3675bb007673a31fbc5d7" - }, - { - "op": "delete", - "key": "0x00e61994d31ceae39cae5feb02303eb90e0d4ae90f5b0755c3c26adc81f360ec8516" - }, - { - "op": "set", - "key": "0xff7293ce391f5cd652216ca8ea31069a06a82f393028dfc8786aa5efd7be734b2ca38663c8c6b9a008190c48cd4ee4fd87a0d65a077938f6b18ea2ebb292ca4b0a03", - "value": "0x00000000000000000000000000000000000000000000000000000000b4088d5c" - }, - { - "op": "set", - "key": "0x00c901cfbb39e551189323bea0af6cf9e81008c90dce65cbc2ff4672e263d7bb87fd", - "value": "0x0000000000000000000000000000000000000000000000000000000024c1f69c" - }, - { - "op": "set", - "key": "0xffe57860ba0adcdd1294caf74da8b990a80a8a6861bc01b30dfe07450d25bab38f63b4527cc539651483c1b056383959f91cb98fdbd3c1799a854a2781ddb32290fb", - "value": "0x00000000000000000000000000000000000000000000000000000000b1043313" - }, - { - "op": "set", - "key": "0x0068e867f5f899fc01d641a00cea2c42d58f3cc061797135198a85301d1e29030648", - "value": "0x00000000000000000000000000000000000000000000000000000000fb5f463b" - }, - { - "op": "set", - "key": "0xff1df77729bb76b139342cfc86aff011590bf6bb810f47e69a6babac30af96f9f5451673405763851d69ce90fb56d8dd98d3ef3ad6e6deccec4a6cc4303c04606320", - "value": "0x00000000000000000000000000000000000000000000000000000000694aeca8" - }, - { - "op": "set", - "key": "0xff34bb4f3468340d5f72275da92420aed94db48bbc61823e92f29f14f51590373aeaa08ae14db4be9f284878fd074dce4d3ac11a41a69fde2feb4b3aaaa37da25bc3", - "value": "0x000000000000000000000000000000000000000000000000000000009beca248" - }, - { - "op": "set", - "key": "0x001df77729bb76b139342cfc86aff011590bf6bb810f47e69a6babac30af96f9f5cd", - "value": "0x00000000000000000000000000000000000000000000000000000000fd378d1e" - }, - { - "op": "set", - "key": "0xff15c4813bb5b1499e0a7e0d46daab8e10c70940c008c566d5c68557b1523d1f29d090338251613ee5dbd2ea5b763cb40885f3f40530835887585c70141b84e95741", - "value": "0x000000000000000000000000000000000000000000000000000000008c0aed86" - }, - { - "op": "set", - "key": "0x0029e4e6bbd168fe5b46b7d34c34b628f95d54c4e75f9335a7e96282d1173b07df55", - "value": "0x00000000000000000000000000000000000000000000000000000000b0e9c48a" - }, - { - "op": "set", - "key": "0x00e2f41518226671332da4adddab1fdcb9903c7b895c7943b669067fe96b0baacf0c", - "value": "0x000000000000000000000000000000000000000000000000000000003841a708" - }, - { - "op": "set", - "key": "0x002eb90959edcd0b6a8a7922b8b8d37cd9664001c217b4c8e6d2df4b8ede329a7196", - "value": "0x00000000000000000000000000000000000000000000000000000000f7e377a8" - }, - { - "op": "set", - "key": "0xff9c847c439d6431d36ddeb326cb553da2e967778a24afbd1d23a62412186faf01fe7c1926f4dca1966bcd404358f4eb1d2d8e2104e04d995e7e23fbfd410d75cd9c", - "value": "0x0000000000000000000000000000000000000000000000000000000006937fd1" - }, - { - "op": "set", - "key": "0x00e61994d31ceae39cae5feb02303eb90e0d4ae90f5b0755c3c26adc81f360ec857c", - "value": "0x000000000000000000000000000000000000000000000000000000003f08c9e1" - }, - { - "op": "delete", - "key": "0xff4b83f061efb2f4708114e46b5ef50a255528c28c80bbe5fc6ad88e892bd4c4ea37491f84eb77d352d7d8fca0484b856ceab7236e2f773d157b3fbd6d0586de7fb5" - }, - { - "op": "set", - "key": "0xff18e25b9b05a714bd17ede1445ee7c785983a0194a3ed103783662bdc401dd7ffd358410b57fde52d1120a72e241687991ced2ec523b82168713b13b557349ab826", - "value": "0x00000000000000000000000000000000000000000000000000000000c425ef1b" - }, - { - "op": "delete", - "key": "0x00e61994d31ceae39cae5feb02303eb90e0d4ae90f5b0755c3c26adc81f360ec857c" - }, - { - "op": "set", - "key": "0x0015c4813bb5b1499e0a7e0d46daab8e10c70940c008c566d5c68557b1523d1f293c", - "value": "0x00000000000000000000000000000000000000000000000000000000ea48f5fd" - }, - { - "op": "set", - "key": "0x0055a6923f7414ba482ae8a64b407f386a0bb0491b742bd4b22b5b4b44fca84e8471", - "value": "0x000000000000000000000000000000000000000000000000000000006319b687" - }, - { - "op": "delete", - "key": "0x00a43fb2d7beb12b34786a837467f62b7d6c5dab4ab7eecc89c0b67db76a68292735" - }, - { - "op": "set", - "key": "0xff4c3a8541d9ec369a9fd4bcdad885cc3b166da70ed6a83066924727d9ff147fce963472d01342d91a0a268bdfee2ecf01a6d30f9021151faf2e2e6719120bf40ffc", - "value": "0x00000000000000000000000000000000000000000000000000000000e6005531" - }, - { - "op": "delete", - "key": "0xff0fb23a930b05e3bdfbdd0e6694fa9df9c5059ceabc60618bc40931ddb379f1f64c1111b166c73d061477381d77db7818b678c5a5595df51af307fccfda674238c3" - }, - { - "op": "set", - "key": "0x00e6b6d8d98be54d4c00cee378bedf964f755b5257bf1eabf52cf39aa987686fa7f8", - "value": "0x000000000000000000000000000000000000000000000000000000002665b225" - }, - { - "op": "set", - "key": "0x0046aeb3243f905d4e0eac7f60ee8962848c87e79dfbd019ece14fe45bd720ea6ba4", - "value": "0x00000000000000000000000000000000000000000000000000000000c0e4632f" - }, - { - "op": "set", - "key": "0xffbded9122e6171a5dad9ce2aaebd933abf4718e9bf12448cf113064c9fd8e6ad0551bc91eb18a241f3262e8f9c56018ae23b3e1e9d55d93bf499ad6f830b8447f65", - "value": "0x000000000000000000000000000000000000000000000000000000009740d889" - }, - { - "op": "set", - "key": "0xff18e25b9b05a714bd17ede1445ee7c785983a0194a3ed103783662bdc401dd7ff361b271b7deb81ee7027749296cc9369ae699ca1ef815575a5c0bd3b5b0b0cf8d2", - "value": "0x00000000000000000000000000000000000000000000000000000000ba230288" - }, - { - "op": "delete", - "key": "0x002cebacb183e2f51a3b6cb9f7fe2bd11c399bf1bdb8ce95ed457d3e46190197a0fd" - }, - { - "op": "set", - "key": "0x00ec64b2de1fc1d53b795f49a1c84ca4172bc3292472c3e244e4f8c225fa786f564f", - "value": "0x00000000000000000000000000000000000000000000000000000000798636a1" - }, - { - "op": "set", - "key": "0xff5963897639cd11d686b199366a1249d76bc89e0f7161c9eca48d9a199435952d8457e83ae86e00b414b40db6057395b86b6ef8d238f669d109a1044de6ee415ead", - "value": "0x00000000000000000000000000000000000000000000000000000000b84e8c3d" - }, - { - "op": "set", - "key": "0x007293ce391f5cd652216ca8ea31069a06a82f393028dfc8786aa5efd7be734b2c2c", - "value": "0x00000000000000000000000000000000000000000000000000000000e75bf304" - }, - { - "op": "set", - "key": "0xffc28c21982a56a6cecf595bfeb78297c7432abfb487cc75c09db2856a118f441df2a84c02b0cbaafcabc4cbb5f988d916f3287032f07e3f8bf51588b4311ed04c53", - "value": "0x00000000000000000000000000000000000000000000000000000000d43345f9" - }, - { - "op": "delete", - "key": "0x0055a6923f7414ba482ae8a64b407f386a0bb0491b742bd4b22b5b4b44fca84e8471" - }, - { - "op": "set", - "key": "0xff412c4d14e5bde6ea4c898a1d0f911f12a2cf775a62e3cc4c0f28331d432e8f1064ac84ee2be5e94ce2135d3a58e8221431668d3da7a5b6f8cf31778f0cd7acad7e", - "value": "0x00000000000000000000000000000000000000000000000000000000918cbe16" - }, - { - "op": "set", - "key": "0xff7b1eefd69ea014244937aaeaea4e8d4d296c04edd75ae47897e5612ef5a1a252477cbbc3c4e091807fc19bbcc03de36f41608c44f5a2f4a8cd97490324eb42b585", - "value": "0x00000000000000000000000000000000000000000000000000000000093912d6" - }, - { - "op": "delete", - "key": "0x00e2f41518226671332da4adddab1fdcb9903c7b895c7943b669067fe96b0baacf0c" - }, - { - "op": "set", - "key": "0x00e61994d31ceae39cae5feb02303eb90e0d4ae90f5b0755c3c26adc81f360ec85cf", - "value": "0x00000000000000000000000000000000000000000000000000000000c357d7e2" - }, - { - "op": "set", - "key": "0xff412c4d14e5bde6ea4c898a1d0f911f12a2cf775a62e3cc4c0f28331d432e8f10c065da61a7bd36e4bd11a16fd628e8bd5c6f79eb75cbc8ea1aa2ac6d870713c249", - "value": "0x00000000000000000000000000000000000000000000000000000000391eba89" - }, - { - "op": "delete", - "key": "0xff18e25b9b05a714bd17ede1445ee7c785983a0194a3ed103783662bdc401dd7ff361b271b7deb81ee7027749296cc9369ae699ca1ef815575a5c0bd3b5b0b0cf8d2" - }, - { - "op": "set", - "key": "0x00e6b6d8d98be54d4c00cee378bedf964f755b5257bf1eabf52cf39aa987686fa75b", - "value": "0x0000000000000000000000000000000000000000000000000000000069a38a4b" - }, - { - "op": "delete", - "key": "0xff4b83f061efb2f4708114e46b5ef50a255528c28c80bbe5fc6ad88e892bd4c4ea37491f84eb77d352d7d8fca0484b856ceab7236e2f773d157b3fbd6d0586de7f9d" - }, - { - "op": "delete", - "key": "0x000ed33feae99b046218c56bec56750a98beb5fd6a51ec13218024cb2455586b4323" - }, - { - "op": "set", - "key": "0x007293ce391f5cd652216ca8ea31069a06a82f393028dfc8786aa5efd7be734b2c4c", - "value": "0x0000000000000000000000000000000000000000000000000000000084ac6f36" - }, - { - "op": "set", - "key": "0xff1df77729bb76b139342cfc86aff011590bf6bb810f47e69a6babac30af96f9f5ffa1e869c4566231d09357daec90e71aa93821a7347b29175ef4a1bc02a0b38158", - "value": "0x000000000000000000000000000000000000000000000000000000005902afd7" - }, - { - "op": "set", - "key": "0xffe6b6d8d98be54d4c00cee378bedf964f755b5257bf1eabf52cf39aa987686fa7631039493afa587d6693f06562405982839acd12cb82734e1f59fb5256053d6062", - "value": "0x000000000000000000000000000000000000000000000000000000005d43735b" - }, - { - "op": "set", - "key": "0x0078a2812688114caf4452a5611322977e4b1000e59ef408d4c8e2c90028370bcb77", - "value": "0x00000000000000000000000000000000000000000000000000000000bb55d92e" - }, - { - "op": "set", - "key": "0xff15c4813bb5b1499e0a7e0d46daab8e10c70940c008c566d5c68557b1523d1f29d090338251613ee5dbd2ea5b763cb40885f3f40530835887585c70141b84e957b2", - "value": "0x000000000000000000000000000000000000000000000000000000004668e5c4" - }, - { - "op": "set", - "key": "0x002cebacb183e2f51a3b6cb9f7fe2bd11c399bf1bdb8ce95ed457d3e46190197a0ff", - "value": "0x000000000000000000000000000000000000000000000000000000005ba8964d" - }, - { - "op": "set", - "key": "0x00e61994d31ceae39cae5feb02303eb90e0d4ae90f5b0755c3c26adc81f360ec8582", - "value": "0x00000000000000000000000000000000000000000000000000000000a7b0735b" - }, - { - "op": "delete", - "key": "0x002eb90959edcd0b6a8a7922b8b8d37cd9664001c217b4c8e6d2df4b8ede329a717c" - }, - { - "op": "set", - "key": "0x000861030791246737d706c0d0017de01a26278bbe4ce8a134f5e0cd6f7eb69bcf43", - "value": "0x0000000000000000000000000000000000000000000000000000000052635493" - }, - { - "op": "set", - "key": "0x000ed33feae99b046218c56bec56750a98beb5fd6a51ec13218024cb2455586b43fb", - "value": "0x00000000000000000000000000000000000000000000000000000000d55f3939" - }, - { - "op": "set", - "key": "0x007b1eefd69ea014244937aaeaea4e8d4d296c04edd75ae47897e5612ef5a1a2521e", - "value": "0x000000000000000000000000000000000000000000000000000000006aa15dff" - }, - { - "op": "set", - "key": "0x00ec64b2de1fc1d53b795f49a1c84ca4172bc3292472c3e244e4f8c225fa786f56db", - "value": "0x00000000000000000000000000000000000000000000000000000000a79bbea0" - }, - { - "op": "set", - "key": "0xff0fb23a930b05e3bdfbdd0e6694fa9df9c5059ceabc60618bc40931ddb379f1f6f8ec4b65102d57431a895525e044c325b5cfaf3bec31d977b0a27ca73735d9da14", - "value": "0x00000000000000000000000000000000000000000000000000000000a3db056e" - }, - { - "op": "delete", - "key": "0x0068e867f5f899fc01d641a00cea2c42d58f3cc061797135198a85301d1e29030648" - }, - { - "op": "set", - "key": "0x0078a2812688114caf4452a5611322977e4b1000e59ef408d4c8e2c90028370bcb62", - "value": "0x00000000000000000000000000000000000000000000000000000000b82dbff4" - }, - { - "op": "set", - "key": "0x00e60d91d1d11837912311124089c6b73c9d726c99f4df797eb48c2b7fec6e5e2c22", - "value": "0x00000000000000000000000000000000000000000000000000000000576e2f9d" - }, - { - "op": "set", - "key": "0x0078a2812688114caf4452a5611322977e4b1000e59ef408d4c8e2c90028370bcbb2", - "value": "0x000000000000000000000000000000000000000000000000000000003afc2613" - }, - { - "op": "set", - "key": "0xff0fb23a930b05e3bdfbdd0e6694fa9df9c5059ceabc60618bc40931ddb379f1f6065b90a89bb317ff71b1f933493a08b55f152abe9f5ae5b58400664d141c5121a8", - "value": "0x0000000000000000000000000000000000000000000000000000000032db368d" - }, - { - "op": "set", - "key": "0x0055a6923f7414ba482ae8a64b407f386a0bb0491b742bd4b22b5b4b44fca84e84ee", - "value": "0x00000000000000000000000000000000000000000000000000000000790b45dd" - }, - { - "op": "set", - "key": "0x0078a2812688114caf4452a5611322977e4b1000e59ef408d4c8e2c90028370bcb76", - "value": "0x000000000000000000000000000000000000000000000000000000004ff61c82" - }, - { - "op": "set", - "key": "0x00412c4d14e5bde6ea4c898a1d0f911f12a2cf775a62e3cc4c0f28331d432e8f1060", - "value": "0x0000000000000000000000000000000000000000000000000000000098ae0f91" - }, - { - "op": "set", - "key": "0x00998f08d7054351e5fa3ccadda32ab8d6c0c86a825dd360d57c0fe6b46612297c6d", - "value": "0x00000000000000000000000000000000000000000000000000000000e7bf52c8" - }, - { - "op": "set", - "key": "0x00d1d141c1bed8bb4f809292e9340a0209cd167484a24a5ca993fcac4b97086d89fe", - "value": "0x000000000000000000000000000000000000000000000000000000000c86ac1f" - }, - { - "op": "set", - "key": "0xff543907253acb230f5dc96bbafd8ea0f37dd63b30e8a4a3675bb007673a31fbc54e3628f3b56f95f4e49d0141e2d17a7a533a1aee0d22c6cc49df3fb8b348ad60ff", - "value": "0x00000000000000000000000000000000000000000000000000000000c60fe37a" - }, - { - "op": "set", - "key": "0xff7293ce391f5cd652216ca8ea31069a06a82f393028dfc8786aa5efd7be734b2ca38663c8c6b9a008190c48cd4ee4fd87a0d65a077938f6b18ea2ebb292ca4b0a7e", - "value": "0x00000000000000000000000000000000000000000000000000000000cf47b878" - }, - { - "op": "delete", - "key": "0x007b1eefd69ea014244937aaeaea4e8d4d296c04edd75ae47897e5612ef5a1a2521e" - }, - { - "op": "set", - "key": "0x005963897639cd11d686b199366a1249d76bc89e0f7161c9eca48d9a199435952da0", - "value": "0x00000000000000000000000000000000000000000000000000000000b50709cf" - }, - { - "op": "set", - "key": "0x004c1eb4996f3098c89ee461d61bd5f177587b0f4363ea73f4387e6296c66a260f25", - "value": "0x00000000000000000000000000000000000000000000000000000000b0026e41" - }, - { - "op": "set", - "key": "0x00d1afd6427b13f2ce7beb82a2cd6bbfc58eac8b2d46bc87bfbc78fbf440e18a632c", - "value": "0x000000000000000000000000000000000000000000000000000000003ecb28b7" - }, - { - "op": "set", - "key": "0x0046aeb3243f905d4e0eac7f60ee8962848c87e79dfbd019ece14fe45bd720ea6b0e", - "value": "0x000000000000000000000000000000000000000000000000000000008b1fc034" - }, - { - "op": "set", - "key": "0x00d1afd6427b13f2ce7beb82a2cd6bbfc58eac8b2d46bc87bfbc78fbf440e18a63a2", - "value": "0x00000000000000000000000000000000000000000000000000000000089224ae" - }, - { - "op": "delete", - "key": "0x0015c4813bb5b1499e0a7e0d46daab8e10c70940c008c566d5c68557b1523d1f2999" - }, - { - "op": "set", - "key": "0x004c3a8541d9ec369a9fd4bcdad885cc3b166da70ed6a83066924727d9ff147fce85", - "value": "0x00000000000000000000000000000000000000000000000000000000a940c1b7" - }, - { - "op": "delete", - "key": "0x004c3a8541d9ec369a9fd4bcdad885cc3b166da70ed6a83066924727d9ff147fce85" - }, - { - "op": "delete", - "key": "0xffe57860ba0adcdd1294caf74da8b990a80a8a6861bc01b30dfe07450d25bab38f63b4527cc539651483c1b056383959f91cb98fdbd3c1799a854a2781ddb32290fb" - }, - { - "op": "delete", - "key": "0x00e6b6d8d98be54d4c00cee378bedf964f755b5257bf1eabf52cf39aa987686fa75b" - }, - { - "op": "set", - "key": "0x002cebacb183e2f51a3b6cb9f7fe2bd11c399bf1bdb8ce95ed457d3e46190197a053", - "value": "0x000000000000000000000000000000000000000000000000000000004aff7879" - }, - { - "op": "set", - "key": "0xff0ed33feae99b046218c56bec56750a98beb5fd6a51ec13218024cb2455586b43ce51c5b488a38762ddd335b3b0f645fefed9e6d3f01457ac62638b1dcc5e4e760c", - "value": "0x0000000000000000000000000000000000000000000000000000000029ca0908" - }, - { - "op": "set", - "key": "0x0068e867f5f899fc01d641a00cea2c42d58f3cc061797135198a85301d1e29030605", - "value": "0x00000000000000000000000000000000000000000000000000000000923415f8" - }, - { - "op": "set", - "key": "0x0018e25b9b05a714bd17ede1445ee7c785983a0194a3ed103783662bdc401dd7ffa8", - "value": "0x00000000000000000000000000000000000000000000000000000000bf8ad82f" - }, - { - "op": "delete", - "key": "0x00e6b6d8d98be54d4c00cee378bedf964f755b5257bf1eabf52cf39aa987686fa7f8" - } - ], - "roots_after": [ - "0xcf6ca0dad8d69c45120da214b9052e614649a640e52560d0f47487073325e93d", - "0xa7247b2d9c8c6e49a18add897d235a5e72c292a5144008676972983ebd5e624e", - "0xcf6ca0dad8d69c45120da214b9052e614649a640e52560d0f47487073325e93d", - "0x9b659884c441cf90291fe8e2336c65d34cb0da60709da8a6165486c1a00829e4", - "0x1b5adc55d232b1c9b93c8bae8c1e736a2e596efabd3cc466de195d38ecbe9746", - "0xa604a44382e79cbca1ae6140d150d84980510447609e2a2356dce852861b3217", - "0x67994f2a47248a4677f1691706ec0ad76d479894fb7015f06bda6d4af4d46a55", - "0x99b2ce17b9b779b3f63f68d218182a59b928c80c1ac8d8bcb716cd3c202a20b3", - "0xda86ba5028d29db7d6006f7cf340f6af5203506cbcc76e72ba27302933a4943f", - "0xaa845405281cd62dc569bf03f8a6c1bd35609cd2c1860db56a9478254f8cb25e", - "0x1708bbd96a838a1f0816667f74a2d2a1a01b8c5499bcb533be4a45cf684b8bef", - "0xf8850e79c7adbebb051eb0ea11e3ca9866c11f660cb2b7d2c3892beb20246f26", - "0xc6c221bffc3947d15ad0e994e150fa0e654897bde5039e88bcda287cf24dc827", - "0xf1036ba9ebb2780554ab2596581122a999fe301b33446a3bf13c623dd8796f96", - "0x61cd8daf48607b812e063019741d2ae0d77bf9193b2b5f5996b756a22a488b1f", - "0x73aaf9a0a5b4f179bacf1f1c2fbcb630163797d02cdc5115977d26e8ea9f48ff", - "0xff83d59bd08841abbfc5cf10620dd334ec2fbb9bd0e579aec30905b4e40ccab9", - "0x44b55ce7592e1ba9aa3ea9c465cdadee3f7e09abccbec49aec6290eab112d0a4", - "0xe1d9ef033ebdf35275483848038f858fa43063805c1cc1b5ea96654745cc3b94", - "0xcff0d627850d4d4ea368e66ff2349c0caf454129e387d23df3c8dcf71534768f", - "0xb360073c9f6fea4b2b8613ce10a4dd82166b7b0cf4a58cf03d05b6e1bf0a603a", - "0x208c7e70c17d7209c3694fbf9bcf7e9e7874c87ce2c44b6be466a2356747c308", - "0x9526700579706ca7fdbf2f9559d2e9fa2cb2687e811098ea6b19d7b5e4580723", - "0x2ba99e97e461801969c394fa8826fa3e9d0d7d2fc4c530687a04911e41c276b6", - "0x29b448496222b5901eb9317dcece4d5df49ceda5956390b19ca5d4e72a7aca4b", - "0xda1d4cf6de46c0cdff6d48b8a0c116df3b6b0844799c3c83e8bb694f22415c7e", - "0xb431f7d742f75fbc4b008e8576ce79c98d5e62fee5da59aea65ea1824481dc58", - "0x497441710aa139263ef9fbb9550e582d3f68ab98187bc980fd5f1e1cc77b1915", - "0xc4b25bd3a6559c7a169bdb71e97383ec9a44bda2324704a29f0d3e3c05e109af", - "0xb1ac7843c5e776c94da54b0471ec3f7ec058155491be05b8a4a6a914f532fece", - "0xf04c71372a36681f215c7626a234518f4a397e9ce7ea69b1969da0dbec5c6aba", - "0x963bf8843b41679c0c805fe2e078c88afc89e800ae68c7bac8654d2b9fae8275", - "0xc937494bae732ef43bda9f36f41d7811b724a0000c7f12b18d68fa244f85f83a", - "0xa1b52f51aff2ec9148e27600812a6f7f250b193d547541cb7291ac5b27531377", - "0x49e7d2ed49f0a16a80f9ee32cff2b92b1c5563ca2976dedf4377faa32934b587", - "0x142c035ccdb7dbb9e7c964dd932f0cacd28a730334a714894d87c1d473694115", - "0x257026d0fdee39ac10d16f45fc41bdc67cb7c65f5462b162e97123335a721087", - "0x44210262b17ef2ce604b8f760e40ba4f6810d096a9c6badf610919a4c240e106", - "0x34b2eccd7b5f3767b7b379e23a79b51cb5aa96bc00d14860b85ff4484ad28616", - "0x06d575937706dcab6c13c7c5c50f2dc50347560e8cfcf7d41558b3ea3143a051", - "0xbf5e02b80d4a44b750e14e5005c55693c9c04c17da5af780b76297c1ab54e5a6", - "0x2556391529759b8428f4fe99357b3137b3448f8d7a6ea6935c4f93aa38dc8373", - "0x0798de4a687305f7f63fdc90196e8ae7c40b1abf8b864d169bd5e1e27afa451f", - "0x6a278acb5e817c9bda137843e72bf5472cc85dea63b0a83b8f026d757bfe0e9d", - "0x861f2f4c797ddb8653b22fc5cfea3c015c9c4e9cc361b16f0aad3a0f3a007e23", - "0xc2c1120e643b57b9fdc8951f313a49e5cbd36b6597a1dae37d5966202550ccc1", - "0x8b9d66932ed20ec20776164266f4aafcfe0dfd9e6abda346378b4a128e2a8663", - "0xc1ecbbfc8782deb03b2449a28213cb163a83d00b84a9ea8b1587850c6339a75d", - "0xed23f054d3766b19c862cf082afae679e31dbd3608bd16c2de0a1cf10f2e57ef", - "0x8ef00a29a96eddcae4bc1747194add0f6b633046912a15e065422bb15a02a821", - "0x0b1166c05d3c49875381e7238db4e9a2bd0f31172ffd4640333a94787f78c6a2", - "0x7c9f26d17ce7379676d56b608379b4af2d5f5c8a66422eebd0f2c2ad1d47cc7b", - "0x065ba2ffdead957828b37f2315ac3d4a278a916df97c15e871b311e6975c6a62", - "0x3b65db9ab1623eafbb0be692575c1a0fe8c89e3c44c31c0ae4b8f7e49b143d6b", - "0x248a7ddabf2e6682ad71ec54ce32f23c2b34cc8ecb009b4ea05674e593b15c6e", - "0xe5e21bec2c3bab941951849a4d26d929f0e2ba3cd1fc0a036c8eb7d89a9875ec", - "0x1ac665122b19a6dc65665cae30c062c630502c6383d8a9789a5707a046886936", - "0xf7090e29c8d58586496cefd6be820c5ed07df662e8bf7bb20417990caa798083", - "0xc7c92a89957d8bcde3d89a06af03ba1ffed14a91f322aa9e5c0e8cb4add1b54e", - "0xc7c8c808f74ee9820231ed33be6f19507361432d9db3754eca149722276e3d7b", - "0xfd83b3ef076ffc238e781a0d0e5581667ec298df0392fd1995d4e85a07733ec3", - "0x9f6e3c675e6849dc67b4e8bf74c7ab92625029aea49a457c5fa5a40ccfad8bfb", - "0xec7f5ff4c9a4223a9d5136dfee1781491caf7e2eb6f1bc7b13d63e6339dbb0be", - "0xeab647ff5c2d31ee101e1b14d01c1b19fa9278fc0d6ad87c1ad34d8cb139c69f", - "0x9f59d79991a7dacd90df4ebff5fad773e8cfa2a7aad270e53f95ff5d51025d14", - "0x40c37dfc185c9ad30b36a6c3d70c261d6929dc95dba1d21bb8f21d36d70ac7ae", - "0xcd9f2abf02b4c8dce567e841164ef0aa9878975c17e7d0cd6ec2a283f8bacc0b", - "0x237cd11f13b7f4def4bffba3ca20cfba81257eba3371da7b9b959ab91b92eae9", - "0xb40d35cdf4fec5f5712567d8ffe3755cf06525413569717a6b396c0628298a2a", - "0xcf4a9616f8c8b981cf9e907e11e2065d4ae55936d73838a0b80928c228989a79", - "0xf9c724516024072146d39cba478679a2b72bfecf65c5e4bf6630adb8a5001f69", - "0xb19da381011d4e18e47f011a3649ef120bc64b7e1155f58ee57a23a7c3fdfb0f", - "0xc5cfb10924de3fa07e5915085a6b9bfc16921211247b530bdd6072b61342e58f", - "0x2bc4d35f7b4ad0058ad3cc3643c8e30273c83177e2cad26d3aee861e39a849a4", - "0x0406ed0eb0da6947478234d14425dba144f51b93fe2bcc0ee7665410780f8c09", - "0x08273095920f4b0b4d8ec6f39d4b14852d329ca089c87c79e086488368245dca", - "0xe3b6c6491a6028f66dff35de1f6d1bab2f197fecf10f986880b5a5cf340a4835", - "0xd35bf68cd5bae8ba07b9e2866704ee68284690646176e0fe2f2b41c0ade6ee80", - "0x8fab00b973dfcb6003c0f8d9e91a48c90ee220413c4ab4f92261275d7586e9a1", - "0xfe4795724e5b9f8d80b75caf4dd1b5dd3b3f12fe338537ee36663e2f3e63a5ba", - "0x132d929abced32b1e26c9311a9e6abaf9e562beddac5ccfef40f154a1ba4396f", - "0x2520dd342924ff668896b4a6edcdcc770694c5419f363a45cec4474e48033553", - "0xa48fc239a6d81a479e40eb89781d3c2e7143ab32bf70e6a6bf4bb0c6e940c1b8", - "0x670a87d76a7eb5b1bad2ad6a7242e8fe3620207d34e12ee1ca912c9711d926cd", - "0x31517aec30a94303afef5d99b31932f01af948a02413c3c780f4e0abcf76ffce", - "0x06f21ad8ddeb29f151d30788940737d13659a69e10a8341931e5110de0b78de1", - "0xc508e6241e421a9699d4a9a066c2a12e43ffe9d95d716bb0b40454676869f35d", - "0x7c0b579b0e3567f75fe3e8ee2c5892c66b438ef5b31bced869c84891358d5d5b", - "0xb2aaeec514853a4dbfb295b700c7cf634bbc544e129524bf15c2fbc544177666", - "0x9824d449ce4cfd0b389a62c6c2b63651ff11ec5f9671437bea5f59f9635ebbd7", - "0x0f368a50ab7d5eefa7dd840a5e5b10e36b9b6e4342d851ddde4c65ec51e014fd", - "0x148f52411c2264f84d5fa98f63eb355575874db367701f6d4d44162a0e688450", - "0x1bf78390c33f7f04f515f0c806dcb716eaf40048b0a89cd0f2aecc7c04c45586", - "0x3b3a18162aaa32dc6344b386d64cac002639d6a47d13eb60b94b1890e5deecde", - "0xcb6fc59c42d9c64244d7110dd91f8af058d25859c4c48194f77b4648751d1f73", - "0x62f208cc34424fbc0c40ed208049c677655e9a443afa82f931575897df26bca7", - "0xc7a8ecbccd8bf1975a3d820a43fa7f490777c827703fa340b5acd7801a63a37f", - "0x2686b1ead22026fdd6fbf3efd5d72d58987db12a75ea4c7c6484a8bc7b693145", - "0xfeff75d397216247329810da9c4e1585d875a27c9ba0aee51f4dfd679f02a17c", - "0xc8c528b117b2e87fb327884281b6afa72edde356d92f443803b0c60e1d45fb8f", - "0x08c7cfe28bf531575668da302173840473511a38e10857f0eb61a09be7959559", - "0x4a2520c9a2e39870e8be946efd1dec6221fb323eec658ad7961a728fce936fb7", - "0x77b6bba491fbc13dd07b8679026034494fbf53cffef0992e1e2005c3dfcfed45", - "0xe3c3ebd215b446c5aa9d2923e80b1bef94a3b424a789455fed664556de879e05", - "0x4edb1a5ff8f4bfcd0230433b7c210a55a4f2f78fa5c2bf406d6cbe9df3c86475", - "0xa635fa92e36e6939836f612e3a39ac974a6a38c32e61ef4f26225509e6f04492", - "0x7632441b27590957b807b49ab1a6ebd2c481a8fa2b32c9f052a7315740ae8c69", - "0xb9e84c69e52ffaf839e02addbe4a2ad0d1d2d2803e203e63a0035012435ac5d7", - "0xecb9401c0a9b1408d3e201fac4aa2d5a7d53a0858788ae2cff8df6634ce84dbf", - "0x8972767bae825a2e3335c8067c2d6442f4f98b23cd402638b4a2b9f54811a8eb", - "0x39ad2334dd4be73bd16b9f82c28d1351bcd1e786c9af8fa7613746f983bfabf6", - "0xab65d7a1eabebf9a1cf96fe7ff3c34a4dfd2d192991fafeaf30397c96079c7c5", - "0x39ad2334dd4be73bd16b9f82c28d1351bcd1e786c9af8fa7613746f983bfabf6", - "0xafd2dd6b97b8c543460cdd780aff2d87ceb9db784bfe7a686af6e858c3a22555", - "0x41b885d3566da95eba3e8036b50242145b3d8eabdf0db050a4d8f40e406087b9", - "0x3500c426109dd2f374df8ae87fc054bff2e222831ef21dfd6a77146e8f8d7e91", - "0x853cc2b1cea36805654ad6d2137f65bc443edb7829ea6cda66df575d9a098404", - "0x5acd94e7c18c7b26b7dee3716e72e8fc096dc2e53ca281034aa34143b2cadab2", - "0x64f69ee1df07d749cae00990210420c8ed362fa2e0d63391267d3e34323da6bc", - "0xaa7c0921513588b382bb634d1cf114bbfd3c72b0033b50503101e9c2b3025bd4" - ] - }, - { - "seed": 11832, - "ops": [ - { - "op": "set", - "key": "0xff1df77729bb76b139342cfc86aff011590bf6bb810f47e69a6babac30af96f9f5ffa1e869c4566231d09357daec90e71aa93821a7347b29175ef4a1bc02a0b38120", - "value": "0x0000000000000000000000000000000000000000000000000000000079b57838" - }, - { - "op": "set", - "key": "0xff3cbfd03e3f083b0987f841c4372031faf655a63472518a3613e2e30ecb542b51ef2c19e3ed459de7a67968b7004609b54e8e34fef08d7f9ed3c6eedcc980488255", - "value": "0x00000000000000000000000000000000000000000000000000000000449c8b5d" - }, - { - "op": "set", - "key": "0x007b1eefd69ea014244937aaeaea4e8d4d296c04edd75ae47897e5612ef5a1a25289", - "value": "0x00000000000000000000000000000000000000000000000000000000b5b13d29" - }, - { - "op": "set", - "key": "0xffa43fb2d7beb12b34786a837467f62b7d6c5dab4ab7eecc89c0b67db76a6829272094bf023796a3d4bdc16643cddf5395ab048ba346a4f50f1cc64da78ad7dd5266", - "value": "0x000000000000000000000000000000000000000000000000000000008cc69019" - }, - { - "op": "set", - "key": "0xff5963897639cd11d686b199366a1249d76bc89e0f7161c9eca48d9a199435952d4a28b184a4e022d38844c7967da6fbdf879ffafc6cfabf88c929ec9c9ff90040d1", - "value": "0x00000000000000000000000000000000000000000000000000000000af9bbd7d" - }, - { - "op": "set", - "key": "0x00e61994d31ceae39cae5feb02303eb90e0d4ae90f5b0755c3c26adc81f360ec85bf", - "value": "0x000000000000000000000000000000000000000000000000000000005dde837c" - }, - { - "op": "delete", - "key": "0xffa43fb2d7beb12b34786a837467f62b7d6c5dab4ab7eecc89c0b67db76a6829272094bf023796a3d4bdc16643cddf5395ab048ba346a4f50f1cc64da78ad7dd5266" - }, - { - "op": "delete", - "key": "0xff1df77729bb76b139342cfc86aff011590bf6bb810f47e69a6babac30af96f9f5ffa1e869c4566231d09357daec90e71aa93821a7347b29175ef4a1bc02a0b38120" - }, - { - "op": "delete", - "key": "0xff3cbfd03e3f083b0987f841c4372031faf655a63472518a3613e2e30ecb542b51ef2c19e3ed459de7a67968b7004609b54e8e34fef08d7f9ed3c6eedcc980488255" - }, - { - "op": "delete", - "key": "0x00e61994d31ceae39cae5feb02303eb90e0d4ae90f5b0755c3c26adc81f360ec85bf" - }, - { - "op": "set", - "key": "0xff0fb23a930b05e3bdfbdd0e6694fa9df9c5059ceabc60618bc40931ddb379f1f6065b90a89bb317ff71b1f933493a08b55f152abe9f5ae5b58400664d141c512119", - "value": "0x000000000000000000000000000000000000000000000000000000000a082d85" - }, - { - "op": "set", - "key": "0xff998f08d7054351e5fa3ccadda32ab8d6c0c86a825dd360d57c0fe6b46612297c725184c1ce99f155151a19aa14a427e1a784778319e2b193ac66bd6374b328cdff", - "value": "0x00000000000000000000000000000000000000000000000000000000a3ea3eb4" - }, - { - "op": "set", - "key": "0xff78a2812688114caf4452a5611322977e4b1000e59ef408d4c8e2c90028370bcb71e2e06bd4204550cd67cae560e42f8afcdc75e454a14f00ed2a8ec7c885869e40", - "value": "0x000000000000000000000000000000000000000000000000000000007435a9e4" - }, - { - "op": "delete", - "key": "0xff0fb23a930b05e3bdfbdd0e6694fa9df9c5059ceabc60618bc40931ddb379f1f6065b90a89bb317ff71b1f933493a08b55f152abe9f5ae5b58400664d141c512119" - }, - { - "op": "set", - "key": "0x000fb23a930b05e3bdfbdd0e6694fa9df9c5059ceabc60618bc40931ddb379f1f668", - "value": "0x000000000000000000000000000000000000000000000000000000000275abc8" - }, - { - "op": "delete", - "key": "0xff5963897639cd11d686b199366a1249d76bc89e0f7161c9eca48d9a199435952d4a28b184a4e022d38844c7967da6fbdf879ffafc6cfabf88c929ec9c9ff90040d1" - }, - { - "op": "set", - "key": "0x007293ce391f5cd652216ca8ea31069a06a82f393028dfc8786aa5efd7be734b2cbe", - "value": "0x0000000000000000000000000000000000000000000000000000000094f87f55" - }, - { - "op": "set", - "key": "0xff4c3a8541d9ec369a9fd4bcdad885cc3b166da70ed6a83066924727d9ff147fce963472d01342d91a0a268bdfee2ecf01a6d30f9021151faf2e2e6719120bf40f0a", - "value": "0x00000000000000000000000000000000000000000000000000000000fdac9fff" - }, - { - "op": "set", - "key": "0xff1df77729bb76b139342cfc86aff011590bf6bb810f47e69a6babac30af96f9f5ffa1e869c4566231d09357daec90e71aa93821a7347b29175ef4a1bc02a0b38181", - "value": "0x00000000000000000000000000000000000000000000000000000000e4d876b8" - }, - { - "op": "set", - "key": "0x00e2f41518226671332da4adddab1fdcb9903c7b895c7943b669067fe96b0baacfd5", - "value": "0x0000000000000000000000000000000000000000000000000000000019be8821" - }, - { - "op": "set", - "key": "0x0046aeb3243f905d4e0eac7f60ee8962848c87e79dfbd019ece14fe45bd720ea6b40", - "value": "0x00000000000000000000000000000000000000000000000000000000a4a3d6a1" - }, - { - "op": "delete", - "key": "0x007293ce391f5cd652216ca8ea31069a06a82f393028dfc8786aa5efd7be734b2cbe" - }, - { - "op": "set", - "key": "0x0034bb4f3468340d5f72275da92420aed94db48bbc61823e92f29f14f51590373a6e", - "value": "0x00000000000000000000000000000000000000000000000000000000f8bb0323" - }, - { - "op": "delete", - "key": "0x00e2f41518226671332da4adddab1fdcb9903c7b895c7943b669067fe96b0baacfd5" - }, - { - "op": "delete", - "key": "0x007b1eefd69ea014244937aaeaea4e8d4d296c04edd75ae47897e5612ef5a1a25289" - }, - { - "op": "set", - "key": "0xff4b83f061efb2f4708114e46b5ef50a255528c28c80bbe5fc6ad88e892bd4c4ea4b53fa472b269bd8517302932b840c9802e6edca11b366371a9b7cd8c280f791e9", - "value": "0x00000000000000000000000000000000000000000000000000000000c87c281b" - }, - { - "op": "set", - "key": "0x00e57860ba0adcdd1294caf74da8b990a80a8a6861bc01b30dfe07450d25bab38f2a", - "value": "0x00000000000000000000000000000000000000000000000000000000a34f6676" - }, - { - "op": "delete", - "key": "0xff78a2812688114caf4452a5611322977e4b1000e59ef408d4c8e2c90028370bcb71e2e06bd4204550cd67cae560e42f8afcdc75e454a14f00ed2a8ec7c885869e40" - }, - { - "op": "set", - "key": "0xff5ac76a8f23131e4ca988c8afcc313130cc95e521d2691635fc26196d63e66a1f893d3c65a10fbfddef979c778efdeaf625c4d5326e417354a396e996bacabea50e", - "value": "0x00000000000000000000000000000000000000000000000000000000cfe3e137" - }, - { - "op": "delete", - "key": "0xff998f08d7054351e5fa3ccadda32ab8d6c0c86a825dd360d57c0fe6b46612297c725184c1ce99f155151a19aa14a427e1a784778319e2b193ac66bd6374b328cdff" - }, - { - "op": "delete", - "key": "0xff4b83f061efb2f4708114e46b5ef50a255528c28c80bbe5fc6ad88e892bd4c4ea4b53fa472b269bd8517302932b840c9802e6edca11b366371a9b7cd8c280f791e9" - }, - { - "op": "set", - "key": "0x0078a2812688114caf4452a5611322977e4b1000e59ef408d4c8e2c90028370bcb61", - "value": "0x000000000000000000000000000000000000000000000000000000005fd626fb" - }, - { - "op": "set", - "key": "0xff4c1eb4996f3098c89ee461d61bd5f177587b0f4363ea73f4387e6296c66a260fb343f74f407821bc354e33da7a0ff703c3a0b8a8ba6ed25c75dddafc0182fa025f", - "value": "0x00000000000000000000000000000000000000000000000000000000e8af3939" - }, - { - "op": "set", - "key": "0x00998f08d7054351e5fa3ccadda32ab8d6c0c86a825dd360d57c0fe6b46612297c64", - "value": "0x00000000000000000000000000000000000000000000000000000000c60d0d76" - }, - { - "op": "set", - "key": "0xffe57860ba0adcdd1294caf74da8b990a80a8a6861bc01b30dfe07450d25bab38f633bf20195771cd09de04706b99eda30093d53b7c3b76cc8888c52db5ac6b223c6", - "value": "0x0000000000000000000000000000000000000000000000000000000001e813df" - }, - { - "op": "set", - "key": "0xffe2f41518226671332da4adddab1fdcb9903c7b895c7943b669067fe96b0baacf7d209e014165b283a74c97557e34dd6f49626dcfa254a7af4fc2ec4b667c55093a", - "value": "0x00000000000000000000000000000000000000000000000000000000270d998e" - }, - { - "op": "set", - "key": "0xff7293ce391f5cd652216ca8ea31069a06a82f393028dfc8786aa5efd7be734b2ca38663c8c6b9a008190c48cd4ee4fd87a0d65a077938f6b18ea2ebb292ca4b0a5d", - "value": "0x00000000000000000000000000000000000000000000000000000000d7608e7c" - }, - { - "op": "set", - "key": "0x00c28c21982a56a6cecf595bfeb78297c7432abfb487cc75c09db2856a118f441db2", - "value": "0x00000000000000000000000000000000000000000000000000000000c4224d37" - }, - { - "op": "delete", - "key": "0xff4c1eb4996f3098c89ee461d61bd5f177587b0f4363ea73f4387e6296c66a260fb343f74f407821bc354e33da7a0ff703c3a0b8a8ba6ed25c75dddafc0182fa025f" - }, - { - "op": "set", - "key": "0x00d1d141c1bed8bb4f809292e9340a0209cd167484a24a5ca993fcac4b97086d890e", - "value": "0x00000000000000000000000000000000000000000000000000000000b90275c0" - }, - { - "op": "set", - "key": "0xff998f08d7054351e5fa3ccadda32ab8d6c0c86a825dd360d57c0fe6b46612297c725184c1ce99f155151a19aa14a427e1a784778319e2b193ac66bd6374b328cd81", - "value": "0x0000000000000000000000000000000000000000000000000000000032b77bf5" - }, - { - "op": "set", - "key": "0x009c847c439d6431d36ddeb326cb553da2e967778a24afbd1d23a62412186faf01bc", - "value": "0x0000000000000000000000000000000000000000000000000000000088b6deef" - }, - { - "op": "set", - "key": "0x00c28c21982a56a6cecf595bfeb78297c7432abfb487cc75c09db2856a118f441da5", - "value": "0x0000000000000000000000000000000000000000000000000000000026fe225f" - }, - { - "op": "delete", - "key": "0xffe2f41518226671332da4adddab1fdcb9903c7b895c7943b669067fe96b0baacf7d209e014165b283a74c97557e34dd6f49626dcfa254a7af4fc2ec4b667c55093a" - }, - { - "op": "set", - "key": "0x00e61994d31ceae39cae5feb02303eb90e0d4ae90f5b0755c3c26adc81f360ec8542", - "value": "0x000000000000000000000000000000000000000000000000000000004311a76d" - }, - { - "op": "delete", - "key": "0x0078a2812688114caf4452a5611322977e4b1000e59ef408d4c8e2c90028370bcb61" - }, - { - "op": "set", - "key": "0xff46aeb3243f905d4e0eac7f60ee8962848c87e79dfbd019ece14fe45bd720ea6b919d757e67485d253a689ff08c388cf32b629d2fb6fc4c514296d9b64fb4e58d12", - "value": "0x0000000000000000000000000000000000000000000000000000000084c560be" - }, - { - "op": "set", - "key": "0xffbded9122e6171a5dad9ce2aaebd933abf4718e9bf12448cf113064c9fd8e6ad0551bc91eb18a241f3262e8f9c56018ae23b3e1e9d55d93bf499ad6f830b8447f6a", - "value": "0x0000000000000000000000000000000000000000000000000000000014a070f9" - }, - { - "op": "set", - "key": "0x00e60d91d1d11837912311124089c6b73c9d726c99f4df797eb48c2b7fec6e5e2c18", - "value": "0x00000000000000000000000000000000000000000000000000000000226a563e" - }, - { - "op": "set", - "key": "0x000fb23a930b05e3bdfbdd0e6694fa9df9c5059ceabc60618bc40931ddb379f1f625", - "value": "0x0000000000000000000000000000000000000000000000000000000011ca60ad" - }, - { - "op": "delete", - "key": "0x00998f08d7054351e5fa3ccadda32ab8d6c0c86a825dd360d57c0fe6b46612297c64" - }, - { - "op": "set", - "key": "0x00e6b6d8d98be54d4c00cee378bedf964f755b5257bf1eabf52cf39aa987686fa786", - "value": "0x00000000000000000000000000000000000000000000000000000000622a6640" - }, - { - "op": "set", - "key": "0xffc28c21982a56a6cecf595bfeb78297c7432abfb487cc75c09db2856a118f441df2a84c02b0cbaafcabc4cbb5f988d916f3287032f07e3f8bf51588b4311ed04c2e", - "value": "0x0000000000000000000000000000000000000000000000000000000021e1aa16" - }, - { - "op": "set", - "key": "0xffc28c21982a56a6cecf595bfeb78297c7432abfb487cc75c09db2856a118f441df2a84c02b0cbaafcabc4cbb5f988d916f3287032f07e3f8bf51588b4311ed04cc6", - "value": "0x0000000000000000000000000000000000000000000000000000000068067cb5" - }, - { - "op": "delete", - "key": "0xff998f08d7054351e5fa3ccadda32ab8d6c0c86a825dd360d57c0fe6b46612297c725184c1ce99f155151a19aa14a427e1a784778319e2b193ac66bd6374b328cd81" - }, - { - "op": "set", - "key": "0x0068e867f5f899fc01d641a00cea2c42d58f3cc061797135198a85301d1e29030671", - "value": "0x00000000000000000000000000000000000000000000000000000000e9f7d15a" - }, - { - "op": "delete", - "key": "0x00c28c21982a56a6cecf595bfeb78297c7432abfb487cc75c09db2856a118f441db2" - }, - { - "op": "set", - "key": "0x00e2f41518226671332da4adddab1fdcb9903c7b895c7943b669067fe96b0baacf9d", - "value": "0x0000000000000000000000000000000000000000000000000000000020f14759" - }, - { - "op": "set", - "key": "0xff5ac76a8f23131e4ca988c8afcc313130cc95e521d2691635fc26196d63e66a1f33b1e44125772918b9d44a12766eb0dad773e863d120780173c394789aa668e9ab", - "value": "0x00000000000000000000000000000000000000000000000000000000e1d034a1" - }, - { - "op": "set", - "key": "0x000861030791246737d706c0d0017de01a26278bbe4ce8a134f5e0cd6f7eb69bcf67", - "value": "0x00000000000000000000000000000000000000000000000000000000ee7dbf42" - }, - { - "op": "set", - "key": "0x00c901cfbb39e551189323bea0af6cf9e81008c90dce65cbc2ff4672e263d7bb8795", - "value": "0x00000000000000000000000000000000000000000000000000000000cb564e14" - }, - { - "op": "set", - "key": "0x00e57860ba0adcdd1294caf74da8b990a80a8a6861bc01b30dfe07450d25bab38fa4", - "value": "0x00000000000000000000000000000000000000000000000000000000cc0627e7" - }, - { - "op": "set", - "key": "0xff543907253acb230f5dc96bbafd8ea0f37dd63b30e8a4a3675bb007673a31fbc54e3628f3b56f95f4e49d0141e2d17a7a533a1aee0d22c6cc49df3fb8b348ad60ea", - "value": "0x00000000000000000000000000000000000000000000000000000000d00483f6" - }, - { - "op": "delete", - "key": "0x00e57860ba0adcdd1294caf74da8b990a80a8a6861bc01b30dfe07450d25bab38f2a" - }, - { - "op": "set", - "key": "0x0068e867f5f899fc01d641a00cea2c42d58f3cc061797135198a85301d1e2903067f", - "value": "0x000000000000000000000000000000000000000000000000000000007989dc89" - }, - { - "op": "delete", - "key": "0xff7293ce391f5cd652216ca8ea31069a06a82f393028dfc8786aa5efd7be734b2ca38663c8c6b9a008190c48cd4ee4fd87a0d65a077938f6b18ea2ebb292ca4b0a5d" - }, - { - "op": "set", - "key": "0x00bded9122e6171a5dad9ce2aaebd933abf4718e9bf12448cf113064c9fd8e6ad00e", - "value": "0x0000000000000000000000000000000000000000000000000000000094bd932f" - }, - { - "op": "set", - "key": "0xffe6b6d8d98be54d4c00cee378bedf964f755b5257bf1eabf52cf39aa987686fa7eba9c3c87289872a212b18c553d1652d395a1ec2ba73555b79fc5c8a5b880c3685", - "value": "0x00000000000000000000000000000000000000000000000000000000cc2b7c13" - }, - { - "op": "set", - "key": "0x003cbfd03e3f083b0987f841c4372031faf655a63472518a3613e2e30ecb542b5132", - "value": "0x00000000000000000000000000000000000000000000000000000000414b993e" - }, - { - "op": "set", - "key": "0xff0861030791246737d706c0d0017de01a26278bbe4ce8a134f5e0cd6f7eb69bcfd972a2766ba0be044a7342182891121b96cd6d1f005fd95f01ffc9b24311a22fed", - "value": "0x00000000000000000000000000000000000000000000000000000000353a2c08" - }, - { - "op": "set", - "key": "0x0078a2812688114caf4452a5611322977e4b1000e59ef408d4c8e2c90028370bcb59", - "value": "0x000000000000000000000000000000000000000000000000000000004ba4d334" - }, - { - "op": "set", - "key": "0xff4c1eb4996f3098c89ee461d61bd5f177587b0f4363ea73f4387e6296c66a260f84715f048d0204439e2d98c4c2e5cb144730a98fb2ee40b399d12f783c1fbd31b5", - "value": "0x0000000000000000000000000000000000000000000000000000000090d86f57" - }, - { - "op": "set", - "key": "0x00543907253acb230f5dc96bbafd8ea0f37dd63b30e8a4a3675bb007673a31fbc551", - "value": "0x00000000000000000000000000000000000000000000000000000000066f4f8c" - }, - { - "op": "set", - "key": "0x00c28c21982a56a6cecf595bfeb78297c7432abfb487cc75c09db2856a118f441d34", - "value": "0x00000000000000000000000000000000000000000000000000000000696c1855" - }, - { - "op": "set", - "key": "0xff18e25b9b05a714bd17ede1445ee7c785983a0194a3ed103783662bdc401dd7ffd358410b57fde52d1120a72e241687991ced2ec523b82168713b13b557349ab807", - "value": "0x0000000000000000000000000000000000000000000000000000000063765b8b" - }, - { - "op": "set", - "key": "0xffe60d91d1d11837912311124089c6b73c9d726c99f4df797eb48c2b7fec6e5e2c6066e8c722a574a900a59551ed956d32ca2b36e48771ed8b373da6b223bbbe9c00", - "value": "0x00000000000000000000000000000000000000000000000000000000a429e408" - }, - { - "op": "delete", - "key": "0xff543907253acb230f5dc96bbafd8ea0f37dd63b30e8a4a3675bb007673a31fbc54e3628f3b56f95f4e49d0141e2d17a7a533a1aee0d22c6cc49df3fb8b348ad60ea" - }, - { - "op": "set", - "key": "0x00c28c21982a56a6cecf595bfeb78297c7432abfb487cc75c09db2856a118f441d7b", - "value": "0x00000000000000000000000000000000000000000000000000000000b8e97a76" - }, - { - "op": "delete", - "key": "0x00e61994d31ceae39cae5feb02303eb90e0d4ae90f5b0755c3c26adc81f360ec8542" - }, - { - "op": "delete", - "key": "0x00e57860ba0adcdd1294caf74da8b990a80a8a6861bc01b30dfe07450d25bab38fa4" - }, - { - "op": "delete", - "key": "0xffe6b6d8d98be54d4c00cee378bedf964f755b5257bf1eabf52cf39aa987686fa7eba9c3c87289872a212b18c553d1652d395a1ec2ba73555b79fc5c8a5b880c3685" - }, - { - "op": "set", - "key": "0x00e57860ba0adcdd1294caf74da8b990a80a8a6861bc01b30dfe07450d25bab38f21", - "value": "0x00000000000000000000000000000000000000000000000000000000ce5959ab" - }, - { - "op": "delete", - "key": "0x000861030791246737d706c0d0017de01a26278bbe4ce8a134f5e0cd6f7eb69bcf67" - }, - { - "op": "set", - "key": "0xff1df77729bb76b139342cfc86aff011590bf6bb810f47e69a6babac30af96f9f5ffa1e869c4566231d09357daec90e71aa93821a7347b29175ef4a1bc02a0b38153", - "value": "0x000000000000000000000000000000000000000000000000000000004dcbc52d" - }, - { - "op": "delete", - "key": "0x00c901cfbb39e551189323bea0af6cf9e81008c90dce65cbc2ff4672e263d7bb8795" - }, - { - "op": "set", - "key": "0xff7293ce391f5cd652216ca8ea31069a06a82f393028dfc8786aa5efd7be734b2c5c915dbeeb53329739ac0626eb5034da5b64e4e02399423c11fa9c3245cdba5a53", - "value": "0x000000000000000000000000000000000000000000000000000000008640a4f1" - }, - { - "op": "set", - "key": "0xff0fb23a930b05e3bdfbdd0e6694fa9df9c5059ceabc60618bc40931ddb379f1f69b7b14fb2b661d561b45215872510d7417350a448e39ebefec4984fbfc5722eb07", - "value": "0x00000000000000000000000000000000000000000000000000000000609bc375" - }, - { - "op": "set", - "key": "0xff412c4d14e5bde6ea4c898a1d0f911f12a2cf775a62e3cc4c0f28331d432e8f10c065da61a7bd36e4bd11a16fd628e8bd5c6f79eb75cbc8ea1aa2ac6d870713c2f6", - "value": "0x00000000000000000000000000000000000000000000000000000000af79da23" - }, - { - "op": "set", - "key": "0x00d1d141c1bed8bb4f809292e9340a0209cd167484a24a5ca993fcac4b97086d8952", - "value": "0x000000000000000000000000000000000000000000000000000000003be4e1a3" - }, - { - "op": "set", - "key": "0x004c1eb4996f3098c89ee461d61bd5f177587b0f4363ea73f4387e6296c66a260f5e", - "value": "0x0000000000000000000000000000000000000000000000000000000061bf8586" - }, - { - "op": "delete", - "key": "0xffe57860ba0adcdd1294caf74da8b990a80a8a6861bc01b30dfe07450d25bab38f633bf20195771cd09de04706b99eda30093d53b7c3b76cc8888c52db5ac6b223c6" - }, - { - "op": "set", - "key": "0xff1df77729bb76b139342cfc86aff011590bf6bb810f47e69a6babac30af96f9f5451673405763851d69ce90fb56d8dd98d3ef3ad6e6deccec4a6cc4303c0460632f", - "value": "0x000000000000000000000000000000000000000000000000000000005f032ac1" - }, - { - "op": "set", - "key": "0xffc28c21982a56a6cecf595bfeb78297c7432abfb487cc75c09db2856a118f441d4d68c9b03da1dec117fa853416c0249fb7b863e57d51f6ac8e86e023c336f4688d", - "value": "0x00000000000000000000000000000000000000000000000000000000be4a0420" - }, - { - "op": "set", - "key": "0xff4b83f061efb2f4708114e46b5ef50a255528c28c80bbe5fc6ad88e892bd4c4ea4b53fa472b269bd8517302932b840c9802e6edca11b366371a9b7cd8c280f79190", - "value": "0x000000000000000000000000000000000000000000000000000000005f60883d" - }, - { - "op": "set", - "key": "0xff412c4d14e5bde6ea4c898a1d0f911f12a2cf775a62e3cc4c0f28331d432e8f10fb3754ee233f7964e47e5e73dc4342ee28be4dcf5e0bb64009bcbfd0c440559c06", - "value": "0x00000000000000000000000000000000000000000000000000000000d2bc3cb2" - }, - { - "op": "set", - "key": "0xffe61994d31ceae39cae5feb02303eb90e0d4ae90f5b0755c3c26adc81f360ec854d6578131d380078a46e85324e6c5548cbb1581bf0c2505cedd28736583e573019", - "value": "0x0000000000000000000000000000000000000000000000000000000074c5e9a9" - }, - { - "op": "set", - "key": "0x0068e867f5f899fc01d641a00cea2c42d58f3cc061797135198a85301d1e29030601", - "value": "0x000000000000000000000000000000000000000000000000000000006c7b5ae3" - }, - { - "op": "set", - "key": "0x0068e867f5f899fc01d641a00cea2c42d58f3cc061797135198a85301d1e2903068a", - "value": "0x000000000000000000000000000000000000000000000000000000006584a459" - }, - { - "op": "set", - "key": "0x00543907253acb230f5dc96bbafd8ea0f37dd63b30e8a4a3675bb007673a31fbc5be", - "value": "0x00000000000000000000000000000000000000000000000000000000996c5e6b" - }, - { - "op": "set", - "key": "0x00e60d91d1d11837912311124089c6b73c9d726c99f4df797eb48c2b7fec6e5e2c3e", - "value": "0x000000000000000000000000000000000000000000000000000000008887aa4b" - }, - { - "op": "set", - "key": "0x0018e25b9b05a714bd17ede1445ee7c785983a0194a3ed103783662bdc401dd7ffa0", - "value": "0x000000000000000000000000000000000000000000000000000000000b876b7d" - }, - { - "op": "set", - "key": "0x0034bb4f3468340d5f72275da92420aed94db48bbc61823e92f29f14f51590373a19", - "value": "0x0000000000000000000000000000000000000000000000000000000006c64054" - }, - { - "op": "set", - "key": "0xffc901cfbb39e551189323bea0af6cf9e81008c90dce65cbc2ff4672e263d7bb871b0daa3888412359e9769bc566073558cc37b927578bcc74743311f2e7487706d3", - "value": "0x000000000000000000000000000000000000000000000000000000006b9a2b07" - }, - { - "op": "delete", - "key": "0xff1df77729bb76b139342cfc86aff011590bf6bb810f47e69a6babac30af96f9f5ffa1e869c4566231d09357daec90e71aa93821a7347b29175ef4a1bc02a0b38181" - }, - { - "op": "set", - "key": "0x001df77729bb76b139342cfc86aff011590bf6bb810f47e69a6babac30af96f9f57c", - "value": "0x00000000000000000000000000000000000000000000000000000000881ea9ad" - }, - { - "op": "set", - "key": "0x0055a6923f7414ba482ae8a64b407f386a0bb0491b742bd4b22b5b4b44fca84e8444", - "value": "0x000000000000000000000000000000000000000000000000000000000c07da3a" - }, - { - "op": "set", - "key": "0xff998f08d7054351e5fa3ccadda32ab8d6c0c86a825dd360d57c0fe6b46612297cc290a8b5ac7a453c9cec5724a8a85ad4002fb6dc9c73e86aedfa05133317c040f6", - "value": "0x0000000000000000000000000000000000000000000000000000000094362837" - }, - { - "op": "set", - "key": "0x007b1eefd69ea014244937aaeaea4e8d4d296c04edd75ae47897e5612ef5a1a2524c", - "value": "0x00000000000000000000000000000000000000000000000000000000d0e6ee22" - }, - { - "op": "set", - "key": "0x000fb23a930b05e3bdfbdd0e6694fa9df9c5059ceabc60618bc40931ddb379f1f6fb", - "value": "0x000000000000000000000000000000000000000000000000000000005235ca9f" - }, - { - "op": "delete", - "key": "0x0018e25b9b05a714bd17ede1445ee7c785983a0194a3ed103783662bdc401dd7ffa0" - }, - { - "op": "delete", - "key": "0x004c1eb4996f3098c89ee461d61bd5f177587b0f4363ea73f4387e6296c66a260f5e" - }, - { - "op": "delete", - "key": "0xff4c3a8541d9ec369a9fd4bcdad885cc3b166da70ed6a83066924727d9ff147fce963472d01342d91a0a268bdfee2ecf01a6d30f9021151faf2e2e6719120bf40f0a" - }, - { - "op": "set", - "key": "0xffc28c21982a56a6cecf595bfeb78297c7432abfb487cc75c09db2856a118f441d0b16068d4b14101ddeb56b5b42f0587a3a4aa093b1de8fb5ecb2311977bdbcbee0", - "value": "0x00000000000000000000000000000000000000000000000000000000acf0f355" - }, - { - "op": "set", - "key": "0xff15c4813bb5b1499e0a7e0d46daab8e10c70940c008c566d5c68557b1523d1f29d090338251613ee5dbd2ea5b763cb40885f3f40530835887585c70141b84e957e3", - "value": "0x0000000000000000000000000000000000000000000000000000000028346819" - }, - { - "op": "delete", - "key": "0xffc28c21982a56a6cecf595bfeb78297c7432abfb487cc75c09db2856a118f441d0b16068d4b14101ddeb56b5b42f0587a3a4aa093b1de8fb5ecb2311977bdbcbee0" - }, - { - "op": "set", - "key": "0xff9c847c439d6431d36ddeb326cb553da2e967778a24afbd1d23a62412186faf01d11c941f9715afff547cb69563e69a920e6d5098967069e5002620d5124791d872", - "value": "0x00000000000000000000000000000000000000000000000000000000c058f48b" - }, - { - "op": "set", - "key": "0xff7293ce391f5cd652216ca8ea31069a06a82f393028dfc8786aa5efd7be734b2cea4832fe56448fc97a7b636806f7e3afef77ab0a5a99e9d5e8d12b5ba815af4a1b", - "value": "0x0000000000000000000000000000000000000000000000000000000079aa2bb6" - }, - { - "op": "set", - "key": "0x0055bbfb5b05f80543bd662a7b7a74980a2992612798087fa24e1fc86b8e8508e466", - "value": "0x000000000000000000000000000000000000000000000000000000000ec6d4ad" - }, - { - "op": "set", - "key": "0xff4b83f061efb2f4708114e46b5ef50a255528c28c80bbe5fc6ad88e892bd4c4ea37491f84eb77d352d7d8fca0484b856ceab7236e2f773d157b3fbd6d0586de7fa9", - "value": "0x0000000000000000000000000000000000000000000000000000000045e9c809" - }, - { - "op": "set", - "key": "0xffa43fb2d7beb12b34786a837467f62b7d6c5dab4ab7eecc89c0b67db76a6829272094bf023796a3d4bdc16643cddf5395ab048ba346a4f50f1cc64da78ad7dd52a4", - "value": "0x000000000000000000000000000000000000000000000000000000009b943498" - } - ], - "roots_after": [ - "0x5cdba549646b7612059692efa58d33f90059dfa9df06433f4438510c1b32e049", - "0x6ae6cd7fe0f7f9dfbff03d1d6b88c55f8c9f247584c70427a2a53612cdf202e6", - "0xcbe8e97d34728b819654a46096d4fe13abcdeed3e31cf088be21de915051259a", - "0x155969c29e159077b8a9997325ac9962fee077d64c9467f1ccd68618c9abbcdb", - "0x2439229c4decda037eb0a692540821cdd6c77df8ba51d478291d905e5de4f5e4", - "0xc252ede6498166ca351dc0346fdd1ee5df6694e3acf7f5714f2e80db687ccc79", - "0x7fcfd5973d297879b65209bb9cf1ca74c30ee717e13350d2c0447675165cb80b", - "0xdbb4e48dda8ae1f9287abccd62637085bb171f5897ad42e5019500209e89202d", - "0x14712da3d0ce63185cae268f29bf2593295476a83a8e95b3b4daf01a83274d6f", - "0x51098829074dc7618bdca7c079f0ac0577e0a08a0df5c92dcd104bf6cad43ea6", - "0x2643869090c9ca916157631cac68996dd01de67c66639eeb443f9b93ebe7df1c", - "0x9793adae9d275d569398dc631e09da40464320a0dc0c644d36d79f97d34b7288", - "0xef2bd1092bc3404855dd94e9452f97cebeb425d8027e7a56dc1c5fc0e93c38f6", - "0x85170f6f2bed9f200e8835f9d2c9bd13cb9267d77e834dcd73d43d1f897cc346", - "0x71c46fa0b2804adc2012db0d399c477429071b47f13fc31ddc125bc599e8fafb", - "0xeba1b27a56ff60fd186ede1c5ee445c59b2733f527c6ccc30b765b6917491720", - "0x2d0dcb278bc7892ad37afe2c523b73d18a60e76b1dc41d04469896c3b355d490", - "0x5aadbab832910092a62a49e78c5d76b10ffe26f3452a0bd47fd049e36eb31d63", - "0x53b8d1dab39520c882f5666b092ab47f59f4f3084c9471eefbb15d6a0c58ad5d", - "0x52ff67405b74b808b1649b64764de113df17e70ea5aebf5489bc1db7aad52422", - "0xde35ec177dd5c384cf24be794135d622ddd2a666b9d67d1d46fa149d5aa4caf9", - "0x18f08d2cfe81c6db210cd9ffcfd632acf3adc0070cca7e9af1754923edd32ae7", - "0x3361b009bb8a11272223d2b625739ffeffb7f096e2e09a22bce9fdd247322784", - "0x938df1fca555925f0c85971ca7b13842c86b161dee9056cb89f8e6e144f22bff", - "0x813308a0141f9e4eeccd35f41b439c5dfc3e8759ef516e83c01e0f1e6dc86f43", - "0x0c00cd26f4296a59ee426a5c1e262c43d56d57528832c012775a8f4735ac7471", - "0x3d9f7d81ec22374d4fbce9b0d7d9b435c6445bb86ecbe0eb69a10d22091c03a7", - "0x5eb7a6660d0856276ec22ea96d9e745e5fd7d12efbef04f39dbcb779e1adb32c", - "0x85edd30c13788e8e6fdf1c3ee5e2a24173117ce61839a76881ffc050442ba818", - "0xd6cd1175b20eba2b0158adf019376de82382c8f042cdce53b9879a76a211e518", - "0xe6aa26c153493c447c58aea50a5973e696afebefed800f84f2e829e2dd670282", - "0x7edb4484f862c0055164aa33198f81db08dd7fffb34c7bcae8fd06707191a15a", - "0x70c6c933187eaf011a83d4db61e8f022e7f94e36712371156bf0c41e4f46c42b", - "0x7073f03032107282c802817ecddd49294ce407e567465c0494386d22eb1faedc", - "0xcf5a920598934a4b21c39b3fee92586dec28802279dd709ace6cf5baf2dd11f9", - "0x9a0fcaebdd11a283f596942ff8e7cb7246458f8fb5c706d172f730ae108b20d3", - "0x7ca3a8a966a560dd237b6fdaa468152bc1617bde6cd68ce6e4ca547473b00b84", - "0xb5b7b22a573b230cbf2d04d3f6be6277d10fc262c7768a189af2426564c5257f", - "0xdca78a4aa2f85748f3a1660b22d2fde59d506a85d155f7a0d928b91339c2c85b", - "0xf348b3e47434a30a2a72f6cd67156f637bef3ba0b91803e186bda23b458312e8", - "0xd94b253d3bb77d5351177f2ff1af9d494d955fedf3614dfae18c82d2582e51af", - "0x710342a386d0270bd892c68c0f3c70acc40bccb744466a02e14dd7b47a78b28a", - "0x7de7721305a34da9a88031afae727da365c476a2f5b61fb0d8a79a64935abebb", - "0xe9523320ccaf8cefa207dab523c9eef34b6efe9ac3700e1f95b6a0752e58d432", - "0x04eb69694e545df95face8a1ed89ab7ed6f3d9598d5e0ca47607e1ceac69ecb6", - "0x608f4c6465c877ed24984c890edd30d2997202eb2d184ab759cedf2713140ba8", - "0xc13778b97187a09ef78dcf1e891562379c577a0ebbbf464398a371b8fac1adc6", - "0xe709bb96efe184d77f40bc5fcaeb1036cca00e9d99f4769f0a8dd15005321062", - "0x60c84e4635c2adb2fca77a7f7945ab04714ea254c1e8cdf5daf880f25b3ea556", - "0x25c47b0e563c5a006e063ec274cdf7626b4e8481e3e90f1f95e720335b60a168", - "0x416df08ef16042b79682a3582669f55c84d339208b121023f1b92c92858f2bc3", - "0x75fb6c30d6943ae9ff620c9b3f10df7c7e851d9d0fe5b39a880cc71ad8593e71", - "0x2dbc418daa3ea94d39b7264f4ebaa8a8414fabd5c1733cd041db901deade5fc7", - "0xd30d126c66e8d995684f056d0fa6cdfa3d74298622a74c09810f7fedfbb47a0a", - "0xc9f6d4bf69fbc09640af570c9d8a1e67020a3209ceb131531f72912e5849eee6", - "0x115b91274eba5af0ed852f198cccbadc6cca83441e4e8be6475598d15b61e90d", - "0x0632ba803f2b4f34f37a6da6be87813e5a10c6598bfb16e4dfdad6ab040f6c79", - "0x2891d8b8cd3afb1d09beefe9949971a9c5ffaa4735810fd62739f2171cf36f36", - "0x49d04f88c23d6f57e0928c694290300ac973ac2d96996472ca0d3b6743bb42c0", - "0xe0dc4ddf3c96c8107a95a9826fc96ff3f15e6d07c1abdf0647410645c4cc472d", - "0xd2845932e63e79547b546ef2c020c82d41b920769a2ff16cda86c48dd7362789", - "0xc5450ad1eed3c356da39e3a60d6745be4edbe1877eb96e6440e97d49543862fd", - "0x3a7c0da4984cac174966a1da6ed11e28ba87ceccd549c50d756964fcdf913328", - "0x721b9141b313fff8513f15cb83e2a229dbd960b99745f4bec9e6aabf42c52467", - "0xdddbde643821c34afec8702ed12561103065c9258fefa28ee78a9b411373e0cf", - "0xafcaa951c9c7eb8af986a4edcedf098d680fce7ae4c28bde28c0bdab73ad806c", - "0xd3c93b18b889b8b6807d7416eb3651bc3cb24076026f3531ce5f300e90e28d0a", - "0xabe59ae5af09518a481d24ed0be9d33aa2408a131135c43eb5a193bdb8607b7f", - "0x70df88d62fafba7dfe9c4f51347eab8d3e3e880b6bc3d748b85a9fd8f81504b4", - "0x57942b88183644f597553794cef3849a64e8b4d30b6355e3578345bf93454798", - "0xfd8c711a7666534b069ae7ce59fe462a70262a6b96cbc7f8a84ca9b51e02f4f2", - "0xae571d58d248c86182fb5f82c039a5d085ec87cdd88fb44f72e18a4d17e08462", - "0x8c2a2961b0e2af42659eaeb11d19dbfb08297ca2224d4d378bb0063477396c2e", - "0x5cd4b7183ab194c9b1e7e793ee5ba2d0458300594ee6fe8c590782fc7c3eb7b4", - "0x4729519398ef89012ca69b75a0a9f7689b7e9b80b4161b9643865069cac6363d", - "0xc50a746ceb60df8f1ff0242190a7b1652b493dec93b2a6fecda756751d50633c", - "0xc43995327da1f6d47067fbc31949360de852577b4c1548ff662f9f3040508264", - "0x8ff981e26abf25aec33f2afbb28af0742451614047283f7bb70e31e38c51a9df", - "0x99fcd88960a49a0824be2a642620020c95d5b7488bad67ab1ec1ee0db601a492", - "0x64855c99a705a385fb8aa4f4c05bb17d5c7adae93f3c04a197891ecf9b430686", - "0x0a6b3a96b3d4a4fcde8a85b3b8d76522ceb01997d884d5879b767cc8eede2237", - "0x8ea008ea01671788c07f9257c776375137361f5cb005328015d1a21a843f736f", - "0x655fba748caaa3ab8cb7e46690eb2d2121462f1e1f38d75a0261fdec62455b81", - "0x864c76a20fe992ea8956fed530dda10941723fa61dc174af9569365df5dda004", - "0xb1d791aacaabd12e3565ad9a89f7831e032d1f4d0c765c12c0346014dc1097f5", - "0x744c37d3da39ef9db08b6492d2c217df89f0e2e9c5986daa2e44f49ab41781ab", - "0x8cde4594d8311716a5b139977268ab0084ac8bc23650bef3c24200742532b93e", - "0x8279bcc9c549e772762ef59318ba63c056dc8b8ac3568481f43bf8592c87ea42", - "0x2a03117b65aefbe812d8e5d5dfbc21cafd186a150cfb71e4310705c4e2f7b221", - "0x64124ad4a041176d1d99405c0662d2546482a36c20a95aaafced0df4b96375b6", - "0xb12379d77600f4c80a8e3092c6f1df31b546163af05be011c755fb07d707cb02", - "0xc0e5483bfef982691db8d431d74ecc0c7f6325ccdd955da00ba31903f6f80174", - "0x12c810d02b7807a79e29085dfd6246506d7a76a20e857a0501d6bf7f92b0d719", - "0x0f47737a2686703b19045bd5d1c98e9b2f6f989cde9a6eeb5ab2d959165c35d6", - "0x1fa31221ea50b19060c07eb3d9583fb4040674d085acefcf604a824842fc87e2", - "0x27492b69b3669a3388b16acea6eb010af60d1eb4e892a5b5f2190be8137e5f06", - "0x1940b21301c53319e34cbe54bad39c3c48f229ad2780a9121429f9863fa1a384", - "0x7ce8e6ff56c62ebf9172490eb9fdeeede0f46b8aea7bc24ec435511f071e18e9", - "0x7c570ab7810ef6c96503a503521c9d7a683eb032d234770c9c0a5ba4ef80cb39", - "0x212ce3d1f8a7a80a583d7841a813b123bc6ece2189c57949f4a40135f594b279", - "0x45e5541d748eba70d7a246cf6fa0a0db2edb0d4ad912c5fcde39c7a48350b1e3", - "0x220942366fb7a9b76412d91179d77dbbf30d2e659485b021d7d2296e377add42", - "0xab3b810ed5f6b42a32cfa29ade1fe14d42d1c080941ca2feec6782a4260e1819", - "0x5c599030220325ae60db3fd89485c575349f01c66d8bedee2e3c8912fa611f3f", - "0xb4d39480c8b7e32680ab30be4f766d99a7df7cc3d3bc584387a1bf8a2c5ee9ea", - "0x0a56dff343f24da8e6a702265d25da852eb385c0ea385f49594dd68d22d31d32", - "0x90e521dc6503b2ddc37fcf4e71aae22c8d996ba292316e1986f2699aff1fda53", - "0xa92a89954dcdd759a54d3439a6e6ddd985542123d13e15f2a709ca41ecbfc4d2", - "0xe501ac34a1d59da80219272f28fbd1704219c12072596b8bab7139d2bc91a586", - "0x805caae702b7ef3f71e77490595eb3539fffb8595e1d17345e77aea5cb0f025b", - "0x77c082e75dbc39c6e3a474c776f38e44e83863eb9c218c41cad70cd99492c213", - "0xecc65146f292e965138644ba4dccd0fe044dcc8953765eca89335a9287043467", - "0x267fe34c53aff1d596f12b931940c206a2a889e4d21236151f6c46ba0b46f5d1", - "0xc1bf3b0ecf8ad1fb72f847e17855833f578b6ecd6803d509bdd87244c4b76c6d", - "0x2845e63ae4b3e4bb0b1bc4979c108b90ff0189997f06d9958bf1800bf8a8d164", - "0xe2a246d4da5fe62cde8cf4ca8b687d182117b9e1131b817c7a8484ec4b79c92e", - "0xd52ced602f33b718493b514b2ffb0f028c134b57ba918ce4e0928c7d7df8c395", - "0x55ac0ef97885e52596284c93bb673cfaa1f2f2a049b206a5779a52f1e6ecd7d1", - "0x7ae69a4c2194e79d883376de224df0cddc2254a0882cf0cd3c039c8c5e2bfc03", - "0x729168791ebce2fedddf26fbdbb59bc4c49b42e4a71e203af6e090783c1a20ab" - ] - }, - { - "seed": 3102, - "ops": [ - { - "op": "set", - "key": "0xff2cebacb183e2f51a3b6cb9f7fe2bd11c399bf1bdb8ce95ed457d3e46190197a02a6a55293936c1e1b6894fbb3b2873c993bd580ec835889bf0f211410e368fc199", - "value": "0x000000000000000000000000000000000000000000000000000000002e422f9a" - }, - { - "op": "delete", - "key": "0xff2cebacb183e2f51a3b6cb9f7fe2bd11c399bf1bdb8ce95ed457d3e46190197a02a6a55293936c1e1b6894fbb3b2873c993bd580ec835889bf0f211410e368fc199" - }, - { - "op": "set", - "key": "0xff29e4e6bbd168fe5b46b7d34c34b628f95d54c4e75f9335a7e96282d1173b07dfd0f9925799d37553e0f34b2d7a49a135fc34a90b6fb99bfe70f2e43c28e758854d", - "value": "0x000000000000000000000000000000000000000000000000000000002ecaa733" - }, - { - "op": "set", - "key": "0x00d1afd6427b13f2ce7beb82a2cd6bbfc58eac8b2d46bc87bfbc78fbf440e18a635f", - "value": "0x0000000000000000000000000000000000000000000000000000000076fe3750" - }, - { - "op": "set", - "key": "0x000861030791246737d706c0d0017de01a26278bbe4ce8a134f5e0cd6f7eb69bcff9", - "value": "0x0000000000000000000000000000000000000000000000000000000035fd5ae2" - }, - { - "op": "set", - "key": "0xff998f08d7054351e5fa3ccadda32ab8d6c0c86a825dd360d57c0fe6b46612297cc290a8b5ac7a453c9cec5724a8a85ad4002fb6dc9c73e86aedfa05133317c0402e", - "value": "0x00000000000000000000000000000000000000000000000000000000be9e2390" - }, - { - "op": "set", - "key": "0x005963897639cd11d686b199366a1249d76bc89e0f7161c9eca48d9a199435952d33", - "value": "0x00000000000000000000000000000000000000000000000000000000b3e90b26" - }, - { - "op": "set", - "key": "0xffe57860ba0adcdd1294caf74da8b990a80a8a6861bc01b30dfe07450d25bab38f20483a0da833c6de846c07a8214bee6ee90bd1da5d9b9553f74784602fdd83dfb9", - "value": "0x0000000000000000000000000000000000000000000000000000000051dcd3af" - }, - { - "op": "set", - "key": "0x00e6b6d8d98be54d4c00cee378bedf964f755b5257bf1eabf52cf39aa987686fa743", - "value": "0x0000000000000000000000000000000000000000000000000000000083a3dad3" - }, - { - "op": "set", - "key": "0xff0fb23a930b05e3bdfbdd0e6694fa9df9c5059ceabc60618bc40931ddb379f1f6065b90a89bb317ff71b1f933493a08b55f152abe9f5ae5b58400664d141c512161", - "value": "0x00000000000000000000000000000000000000000000000000000000939e31a5" - }, - { - "op": "set", - "key": "0xff2cebacb183e2f51a3b6cb9f7fe2bd11c399bf1bdb8ce95ed457d3e46190197a018eb952465e3dd252f35c719bb6d7d14f62bf363e0afa74100d3046f81539e08b9", - "value": "0x000000000000000000000000000000000000000000000000000000002da16542" - }, - { - "op": "set", - "key": "0xff4b83f061efb2f4708114e46b5ef50a255528c28c80bbe5fc6ad88e892bd4c4ea37491f84eb77d352d7d8fca0484b856ceab7236e2f773d157b3fbd6d0586de7f48", - "value": "0x000000000000000000000000000000000000000000000000000000003b1510f6" - }, - { - "op": "set", - "key": "0x00c901cfbb39e551189323bea0af6cf9e81008c90dce65cbc2ff4672e263d7bb87f3", - "value": "0x0000000000000000000000000000000000000000000000000000000087d0f3c4" - }, - { - "op": "set", - "key": "0x0055a6923f7414ba482ae8a64b407f386a0bb0491b742bd4b22b5b4b44fca84e848f", - "value": "0x000000000000000000000000000000000000000000000000000000008cfbc63e" - }, - { - "op": "set", - "key": "0xff7b1eefd69ea014244937aaeaea4e8d4d296c04edd75ae47897e5612ef5a1a25254d51e5b726c0f3cfd1b03f07f25907b18767b99fef9eb727475858c458e1c7087", - "value": "0x00000000000000000000000000000000000000000000000000000000af70ae1b" - }, - { - "op": "set", - "key": "0xff29e4e6bbd168fe5b46b7d34c34b628f95d54c4e75f9335a7e96282d1173b07df754b0f807e1dc46306d4cb12e85e18d008678fa80cd9e6fa118c34e179bd412a87", - "value": "0x00000000000000000000000000000000000000000000000000000000d15c3b16" - }, - { - "op": "set", - "key": "0xff29e4e6bbd168fe5b46b7d34c34b628f95d54c4e75f9335a7e96282d1173b07df754b0f807e1dc46306d4cb12e85e18d008678fa80cd9e6fa118c34e179bd412a57", - "value": "0x000000000000000000000000000000000000000000000000000000003e5f6e17" - }, - { - "op": "set", - "key": "0x0055bbfb5b05f80543bd662a7b7a74980a2992612798087fa24e1fc86b8e8508e4b6", - "value": "0x000000000000000000000000000000000000000000000000000000002a25f39d" - }, - { - "op": "delete", - "key": "0xff0fb23a930b05e3bdfbdd0e6694fa9df9c5059ceabc60618bc40931ddb379f1f6065b90a89bb317ff71b1f933493a08b55f152abe9f5ae5b58400664d141c512161" - }, - { - "op": "set", - "key": "0x00e61994d31ceae39cae5feb02303eb90e0d4ae90f5b0755c3c26adc81f360ec85de", - "value": "0x00000000000000000000000000000000000000000000000000000000fd3f724c" - }, - { - "op": "set", - "key": "0xff543907253acb230f5dc96bbafd8ea0f37dd63b30e8a4a3675bb007673a31fbc54e3628f3b56f95f4e49d0141e2d17a7a533a1aee0d22c6cc49df3fb8b348ad608b", - "value": "0x0000000000000000000000000000000000000000000000000000000077d80388" - }, - { - "op": "set", - "key": "0x0018e25b9b05a714bd17ede1445ee7c785983a0194a3ed103783662bdc401dd7ff6a", - "value": "0x00000000000000000000000000000000000000000000000000000000fd3b61da" - }, - { - "op": "delete", - "key": "0xff29e4e6bbd168fe5b46b7d34c34b628f95d54c4e75f9335a7e96282d1173b07df754b0f807e1dc46306d4cb12e85e18d008678fa80cd9e6fa118c34e179bd412a87" - }, - { - "op": "set", - "key": "0x004c3a8541d9ec369a9fd4bcdad885cc3b166da70ed6a83066924727d9ff147fce0e", - "value": "0x0000000000000000000000000000000000000000000000000000000028a51712" - }, - { - "op": "set", - "key": "0x0015c4813bb5b1499e0a7e0d46daab8e10c70940c008c566d5c68557b1523d1f297c", - "value": "0x00000000000000000000000000000000000000000000000000000000e0e8d974" - }, - { - "op": "delete", - "key": "0xffe57860ba0adcdd1294caf74da8b990a80a8a6861bc01b30dfe07450d25bab38f20483a0da833c6de846c07a8214bee6ee90bd1da5d9b9553f74784602fdd83dfb9" - }, - { - "op": "delete", - "key": "0x004c3a8541d9ec369a9fd4bcdad885cc3b166da70ed6a83066924727d9ff147fce0e" - }, - { - "op": "delete", - "key": "0x0055a6923f7414ba482ae8a64b407f386a0bb0491b742bd4b22b5b4b44fca84e848f" - }, - { - "op": "set", - "key": "0xff2eb90959edcd0b6a8a7922b8b8d37cd9664001c217b4c8e6d2df4b8ede329a71386c0526d8dbab547c20a9e4f76e5cb3823def2a6851f17a6623ca792d40c20af9", - "value": "0x00000000000000000000000000000000000000000000000000000000aa623352" - }, - { - "op": "delete", - "key": "0x0015c4813bb5b1499e0a7e0d46daab8e10c70940c008c566d5c68557b1523d1f297c" - }, - { - "op": "set", - "key": "0xff998f08d7054351e5fa3ccadda32ab8d6c0c86a825dd360d57c0fe6b46612297c36c8cce2938e709d43205827ddebecc780d7e631bb1acb24046af6e5b23e445a99", - "value": "0x0000000000000000000000000000000000000000000000000000000045bfc591" - }, - { - "op": "delete", - "key": "0xff29e4e6bbd168fe5b46b7d34c34b628f95d54c4e75f9335a7e96282d1173b07dfd0f9925799d37553e0f34b2d7a49a135fc34a90b6fb99bfe70f2e43c28e758854d" - }, - { - "op": "set", - "key": "0xffc28c21982a56a6cecf595bfeb78297c7432abfb487cc75c09db2856a118f441d4d68c9b03da1dec117fa853416c0249fb7b863e57d51f6ac8e86e023c336f468d4", - "value": "0x000000000000000000000000000000000000000000000000000000006569ec1a" - }, - { - "op": "set", - "key": "0xffe57860ba0adcdd1294caf74da8b990a80a8a6861bc01b30dfe07450d25bab38f633bf20195771cd09de04706b99eda30093d53b7c3b76cc8888c52db5ac6b22388", - "value": "0x000000000000000000000000000000000000000000000000000000005110212d" - }, - { - "op": "set", - "key": "0xff5ac76a8f23131e4ca988c8afcc313130cc95e521d2691635fc26196d63e66a1f893d3c65a10fbfddef979c778efdeaf625c4d5326e417354a396e996bacabea561", - "value": "0x0000000000000000000000000000000000000000000000000000000054bfda98" - }, - { - "op": "delete", - "key": "0x00e61994d31ceae39cae5feb02303eb90e0d4ae90f5b0755c3c26adc81f360ec85de" - }, - { - "op": "set", - "key": "0xff2eb90959edcd0b6a8a7922b8b8d37cd9664001c217b4c8e6d2df4b8ede329a71715725eb2030fcfeb52b18f6f9d8245268cfdc4361b456b193745cf6a187bdcb4c", - "value": "0x00000000000000000000000000000000000000000000000000000000f3ea16f3" - }, - { - "op": "set", - "key": "0x0078a2812688114caf4452a5611322977e4b1000e59ef408d4c8e2c90028370bcbb2", - "value": "0x0000000000000000000000000000000000000000000000000000000036fb95d8" - }, - { - "op": "set", - "key": "0x0078a2812688114caf4452a5611322977e4b1000e59ef408d4c8e2c90028370bcb82", - "value": "0x00000000000000000000000000000000000000000000000000000000109f55b9" - }, - { - "op": "set", - "key": "0x004c1eb4996f3098c89ee461d61bd5f177587b0f4363ea73f4387e6296c66a260f08", - "value": "0x000000000000000000000000000000000000000000000000000000007f6ea99d" - }, - { - "op": "set", - "key": "0x0015c4813bb5b1499e0a7e0d46daab8e10c70940c008c566d5c68557b1523d1f2951", - "value": "0x0000000000000000000000000000000000000000000000000000000073e47b48" - }, - { - "op": "delete", - "key": "0x0055bbfb5b05f80543bd662a7b7a74980a2992612798087fa24e1fc86b8e8508e4b6" - }, - { - "op": "set", - "key": "0xff4c1eb4996f3098c89ee461d61bd5f177587b0f4363ea73f4387e6296c66a260f6ec3a127efd23185b926d47d1bd096f711c14587e91f703452a6f9499afe2a6ade", - "value": "0x000000000000000000000000000000000000000000000000000000001da721b0" - }, - { - "op": "delete", - "key": "0x004c1eb4996f3098c89ee461d61bd5f177587b0f4363ea73f4387e6296c66a260f08" - }, - { - "op": "delete", - "key": "0xff4c1eb4996f3098c89ee461d61bd5f177587b0f4363ea73f4387e6296c66a260f6ec3a127efd23185b926d47d1bd096f711c14587e91f703452a6f9499afe2a6ade" - }, - { - "op": "delete", - "key": "0xff4b83f061efb2f4708114e46b5ef50a255528c28c80bbe5fc6ad88e892bd4c4ea37491f84eb77d352d7d8fca0484b856ceab7236e2f773d157b3fbd6d0586de7f48" - }, - { - "op": "set", - "key": "0x0015c4813bb5b1499e0a7e0d46daab8e10c70940c008c566d5c68557b1523d1f2969", - "value": "0x0000000000000000000000000000000000000000000000000000000063a406d6" - }, - { - "op": "set", - "key": "0xff412c4d14e5bde6ea4c898a1d0f911f12a2cf775a62e3cc4c0f28331d432e8f10fb3754ee233f7964e47e5e73dc4342ee28be4dcf5e0bb64009bcbfd0c440559c0e", - "value": "0x00000000000000000000000000000000000000000000000000000000e350d607" - }, - { - "op": "set", - "key": "0x001df77729bb76b139342cfc86aff011590bf6bb810f47e69a6babac30af96f9f576", - "value": "0x0000000000000000000000000000000000000000000000000000000088ec24d4" - }, - { - "op": "set", - "key": "0x0018e25b9b05a714bd17ede1445ee7c785983a0194a3ed103783662bdc401dd7ffe4", - "value": "0x000000000000000000000000000000000000000000000000000000004e868a1b" - }, - { - "op": "set", - "key": "0x00e6b6d8d98be54d4c00cee378bedf964f755b5257bf1eabf52cf39aa987686fa73d", - "value": "0x0000000000000000000000000000000000000000000000000000000098928b05" - }, - { - "op": "delete", - "key": "0x00e6b6d8d98be54d4c00cee378bedf964f755b5257bf1eabf52cf39aa987686fa743" - }, - { - "op": "delete", - "key": "0x0018e25b9b05a714bd17ede1445ee7c785983a0194a3ed103783662bdc401dd7ffe4" - }, - { - "op": "set", - "key": "0x00e2f41518226671332da4adddab1fdcb9903c7b895c7943b669067fe96b0baacf6e", - "value": "0x000000000000000000000000000000000000000000000000000000008acb81b2" - }, - { - "op": "set", - "key": "0x00ec64b2de1fc1d53b795f49a1c84ca4172bc3292472c3e244e4f8c225fa786f56df", - "value": "0x00000000000000000000000000000000000000000000000000000000328f7460" - }, - { - "op": "delete", - "key": "0xff543907253acb230f5dc96bbafd8ea0f37dd63b30e8a4a3675bb007673a31fbc54e3628f3b56f95f4e49d0141e2d17a7a533a1aee0d22c6cc49df3fb8b348ad608b" - }, - { - "op": "delete", - "key": "0xff2cebacb183e2f51a3b6cb9f7fe2bd11c399bf1bdb8ce95ed457d3e46190197a018eb952465e3dd252f35c719bb6d7d14f62bf363e0afa74100d3046f81539e08b9" - }, - { - "op": "delete", - "key": "0x00ec64b2de1fc1d53b795f49a1c84ca4172bc3292472c3e244e4f8c225fa786f56df" - }, - { - "op": "set", - "key": "0xff29e4e6bbd168fe5b46b7d34c34b628f95d54c4e75f9335a7e96282d1173b07dfd0f9925799d37553e0f34b2d7a49a135fc34a90b6fb99bfe70f2e43c28e75885e4", - "value": "0x00000000000000000000000000000000000000000000000000000000bb996469" - }, - { - "op": "set", - "key": "0x007b1eefd69ea014244937aaeaea4e8d4d296c04edd75ae47897e5612ef5a1a25262", - "value": "0x00000000000000000000000000000000000000000000000000000000ea04efcd" - }, - { - "op": "set", - "key": "0x00412c4d14e5bde6ea4c898a1d0f911f12a2cf775a62e3cc4c0f28331d432e8f103b", - "value": "0x00000000000000000000000000000000000000000000000000000000a1afc0b8" - }, - { - "op": "delete", - "key": "0xff2eb90959edcd0b6a8a7922b8b8d37cd9664001c217b4c8e6d2df4b8ede329a71715725eb2030fcfeb52b18f6f9d8245268cfdc4361b456b193745cf6a187bdcb4c" - }, - { - "op": "set", - "key": "0x001df77729bb76b139342cfc86aff011590bf6bb810f47e69a6babac30af96f9f515", - "value": "0x0000000000000000000000000000000000000000000000000000000006c72b7d" - }, - { - "op": "set", - "key": "0x001df77729bb76b139342cfc86aff011590bf6bb810f47e69a6babac30af96f9f5f5", - "value": "0x0000000000000000000000000000000000000000000000000000000089929d12" - }, - { - "op": "set", - "key": "0x00c28c21982a56a6cecf595bfeb78297c7432abfb487cc75c09db2856a118f441def", - "value": "0x0000000000000000000000000000000000000000000000000000000070efd653" - }, - { - "op": "set", - "key": "0x00e2f41518226671332da4adddab1fdcb9903c7b895c7943b669067fe96b0baacf05", - "value": "0x00000000000000000000000000000000000000000000000000000000bcccc339" - }, - { - "op": "set", - "key": "0x00e57860ba0adcdd1294caf74da8b990a80a8a6861bc01b30dfe07450d25bab38fd3", - "value": "0x0000000000000000000000000000000000000000000000000000000017d57e02" - }, - { - "op": "set", - "key": "0x00e2f41518226671332da4adddab1fdcb9903c7b895c7943b669067fe96b0baacf8c", - "value": "0x000000000000000000000000000000000000000000000000000000004c32ab2f" - }, - { - "op": "delete", - "key": "0x007b1eefd69ea014244937aaeaea4e8d4d296c04edd75ae47897e5612ef5a1a25262" - }, - { - "op": "set", - "key": "0x00d1d141c1bed8bb4f809292e9340a0209cd167484a24a5ca993fcac4b97086d897e", - "value": "0x000000000000000000000000000000000000000000000000000000002dfa839a" - }, - { - "op": "set", - "key": "0x00e60d91d1d11837912311124089c6b73c9d726c99f4df797eb48c2b7fec6e5e2ca2", - "value": "0x000000000000000000000000000000000000000000000000000000004d4df975" - }, - { - "op": "set", - "key": "0xff998f08d7054351e5fa3ccadda32ab8d6c0c86a825dd360d57c0fe6b46612297cc290a8b5ac7a453c9cec5724a8a85ad4002fb6dc9c73e86aedfa05133317c04050", - "value": "0x00000000000000000000000000000000000000000000000000000000b1eed0c4" - }, - { - "op": "set", - "key": "0x0055a6923f7414ba482ae8a64b407f386a0bb0491b742bd4b22b5b4b44fca84e8454", - "value": "0x0000000000000000000000000000000000000000000000000000000046f2e508" - }, - { - "op": "delete", - "key": "0xff29e4e6bbd168fe5b46b7d34c34b628f95d54c4e75f9335a7e96282d1173b07dfd0f9925799d37553e0f34b2d7a49a135fc34a90b6fb99bfe70f2e43c28e75885e4" - }, - { - "op": "set", - "key": "0x000861030791246737d706c0d0017de01a26278bbe4ce8a134f5e0cd6f7eb69bcff9", - "value": "0x00000000000000000000000000000000000000000000000000000000ba9f1917" - }, - { - "op": "set", - "key": "0xff0ed33feae99b046218c56bec56750a98beb5fd6a51ec13218024cb2455586b43ce51c5b488a38762ddd335b3b0f645fefed9e6d3f01457ac62638b1dcc5e4e7644", - "value": "0x00000000000000000000000000000000000000000000000000000000c99448d2" - }, - { - "op": "set", - "key": "0xff0ed33feae99b046218c56bec56750a98beb5fd6a51ec13218024cb2455586b43b0ff3483ae67aaf30bffac5b9caf32340fc0265d2a99202b2d883fa94657351112", - "value": "0x000000000000000000000000000000000000000000000000000000008d8a0c92" - }, - { - "op": "set", - "key": "0x00e57860ba0adcdd1294caf74da8b990a80a8a6861bc01b30dfe07450d25bab38fb4", - "value": "0x000000000000000000000000000000000000000000000000000000001aff4bd6" - }, - { - "op": "set", - "key": "0x0018e25b9b05a714bd17ede1445ee7c785983a0194a3ed103783662bdc401dd7ff88", - "value": "0x00000000000000000000000000000000000000000000000000000000b3c0a241" - }, - { - "op": "delete", - "key": "0x00e2f41518226671332da4adddab1fdcb9903c7b895c7943b669067fe96b0baacf8c" - }, - { - "op": "delete", - "key": "0xff7b1eefd69ea014244937aaeaea4e8d4d296c04edd75ae47897e5612ef5a1a25254d51e5b726c0f3cfd1b03f07f25907b18767b99fef9eb727475858c458e1c7087" - }, - { - "op": "set", - "key": "0x00bded9122e6171a5dad9ce2aaebd933abf4718e9bf12448cf113064c9fd8e6ad0b9", - "value": "0x00000000000000000000000000000000000000000000000000000000bbef6005" - }, - { - "op": "set", - "key": "0xff34bb4f3468340d5f72275da92420aed94db48bbc61823e92f29f14f51590373a1c7f3420575f2956d219af851645b44537345b7c4f9199195b77d0edde2a379e4f", - "value": "0x0000000000000000000000000000000000000000000000000000000037d02280" - }, - { - "op": "set", - "key": "0x000ed33feae99b046218c56bec56750a98beb5fd6a51ec13218024cb2455586b435b", - "value": "0x0000000000000000000000000000000000000000000000000000000029fb46e0" - }, - { - "op": "delete", - "key": "0x00412c4d14e5bde6ea4c898a1d0f911f12a2cf775a62e3cc4c0f28331d432e8f103b" - }, - { - "op": "delete", - "key": "0x00d1afd6427b13f2ce7beb82a2cd6bbfc58eac8b2d46bc87bfbc78fbf440e18a635f" - }, - { - "op": "set", - "key": "0x00d1d141c1bed8bb4f809292e9340a0209cd167484a24a5ca993fcac4b97086d89af", - "value": "0x000000000000000000000000000000000000000000000000000000002b268a11" - }, - { - "op": "set", - "key": "0x004c1eb4996f3098c89ee461d61bd5f177587b0f4363ea73f4387e6296c66a260fba", - "value": "0x00000000000000000000000000000000000000000000000000000000c45a7015" - }, - { - "op": "delete", - "key": "0xff998f08d7054351e5fa3ccadda32ab8d6c0c86a825dd360d57c0fe6b46612297cc290a8b5ac7a453c9cec5724a8a85ad4002fb6dc9c73e86aedfa05133317c0402e" - }, - { - "op": "set", - "key": "0x00c901cfbb39e551189323bea0af6cf9e81008c90dce65cbc2ff4672e263d7bb87fe", - "value": "0x00000000000000000000000000000000000000000000000000000000203ca99f" - }, - { - "op": "set", - "key": "0x004b83f061efb2f4708114e46b5ef50a255528c28c80bbe5fc6ad88e892bd4c4ea77", - "value": "0x00000000000000000000000000000000000000000000000000000000c4e48d7d" - }, - { - "op": "set", - "key": "0x0015c4813bb5b1499e0a7e0d46daab8e10c70940c008c566d5c68557b1523d1f294f", - "value": "0x000000000000000000000000000000000000000000000000000000000374a3c5" - }, - { - "op": "set", - "key": "0xff15c4813bb5b1499e0a7e0d46daab8e10c70940c008c566d5c68557b1523d1f29d090338251613ee5dbd2ea5b763cb40885f3f40530835887585c70141b84e9572f", - "value": "0x00000000000000000000000000000000000000000000000000000000103e7f8b" - }, - { - "op": "set", - "key": "0x00e6b6d8d98be54d4c00cee378bedf964f755b5257bf1eabf52cf39aa987686fa704", - "value": "0x00000000000000000000000000000000000000000000000000000000c1eb2b88" - }, - { - "op": "set", - "key": "0xff55bbfb5b05f80543bd662a7b7a74980a2992612798087fa24e1fc86b8e8508e45ae026013e95f59126ce308964ea4e1d100de3fd7063ce06c5b1fcfd055b9dfa26", - "value": "0x00000000000000000000000000000000000000000000000000000000d5540993" - }, - { - "op": "set", - "key": "0xff2cebacb183e2f51a3b6cb9f7fe2bd11c399bf1bdb8ce95ed457d3e46190197a0a0a0268860a87b73c5d023e0181059e1d49f8ed387a132e90d5ccf7b7589f952c4", - "value": "0x000000000000000000000000000000000000000000000000000000001dade8e2" - }, - { - "op": "delete", - "key": "0x00c901cfbb39e551189323bea0af6cf9e81008c90dce65cbc2ff4672e263d7bb87f3" - }, - { - "op": "set", - "key": "0x002cebacb183e2f51a3b6cb9f7fe2bd11c399bf1bdb8ce95ed457d3e46190197a0de", - "value": "0x000000000000000000000000000000000000000000000000000000001b88b0bb" - }, - { - "op": "delete", - "key": "0x00bded9122e6171a5dad9ce2aaebd933abf4718e9bf12448cf113064c9fd8e6ad0b9" - }, - { - "op": "set", - "key": "0xffa43fb2d7beb12b34786a837467f62b7d6c5dab4ab7eecc89c0b67db76a6829272094bf023796a3d4bdc16643cddf5395ab048ba346a4f50f1cc64da78ad7dd528e", - "value": "0x000000000000000000000000000000000000000000000000000000003043f2e3" - }, - { - "op": "set", - "key": "0xffec64b2de1fc1d53b795f49a1c84ca4172bc3292472c3e244e4f8c225fa786f56d6f2c814b54068196e618750cac42223b5b384269904d6bc65960c2e623f4ee844", - "value": "0x000000000000000000000000000000000000000000000000000000009ac18f7e" - }, - { - "op": "set", - "key": "0xff1df77729bb76b139342cfc86aff011590bf6bb810f47e69a6babac30af96f9f5451673405763851d69ce90fb56d8dd98d3ef3ad6e6deccec4a6cc4303c046063a5", - "value": "0x000000000000000000000000000000000000000000000000000000005b7ee846" - }, - { - "op": "set", - "key": "0xff78a2812688114caf4452a5611322977e4b1000e59ef408d4c8e2c90028370bcbee4f7f4b500980d1dbb987a5b7dea34a042fca8da2a3671e71d626da6e5b381555", - "value": "0x00000000000000000000000000000000000000000000000000000000c844b5ba" - }, - { - "op": "set", - "key": "0x0046aeb3243f905d4e0eac7f60ee8962848c87e79dfbd019ece14fe45bd720ea6b72", - "value": "0x00000000000000000000000000000000000000000000000000000000cf548087" - }, - { - "op": "set", - "key": "0x00e57860ba0adcdd1294caf74da8b990a80a8a6861bc01b30dfe07450d25bab38f00", - "value": "0x00000000000000000000000000000000000000000000000000000000991126ef" - }, - { - "op": "set", - "key": "0x004b83f061efb2f4708114e46b5ef50a255528c28c80bbe5fc6ad88e892bd4c4ea9d", - "value": "0x000000000000000000000000000000000000000000000000000000000e56e2bc" - }, - { - "op": "set", - "key": "0xff9c847c439d6431d36ddeb326cb553da2e967778a24afbd1d23a62412186faf01d11c941f9715afff547cb69563e69a920e6d5098967069e5002620d5124791d805", - "value": "0x00000000000000000000000000000000000000000000000000000000ed64d79b" - }, - { - "op": "delete", - "key": "0x0046aeb3243f905d4e0eac7f60ee8962848c87e79dfbd019ece14fe45bd720ea6b72" - }, - { - "op": "delete", - "key": "0x0078a2812688114caf4452a5611322977e4b1000e59ef408d4c8e2c90028370bcb82" - }, - { - "op": "set", - "key": "0x0055bbfb5b05f80543bd662a7b7a74980a2992612798087fa24e1fc86b8e8508e487", - "value": "0x00000000000000000000000000000000000000000000000000000000e0f0c857" - }, - { - "op": "set", - "key": "0xff55a6923f7414ba482ae8a64b407f386a0bb0491b742bd4b22b5b4b44fca84e8455b7a5e4231b6e2b3ab80bf426d0ac93576b707e774c56731ed615e56edbb33a8d", - "value": "0x000000000000000000000000000000000000000000000000000000009ccf69a7" - }, - { - "op": "set", - "key": "0xffe6b6d8d98be54d4c00cee378bedf964f755b5257bf1eabf52cf39aa987686fa7ebf533e3e385b221b921f03b253af4ca27802584fa9d4ed08843417da394befa00", - "value": "0x00000000000000000000000000000000000000000000000000000000fd5052c1" - }, - { - "op": "set", - "key": "0xff46aeb3243f905d4e0eac7f60ee8962848c87e79dfbd019ece14fe45bd720ea6bc7d37b238b059b3f39fb34f03f189fc8c596ad3bc45f7231d1a8e723510074013d", - "value": "0x00000000000000000000000000000000000000000000000000000000943e0cfc" - }, - { - "op": "delete", - "key": "0x00e6b6d8d98be54d4c00cee378bedf964f755b5257bf1eabf52cf39aa987686fa73d" - }, - { - "op": "set", - "key": "0xff5ac76a8f23131e4ca988c8afcc313130cc95e521d2691635fc26196d63e66a1f893d3c65a10fbfddef979c778efdeaf625c4d5326e417354a396e996bacabea596", - "value": "0x00000000000000000000000000000000000000000000000000000000d7875175" - }, - { - "op": "set", - "key": "0x0046aeb3243f905d4e0eac7f60ee8962848c87e79dfbd019ece14fe45bd720ea6bb8", - "value": "0x000000000000000000000000000000000000000000000000000000001d328761" - }, - { - "op": "set", - "key": "0xffd1afd6427b13f2ce7beb82a2cd6bbfc58eac8b2d46bc87bfbc78fbf440e18a63cfc13d302b5e8c3d7f15057e1676e80d1556d030824a2893354a8c8c083bb9173b", - "value": "0x00000000000000000000000000000000000000000000000000000000de9dcb28" - }, - { - "op": "delete", - "key": "0xff55a6923f7414ba482ae8a64b407f386a0bb0491b742bd4b22b5b4b44fca84e8455b7a5e4231b6e2b3ab80bf426d0ac93576b707e774c56731ed615e56edbb33a8d" - }, - { - "op": "set", - "key": "0x004c3a8541d9ec369a9fd4bcdad885cc3b166da70ed6a83066924727d9ff147fceeb", - "value": "0x00000000000000000000000000000000000000000000000000000000802e3a74" - }, - { - "op": "set", - "key": "0xff3cbfd03e3f083b0987f841c4372031faf655a63472518a3613e2e30ecb542b5111589c57f4d8ad55d2c4ece4c6b63961c45be7479bfd08d04fab2d26d0acb7a07e", - "value": "0x00000000000000000000000000000000000000000000000000000000ec6c633a" - } - ], - "roots_after": [ - "0xdd16f2b8e1c012d8ed988df1cd9e31fc06bceee33304ec90fdee4c9e0d6f2522", - "0x0000000000000000000000000000000000000000000000000000000000000000", - "0xe7c887160ebfbf178105a04157a5b67d669ac6899fbf632526152fc5cb82cd80", - "0xd77cc71cfc52b7a272f47bb93cf6e3b5ae4a2d7fc991e4e2b76586018620570b", - "0xfb180e479837451381e4ebbf7a21be6937dbab9b830eeb798cf1854b48bc82ad", - "0x663475566fe0b6afa6fb71c73802d98b37948ccc16237229d8722a66b31c48f0", - "0xe26dac98c3f044695b7bda1e7bab4e9e0422d41b367b3fb873d50734d15f8de8", - "0x90263ca13708230b2ae2d6413b76995a7c177ed239310a9628fe4aac1e2d51fe", - "0xd7893e542d99ae8c18bd7a495190a58e2e1e25252d4fa9378ef2694db0e751ee", - "0x4df5042f26d44caf9d655bda9bb46284498628761474d302392daff333e5b610", - "0x1113ff21db5505e243f390d5df0fc11549690813ff7a25e8bc60b5209d39ed72", - "0xe14995d380c1552d2901cacc440b51fed58732d5eb6b6adb202e891f3f53b6d6", - "0x5d3fd815f8282caf95aab8105212fb98b6f5516beb9fba2ed4f4ee502b85ed74", - "0x09b380d8286220693a4a9e4b85e074ea94cf0dadbccacd05d6f8dc5470438087", - "0x957d9940952d058f2523c52603f8cc27537ee0e40b8c452d9691e28ff0f9d782", - "0x7cc8b1cbe418de642947381e3fd9251730dff6165bb7d48de4388cce893d8aea", - "0x5ea579760671e854abc2adfcb037bd6f1470c26e5f4ba6e9392d05e1af830b6d", - "0xb4281adb36825d1f7dd6b7f0536a81ad9950fb727124828f0a94a79c32885052", - "0xa90bdbe39f8833c02f914c2ca37a5d5b91be83ba6a4721923b1ffe8899c40d9e", - "0x8c3fff51ed5749c6147c9bb35b5ac0f20f0f0ff92604b0d7cd0f83bcaecf52b4", - "0xd9d49e4766df0f28b1f78bdbfbc5a2adc12210b96a31e940ba2199c0e66d6810", - "0xa65ed667638a312e9020a833f124c44271d24a3f169318feec746a9c60f859b3", - "0xdaa6ca892827647f4a980b5f506b586baca1e081c8b16fabee12f1f39f192810", - "0x7fe58a68430e077c8ed6e8db50b193f1dd1bcc7ec9868dab5e1af465772eab4d", - "0xda0cd545154385e98833c492823a54a3da062dd2cf63d83edb5cec020769bbfd", - "0x047d9f5bae78aef591535881ecb07ce1a67c6696f5cb25f2dd758d9e24c98304", - "0x0e2ec849aff3ee826b994f897ee5804340a0201019ee1c194b4d1d1e29ea5a47", - "0x36a917bd38446c14d6e5fcce57d12668e3b599eb96cd81b5311e3ef66d369728", - "0x3d0b731a242eef052000febd10cb32e67b9b4332aac81fc3e55f311d5b7bc472", - "0xa1a8a8809091387195b843815e469d148bfa6759c6ed5cb4108c8927959a9880", - "0xebdbaafdd80533829f04616c168dbc66673b4300b541575ecd1fe8db6ea57e18", - "0x94787be76bedcd4bc27a64efbdde1b661081c2e968c007fc5279ed727147e3e7", - "0xd993fb38d1d908f6de3817cb3379958f14ab89b1667fb3a7c78a94d84f940400", - "0xe8d0c2564928aafabff90160758a2585374eee8ecc1d95d5ea14f4bf7e5d5b40", - "0xb4d5bd5daec9b48b19a8f1dc669261a2bf2cec5f1e7943c3929e32115488bcbb", - "0x83a8addb264fc499155e6e4e24596f9ac2ed2cec6566ef37a1f12801a630bc16", - "0xbbac6afa6e7fbd3aa6a1023df32c0b5d33ce8e47f5d1886c84ede5a344c96f4c", - "0x4a8765a66b4ca5d413ec1804806aada4f1b599edca258e9592274022c8e04f79", - "0x2a775af5374764ec81b822547c0ff214ff5f311cae5ef348a0e58079a5c9c293", - "0xb25f2df22a29746d74c445e565937e0799e4f7c85d7f5501a42379fd1d7e2ab6", - "0x8ba3e472d63de597bf2b998a6cb0ecf658ab5975b16f227d763ee58b323091c9", - "0x3537f27c415564b1e091a2dae2f4dda6fb9d5bc7db8fdb8acc19b0d281255f77", - "0x5d3400b400ea8664fd7166a8823fd9ea5975da18949c3f01116f363177f7e558", - "0x2f355dca787be5cca0dc1453de5180af9646566353147c433b491a8ceff10d29", - "0xb578ea4fc20249141685b16bc86622f14bbefea626a3042d3963e0aacc388168", - "0x604344fdb408c344f12b6f3ae45cc40ff2796e97a77194c0236d9e758e4ad2a8", - "0x2578864f41294e06a3ee1d786b7522c2d58dad6fa06037dfa2ccdf793b417b0e", - "0x2d71d6815fbc8190739d87357b5b46f591ed488ab64e6e85674eab95010a4fa6", - "0xc24244541fc2ad742ac6ffe8d4de3313ce280f99c1ae2652c5039e9f39ec62d4", - "0x81385d7fecb9e98d72f5fee87afea8db4859279771f123503064109e30255045", - "0x517f7cb7c5a872e0bbd8e78a3700d39e7e8f8b4045148ddc1e8adeaed4ef31b2", - "0x5f54783d181ede5fa7be7831ed8a1e638bcc9b86c7c976fe1df7894f5e20c533", - "0x1195b09383681ca4d154c9471e97c365b57d0008652d83b8a3451a6314646b96", - "0x194dad20176a4cec03b075fac850504e2ac8fa72803c4eda51e69421190c9b95", - "0x759f81d2c0a37955c1e88f66befd2a5f223a93a22ac7802e77b3f11e5c88f3ff", - "0x75749205d848f106433cc8dfef8677d3ddf49084868d6899fd0c503f2e90a3bc", - "0xf0ba3ab2546cef4f649d643f219bb18253b322793db4d9a9c2f8b6b80b103fb4", - "0x7605cfa9b033e38d1d34456d0378123d5352ace0aeadd602bb8eb845149b6ad3", - "0x4e27fae9d4541f254407e92516d8fd8fa21dd56ee10dc1eb044b51343d8ec706", - "0x792ee7cc1a2e2b89581a1dd7f197a704c8e19098ffa813fcbe477f95f1c226af", - "0xd4c3771bc7dd4808f143bf5578e18121f0c81997fef4b2f45dd24d772d64bbad", - "0x2e29adeb0f6362bc1b332c53cf773ebe430849b993312819b4d977ade8bfa151", - "0xc1003ac3d52752efdb4b469d6c09a737658e26b15903f0b5439c9f98b1e5088f", - "0x4ca9c3dbfc854dc81935fbbe9116bd83517fa3262c7664cd4bff73323258746f", - "0xda8fa4114a9f82cf873ac14c6105aa652cb5fb9534b315edd15fa765aa2df5e6", - "0xf600fccde574311940532aad1305ea7a74f4199744632ace534f29b1fc415733", - "0x9ffe5a5f256ad89b96ec8f8163dbd654e431d382aea21cbd60e54428218a22d1", - "0x0a5da34634a379ce4f936cc85fda7ea26442c1f4bd9120d14a377f055571bddf", - "0xa3cda68f6d3c8f85a9e960e49a5ba5796ba02211140a7c558002ef7977f29efa", - "0x7a42d5dab0d1343afb5cecd47384eab0a292d640d9413b1f3bbdd85003452615", - "0xa633e7f717423349cc4dcacd3c8b9fecd9b7668e9a38d3b1f7b34fe373c9033c", - "0xfd76c56f441715c5144b20ace1c2bef2754b2c6a6afde6358c47e9897003c2d9", - "0x9e2a00917cb42a69d568ed7aa397d2a08e169f913119a4a38a4d72fb94db66cc", - "0x96bc7565f138a55bbccc3ce2e4f1814b107fd95fcd7a1382b9692ca5861edb32", - "0x0d1d83d02a9f1ffa44a6c7dca6165f11076eb4ce3415ccc2a01172f5db455f17", - "0x3d7540bfd48bafe57e6b31a969a21df950905c5509837fd0bc249ccc978008aa", - "0xb5d4733f38c70b18531cb18361a55b9adc5f162150b03aaf2a795c0e3b9323fb", - "0x9bb7ea59c81e23f335d912e26b40a573b82ad13d26e512dd3bdf2234bbc11480", - "0x71190b7b68a9169279213e0d02c157564aed464cc9063f175c8780ce79e99136", - "0xc5d0ba2d5e453c949d26003cf656efe1dc904973db3e5d6c7a63009871083d93", - "0xb5f3f98c140e84b13751dc582334187536b9c57b2a86c79dac48568e10e23b12", - "0x495cdb7c8de836a1a46f9619d9755a9ab28cbb22149abb3ee52431b20fea3425", - "0xc00a3dc5cccf04ed31311515539e0f7cd3f7e852217496c1c13c36de7ea61287", - "0x29dfd41e74eb695ea128b1c3b3fd25610da939d255b10f0e0eb32b19684ac0ba", - "0x40ecae6715cda9f5759160f088ad15d164f678064cb289549d347d505a2b2ed5", - "0x1fddae87aace1d5b183dc019c976b6a1813d146eae8016059f89a5725ed95ba2", - "0x485bfc7ed0acc02e45f0e04450653e7788394ee126834f9e3d485c3cf684cec4", - "0xeb624656580f026104a20b69bf78e6dfcc774055c28c549e9cb8243fa515893f", - "0x1bf7a3151de44574b5404a08e55abb120a6fdbc7406a54a2cdb4d7901a94fe7f", - "0xb527d21e47385ca2142b2621be656f97854f528541fd706c8ee1135dbd06e386", - "0xdc4b0385e67f81daad986e2d49ccf07f8d6c88e1401283dc27c382ed79692ba8", - "0x752261197c970c67bd0b4566e9da57916afed725610ab287ed4c6f6d5210260c", - "0x61f2e6383008b28722612e215b1ed77bfa533803d787c16a373e5973e4ca5faa", - "0x20e918b0baab5e6dadc9963ecb3dc76c43ffe2a26e448f4b3f47a4e8b29ba00d", - "0x31520401bd25f84b0259bc99cf8f89d7d9367a095ef91e5573f423fa427295e3", - "0xcc5dc730bd5e6e6724e3ff334bfb6cb6bbb88b0a310d9c3fbc588a20bd8fb92c", - "0x1a3980997f6379724775e9767a21f8624a2b78a6ffe97524d1ff68c78100f249", - "0x846b4829e8e13667fce2bade085cabf4dcb0c218b22d8113714ec485cc1fd505", - "0x13f1834a6327901509d59706173183c7eb14edf3073bfd00526226e9f786bac6", - "0xa278c52b411b868367a65491fbf920efa9bd927104e8a695abc1a607288667d6", - "0xea5c739baa8f710c47cc1e61144b7023c88aa089d310cd4de9cdce5dc0e8b780", - "0x4f20c6919d878ac275c79bafaee073b3024d975e4ecf3f65108a0467242138eb", - "0x5bbbb454fe9c9318af76bfd5af240e1bf3b48e4e05d84941a3aae8463dbf9cce", - "0xec3d24a6265012cbe5f48e43908defce13b719ca0ef059ec9e6a7c924afa28d9", - "0x0b7b16844e2e38c2268f8d2c0ef7bbf5f0308b745e78d665806b3d29d80c8f81", - "0xde6ff0c5c22ca1fa14afef7c36452d0b5ac37aa8e010ea67553cd8bb1d14be5d", - "0x8996181b1df4c1e066b822eb778f16120748ed2abcde43ef4bdbc410948fc8b4", - "0xdc6d16497c9c1db1edf7b2880413613de1ff9103af4f23c3574dcc57cf332085", - "0x6e6eca38cc5687d043bb80e34d8a540cb3e45cf2b14d47527dc5f39524632226", - "0x41fd1da147aa5932caa08f342d7445d491b0dcd6144bf2691003f11e5425a40a", - "0xf05a71d105eb642fe786c8be04b0c9c2e0195707104b262aa785a78e1f56c4bf", - "0xb98718d5012439008639b7de49fd0bd4ad76f97db2d89c3fccb43aae87e9f1ff", - "0xc99a13119f7c1bc3ea434c84971ce4c4317eb3075517e8df6b2565098c47312d", - "0x68fc3dc2b7232f49e642210deda26dd9d7a1b6c2138476f25abe93bf96bf73a9", - "0xbbc50e576b671232dec276051e26f3882fa4f6062a16354f61c518b7577f5835", - "0x2dfdc171bc11ac1e25d9f71c3e50aa005878d48dc19fffdd9d0d0d333c2c5425", - "0x65b52b3633be0666459906c62baf06fce506236e188bf6005123616d82095f47", - "0xb149c9df31a9fc861878e74102ba3ad83b0e451631737e6a74da8ec49f9548d3", - "0xe5c7847bb607a5ca7070b7e243fbbbb49c95b1cad8e95dfab7bb7a942cd3af66", - "0xc911c60ebf20b097aa3ea07699669d7365005ff0a215aec20d4b814789a283cd" - ] - }, - { - "seed": 90210, - "ops": [ - { - "op": "set", - "key": "0xffd1afd6427b13f2ce7beb82a2cd6bbfc58eac8b2d46bc87bfbc78fbf440e18a63aeb0aca39c8f6ba440835a8e371b8b2719dec5b24a57f1da0e7519c3cd74beff52", - "value": "0x00000000000000000000000000000000000000000000000000000000cec06895" - }, - { - "op": "set", - "key": "0x00e61994d31ceae39cae5feb02303eb90e0d4ae90f5b0755c3c26adc81f360ec8587", - "value": "0x0000000000000000000000000000000000000000000000000000000026a125de" - }, - { - "op": "delete", - "key": "0x00e61994d31ceae39cae5feb02303eb90e0d4ae90f5b0755c3c26adc81f360ec8587" - }, - { - "op": "delete", - "key": "0xffd1afd6427b13f2ce7beb82a2cd6bbfc58eac8b2d46bc87bfbc78fbf440e18a63aeb0aca39c8f6ba440835a8e371b8b2719dec5b24a57f1da0e7519c3cd74beff52" - }, - { - "op": "set", - "key": "0x00c901cfbb39e551189323bea0af6cf9e81008c90dce65cbc2ff4672e263d7bb871a", - "value": "0x0000000000000000000000000000000000000000000000000000000038f9aacc" - }, - { - "op": "set", - "key": "0x005963897639cd11d686b199366a1249d76bc89e0f7161c9eca48d9a199435952d0f", - "value": "0x0000000000000000000000000000000000000000000000000000000053b3bca6" - }, - { - "op": "set", - "key": "0x0068e867f5f899fc01d641a00cea2c42d58f3cc061797135198a85301d1e290306c3", - "value": "0x0000000000000000000000000000000000000000000000000000000058e273d9" - }, - { - "op": "set", - "key": "0x00e61994d31ceae39cae5feb02303eb90e0d4ae90f5b0755c3c26adc81f360ec85b3", - "value": "0x000000000000000000000000000000000000000000000000000000008debe84f" - }, - { - "op": "delete", - "key": "0x00c901cfbb39e551189323bea0af6cf9e81008c90dce65cbc2ff4672e263d7bb871a" - }, - { - "op": "set", - "key": "0x0055bbfb5b05f80543bd662a7b7a74980a2992612798087fa24e1fc86b8e8508e4b2", - "value": "0x0000000000000000000000000000000000000000000000000000000052fbeee9" - }, - { - "op": "set", - "key": "0xff68e867f5f899fc01d641a00cea2c42d58f3cc061797135198a85301d1e290306efdc84373704529ffcbc3d4ed318de0ec2cdf5f61dd800c143f44a741af2f838a0", - "value": "0x0000000000000000000000000000000000000000000000000000000012acb6e5" - }, - { - "op": "set", - "key": "0xffe60d91d1d11837912311124089c6b73c9d726c99f4df797eb48c2b7fec6e5e2c3b9034b692f2597d8c9c9ad6921ecd112d2f69cc50a6e065034e2ace540e8928ab", - "value": "0x0000000000000000000000000000000000000000000000000000000088a67fe9" - }, - { - "op": "set", - "key": "0xffe60d91d1d11837912311124089c6b73c9d726c99f4df797eb48c2b7fec6e5e2c9c921b846417391ff7e9525580b53876b30e3f6cf563e5849525f6fa88bf230f1a", - "value": "0x0000000000000000000000000000000000000000000000000000000075b67af1" - }, - { - "op": "set", - "key": "0xff78a2812688114caf4452a5611322977e4b1000e59ef408d4c8e2c90028370bcb279b454b0f4a6026943322996543a3cbb6c41d7c467067d9bc010242feef3c2c4b", - "value": "0x00000000000000000000000000000000000000000000000000000000fd6d065d" - }, - { - "op": "set", - "key": "0x00bded9122e6171a5dad9ce2aaebd933abf4718e9bf12448cf113064c9fd8e6ad0a4", - "value": "0x000000000000000000000000000000000000000000000000000000001c92d573" - }, - { - "op": "delete", - "key": "0xffe60d91d1d11837912311124089c6b73c9d726c99f4df797eb48c2b7fec6e5e2c9c921b846417391ff7e9525580b53876b30e3f6cf563e5849525f6fa88bf230f1a" - }, - { - "op": "set", - "key": "0x003cbfd03e3f083b0987f841c4372031faf655a63472518a3613e2e30ecb542b514a", - "value": "0x00000000000000000000000000000000000000000000000000000000617ad32c" - }, - { - "op": "set", - "key": "0xffec64b2de1fc1d53b795f49a1c84ca4172bc3292472c3e244e4f8c225fa786f56b7bce92ed9ae70f9ca2c10d21fe857f18cd2b64d54c70193d8a0dd772967c2580d", - "value": "0x00000000000000000000000000000000000000000000000000000000565e29f9" - }, - { - "op": "delete", - "key": "0xff78a2812688114caf4452a5611322977e4b1000e59ef408d4c8e2c90028370bcb279b454b0f4a6026943322996543a3cbb6c41d7c467067d9bc010242feef3c2c4b" - }, - { - "op": "set", - "key": "0xffd1afd6427b13f2ce7beb82a2cd6bbfc58eac8b2d46bc87bfbc78fbf440e18a635bd91a1bb3fea09cd07c1f6f0b5b0928ec465c3d5409c3dc4d9f3b384fd276b0e3", - "value": "0x00000000000000000000000000000000000000000000000000000000f03eb650" - }, - { - "op": "delete", - "key": "0x00bded9122e6171a5dad9ce2aaebd933abf4718e9bf12448cf113064c9fd8e6ad0a4" - }, - { - "op": "set", - "key": "0xffc901cfbb39e551189323bea0af6cf9e81008c90dce65cbc2ff4672e263d7bb871b0daa3888412359e9769bc566073558cc37b927578bcc74743311f2e7487706f3", - "value": "0x000000000000000000000000000000000000000000000000000000004fe9739b" - }, - { - "op": "delete", - "key": "0x0068e867f5f899fc01d641a00cea2c42d58f3cc061797135198a85301d1e290306c3" - }, - { - "op": "set", - "key": "0x003cbfd03e3f083b0987f841c4372031faf655a63472518a3613e2e30ecb542b5188", - "value": "0x00000000000000000000000000000000000000000000000000000000cb3d4cf3" - }, - { - "op": "set", - "key": "0x0046aeb3243f905d4e0eac7f60ee8962848c87e79dfbd019ece14fe45bd720ea6bf1", - "value": "0x0000000000000000000000000000000000000000000000000000000031d31f66" - }, - { - "op": "set", - "key": "0x0015c4813bb5b1499e0a7e0d46daab8e10c70940c008c566d5c68557b1523d1f2974", - "value": "0x000000000000000000000000000000000000000000000000000000007d5ea0f1" - }, - { - "op": "set", - "key": "0xff29e4e6bbd168fe5b46b7d34c34b628f95d54c4e75f9335a7e96282d1173b07dfd0f9925799d37553e0f34b2d7a49a135fc34a90b6fb99bfe70f2e43c28e7588553", - "value": "0x00000000000000000000000000000000000000000000000000000000a389ad08" - }, - { - "op": "set", - "key": "0xff5ac76a8f23131e4ca988c8afcc313130cc95e521d2691635fc26196d63e66a1f3e2bba64771273ebc79252548c60f34e55d39cd7d0084b6e45722cab3f7021e333", - "value": "0x000000000000000000000000000000000000000000000000000000009ffa1deb" - }, - { - "op": "delete", - "key": "0xff29e4e6bbd168fe5b46b7d34c34b628f95d54c4e75f9335a7e96282d1173b07dfd0f9925799d37553e0f34b2d7a49a135fc34a90b6fb99bfe70f2e43c28e7588553" - }, - { - "op": "set", - "key": "0xff5ac76a8f23131e4ca988c8afcc313130cc95e521d2691635fc26196d63e66a1f1dc0339709def5b953c8f87f8f5316c6ea67241e73291c53d9d020d11b9f19e976", - "value": "0x000000000000000000000000000000000000000000000000000000003a91fad5" - }, - { - "op": "set", - "key": "0x00c28c21982a56a6cecf595bfeb78297c7432abfb487cc75c09db2856a118f441d10", - "value": "0x00000000000000000000000000000000000000000000000000000000112c6343" - }, - { - "op": "set", - "key": "0x00998f08d7054351e5fa3ccadda32ab8d6c0c86a825dd360d57c0fe6b46612297c62", - "value": "0x000000000000000000000000000000000000000000000000000000003b456d06" - }, - { - "op": "delete", - "key": "0xff5ac76a8f23131e4ca988c8afcc313130cc95e521d2691635fc26196d63e66a1f3e2bba64771273ebc79252548c60f34e55d39cd7d0084b6e45722cab3f7021e333" - }, - { - "op": "set", - "key": "0xffe60d91d1d11837912311124089c6b73c9d726c99f4df797eb48c2b7fec6e5e2c8ff835410b84cea50f38a8d78746de71c5e0179f2354107cbabfefdd7bdfc0e1bb", - "value": "0x000000000000000000000000000000000000000000000000000000007cf1b2dd" - }, - { - "op": "set", - "key": "0xff5ac76a8f23131e4ca988c8afcc313130cc95e521d2691635fc26196d63e66a1f3e2bba64771273ebc79252548c60f34e55d39cd7d0084b6e45722cab3f7021e337", - "value": "0x0000000000000000000000000000000000000000000000000000000097519cec" - }, - { - "op": "set", - "key": "0x002eb90959edcd0b6a8a7922b8b8d37cd9664001c217b4c8e6d2df4b8ede329a71ac", - "value": "0x00000000000000000000000000000000000000000000000000000000243042d6" - }, - { - "op": "set", - "key": "0xff543907253acb230f5dc96bbafd8ea0f37dd63b30e8a4a3675bb007673a31fbc5985ffe04dbd5489766385a2bbb382923670eb0e2bb3aaf46231bb53efc33320431", - "value": "0x000000000000000000000000000000000000000000000000000000001a9aedd3" - }, - { - "op": "set", - "key": "0x0015c4813bb5b1499e0a7e0d46daab8e10c70940c008c566d5c68557b1523d1f2946", - "value": "0x000000000000000000000000000000000000000000000000000000001c19f56c" - }, - { - "op": "set", - "key": "0x0046aeb3243f905d4e0eac7f60ee8962848c87e79dfbd019ece14fe45bd720ea6ba3", - "value": "0x00000000000000000000000000000000000000000000000000000000b6c4df3f" - }, - { - "op": "delete", - "key": "0x00c28c21982a56a6cecf595bfeb78297c7432abfb487cc75c09db2856a118f441d10" - }, - { - "op": "set", - "key": "0xff2eb90959edcd0b6a8a7922b8b8d37cd9664001c217b4c8e6d2df4b8ede329a71806e6656ad617b45d8e28cb6bd39e7244735a104a3ba4dd21833624b3c7afc179e", - "value": "0x0000000000000000000000000000000000000000000000000000000040a78667" - }, - { - "op": "delete", - "key": "0x00e61994d31ceae39cae5feb02303eb90e0d4ae90f5b0755c3c26adc81f360ec85b3" - }, - { - "op": "delete", - "key": "0x0015c4813bb5b1499e0a7e0d46daab8e10c70940c008c566d5c68557b1523d1f2946" - }, - { - "op": "set", - "key": "0x0034bb4f3468340d5f72275da92420aed94db48bbc61823e92f29f14f51590373acc", - "value": "0x00000000000000000000000000000000000000000000000000000000a3a1004d" - }, - { - "op": "set", - "key": "0x00543907253acb230f5dc96bbafd8ea0f37dd63b30e8a4a3675bb007673a31fbc5ac", - "value": "0x00000000000000000000000000000000000000000000000000000000623bdd27" - }, - { - "op": "delete", - "key": "0xff5ac76a8f23131e4ca988c8afcc313130cc95e521d2691635fc26196d63e66a1f1dc0339709def5b953c8f87f8f5316c6ea67241e73291c53d9d020d11b9f19e976" - }, - { - "op": "set", - "key": "0x009c847c439d6431d36ddeb326cb553da2e967778a24afbd1d23a62412186faf01a4", - "value": "0x0000000000000000000000000000000000000000000000000000000072373c82" - }, - { - "op": "set", - "key": "0x001df77729bb76b139342cfc86aff011590bf6bb810f47e69a6babac30af96f9f5e6", - "value": "0x000000000000000000000000000000000000000000000000000000003ad8ea87" - }, - { - "op": "set", - "key": "0xffd1afd6427b13f2ce7beb82a2cd6bbfc58eac8b2d46bc87bfbc78fbf440e18a63f1c284d88e488ae8ee1cab2420e5f5c90b4e243c1a8ed32228b22b4974400d5982", - "value": "0x0000000000000000000000000000000000000000000000000000000098b77420" - }, - { - "op": "set", - "key": "0xff29e4e6bbd168fe5b46b7d34c34b628f95d54c4e75f9335a7e96282d1173b07df754b0f807e1dc46306d4cb12e85e18d008678fa80cd9e6fa118c34e179bd412a60", - "value": "0x0000000000000000000000000000000000000000000000000000000067da7aa1" - }, - { - "op": "delete", - "key": "0xffd1afd6427b13f2ce7beb82a2cd6bbfc58eac8b2d46bc87bfbc78fbf440e18a63f1c284d88e488ae8ee1cab2420e5f5c90b4e243c1a8ed32228b22b4974400d5982" - }, - { - "op": "set", - "key": "0x00c901cfbb39e551189323bea0af6cf9e81008c90dce65cbc2ff4672e263d7bb875e", - "value": "0x000000000000000000000000000000000000000000000000000000004360379b" - }, - { - "op": "set", - "key": "0x004c3a8541d9ec369a9fd4bcdad885cc3b166da70ed6a83066924727d9ff147fce2a", - "value": "0x0000000000000000000000000000000000000000000000000000000088bedbe4" - }, - { - "op": "set", - "key": "0xff34bb4f3468340d5f72275da92420aed94db48bbc61823e92f29f14f51590373aef2032e9c5c80ba9048f874aaea79ab7ce9e0f910b0e98955e60542e3a7f44645e", - "value": "0x00000000000000000000000000000000000000000000000000000000951aef03" - }, - { - "op": "delete", - "key": "0xffe60d91d1d11837912311124089c6b73c9d726c99f4df797eb48c2b7fec6e5e2c3b9034b692f2597d8c9c9ad6921ecd112d2f69cc50a6e065034e2ace540e8928ab" - }, - { - "op": "delete", - "key": "0xff543907253acb230f5dc96bbafd8ea0f37dd63b30e8a4a3675bb007673a31fbc5985ffe04dbd5489766385a2bbb382923670eb0e2bb3aaf46231bb53efc33320431" - }, - { - "op": "set", - "key": "0xff15c4813bb5b1499e0a7e0d46daab8e10c70940c008c566d5c68557b1523d1f29cac055e3c06fef0ce191be15524a246c11d1103158d4f8b49d31c629e5520053ce", - "value": "0x00000000000000000000000000000000000000000000000000000000f4c28a25" - }, - { - "op": "delete", - "key": "0x0034bb4f3468340d5f72275da92420aed94db48bbc61823e92f29f14f51590373acc" - }, - { - "op": "set", - "key": "0x0029e4e6bbd168fe5b46b7d34c34b628f95d54c4e75f9335a7e96282d1173b07dfae", - "value": "0x000000000000000000000000000000000000000000000000000000005085c081" - }, - { - "op": "set", - "key": "0xff0861030791246737d706c0d0017de01a26278bbe4ce8a134f5e0cd6f7eb69bcfbf2761d1a4e27ad313f45a816d320676aa7ee1cb19958f32f02c32fa7ed16c3753", - "value": "0x00000000000000000000000000000000000000000000000000000000297c61be" - }, - { - "op": "delete", - "key": "0x005963897639cd11d686b199366a1249d76bc89e0f7161c9eca48d9a199435952d0f" - }, - { - "op": "delete", - "key": "0xffd1afd6427b13f2ce7beb82a2cd6bbfc58eac8b2d46bc87bfbc78fbf440e18a635bd91a1bb3fea09cd07c1f6f0b5b0928ec465c3d5409c3dc4d9f3b384fd276b0e3" - }, - { - "op": "set", - "key": "0x0034bb4f3468340d5f72275da92420aed94db48bbc61823e92f29f14f51590373a69", - "value": "0x0000000000000000000000000000000000000000000000000000000084f5ccf0" - }, - { - "op": "set", - "key": "0xff15c4813bb5b1499e0a7e0d46daab8e10c70940c008c566d5c68557b1523d1f294495e8d51dcafaa894ea80097c7ad006d6b75b9eb85fad2698d1e9e7026e25924c", - "value": "0x00000000000000000000000000000000000000000000000000000000382c3c12" - }, - { - "op": "set", - "key": "0xff18e25b9b05a714bd17ede1445ee7c785983a0194a3ed103783662bdc401dd7ff2fb776a093982841cfb2c6c0a1fca9bc72a0ac907d132b7e3c6ac33b402bfba00f", - "value": "0x0000000000000000000000000000000000000000000000000000000056888c29" - }, - { - "op": "set", - "key": "0x00e61994d31ceae39cae5feb02303eb90e0d4ae90f5b0755c3c26adc81f360ec85b0", - "value": "0x00000000000000000000000000000000000000000000000000000000e1920236" - }, - { - "op": "set", - "key": "0x00998f08d7054351e5fa3ccadda32ab8d6c0c86a825dd360d57c0fe6b46612297cce", - "value": "0x0000000000000000000000000000000000000000000000000000000012f2403d" - }, - { - "op": "set", - "key": "0xff2eb90959edcd0b6a8a7922b8b8d37cd9664001c217b4c8e6d2df4b8ede329a71806e6656ad617b45d8e28cb6bd39e7244735a104a3ba4dd21833624b3c7afc17c3", - "value": "0x00000000000000000000000000000000000000000000000000000000988e6a70" - }, - { - "op": "set", - "key": "0x0055a6923f7414ba482ae8a64b407f386a0bb0491b742bd4b22b5b4b44fca84e8420", - "value": "0x00000000000000000000000000000000000000000000000000000000c34d354f" - }, - { - "op": "set", - "key": "0xffc901cfbb39e551189323bea0af6cf9e81008c90dce65cbc2ff4672e263d7bb871b0daa3888412359e9769bc566073558cc37b927578bcc74743311f2e7487706a2", - "value": "0x00000000000000000000000000000000000000000000000000000000f5506557" - }, - { - "op": "delete", - "key": "0xff29e4e6bbd168fe5b46b7d34c34b628f95d54c4e75f9335a7e96282d1173b07df754b0f807e1dc46306d4cb12e85e18d008678fa80cd9e6fa118c34e179bd412a60" - }, - { - "op": "delete", - "key": "0x001df77729bb76b139342cfc86aff011590bf6bb810f47e69a6babac30af96f9f5e6" - }, - { - "op": "set", - "key": "0xff34bb4f3468340d5f72275da92420aed94db48bbc61823e92f29f14f51590373aeaa08ae14db4be9f284878fd074dce4d3ac11a41a69fde2feb4b3aaaa37da25b3a", - "value": "0x000000000000000000000000000000000000000000000000000000004a39fc3e" - }, - { - "op": "set", - "key": "0xffd1d141c1bed8bb4f809292e9340a0209cd167484a24a5ca993fcac4b97086d89fc2fb3a4956876528a21d73d01500adb051b92012a19d7aadb3b3ef7900625d1f8", - "value": "0x000000000000000000000000000000000000000000000000000000003dcc0aa0" - }, - { - "op": "delete", - "key": "0xff18e25b9b05a714bd17ede1445ee7c785983a0194a3ed103783662bdc401dd7ff2fb776a093982841cfb2c6c0a1fca9bc72a0ac907d132b7e3c6ac33b402bfba00f" - }, - { - "op": "set", - "key": "0x002cebacb183e2f51a3b6cb9f7fe2bd11c399bf1bdb8ce95ed457d3e46190197a0b0", - "value": "0x0000000000000000000000000000000000000000000000000000000039770101" - }, - { - "op": "set", - "key": "0x00e61994d31ceae39cae5feb02303eb90e0d4ae90f5b0755c3c26adc81f360ec850a", - "value": "0x000000000000000000000000000000000000000000000000000000002c55bae8" - }, - { - "op": "set", - "key": "0xff55a6923f7414ba482ae8a64b407f386a0bb0491b742bd4b22b5b4b44fca84e8426e25f0b1203d26d74a17012d28956de0586bf66f21665af66b3b0447aa0c5be33", - "value": "0x000000000000000000000000000000000000000000000000000000000151a01d" - }, - { - "op": "delete", - "key": "0x002eb90959edcd0b6a8a7922b8b8d37cd9664001c217b4c8e6d2df4b8ede329a71ac" - }, - { - "op": "set", - "key": "0xffd1afd6427b13f2ce7beb82a2cd6bbfc58eac8b2d46bc87bfbc78fbf440e18a63aeb0aca39c8f6ba440835a8e371b8b2719dec5b24a57f1da0e7519c3cd74beff5f", - "value": "0x0000000000000000000000000000000000000000000000000000000025f33c5c" - }, - { - "op": "set", - "key": "0x00c28c21982a56a6cecf595bfeb78297c7432abfb487cc75c09db2856a118f441d6a", - "value": "0x000000000000000000000000000000000000000000000000000000002b43a7ea" - }, - { - "op": "set", - "key": "0xff46aeb3243f905d4e0eac7f60ee8962848c87e79dfbd019ece14fe45bd720ea6b919d757e67485d253a689ff08c388cf32b629d2fb6fc4c514296d9b64fb4e58d46", - "value": "0x0000000000000000000000000000000000000000000000000000000092b6620d" - }, - { - "op": "set", - "key": "0xff29e4e6bbd168fe5b46b7d34c34b628f95d54c4e75f9335a7e96282d1173b07df754b0f807e1dc46306d4cb12e85e18d008678fa80cd9e6fa118c34e179bd412a28", - "value": "0x00000000000000000000000000000000000000000000000000000000dc940eaf" - }, - { - "op": "delete", - "key": "0xff55a6923f7414ba482ae8a64b407f386a0bb0491b742bd4b22b5b4b44fca84e8426e25f0b1203d26d74a17012d28956de0586bf66f21665af66b3b0447aa0c5be33" - }, - { - "op": "delete", - "key": "0x003cbfd03e3f083b0987f841c4372031faf655a63472518a3613e2e30ecb542b514a" - }, - { - "op": "delete", - "key": "0xff2eb90959edcd0b6a8a7922b8b8d37cd9664001c217b4c8e6d2df4b8ede329a71806e6656ad617b45d8e28cb6bd39e7244735a104a3ba4dd21833624b3c7afc179e" - }, - { - "op": "set", - "key": "0x004b83f061efb2f4708114e46b5ef50a255528c28c80bbe5fc6ad88e892bd4c4ea44", - "value": "0x00000000000000000000000000000000000000000000000000000000ee265e9b" - }, - { - "op": "delete", - "key": "0x0055a6923f7414ba482ae8a64b407f386a0bb0491b742bd4b22b5b4b44fca84e8420" - }, - { - "op": "set", - "key": "0xffec64b2de1fc1d53b795f49a1c84ca4172bc3292472c3e244e4f8c225fa786f56b7bce92ed9ae70f9ca2c10d21fe857f18cd2b64d54c70193d8a0dd772967c2581b", - "value": "0x000000000000000000000000000000000000000000000000000000008e764b06" - }, - { - "op": "set", - "key": "0x0068e867f5f899fc01d641a00cea2c42d58f3cc061797135198a85301d1e2903064d", - "value": "0x00000000000000000000000000000000000000000000000000000000975fb41f" - }, - { - "op": "delete", - "key": "0x00e61994d31ceae39cae5feb02303eb90e0d4ae90f5b0755c3c26adc81f360ec850a" - }, - { - "op": "set", - "key": "0x002eb90959edcd0b6a8a7922b8b8d37cd9664001c217b4c8e6d2df4b8ede329a7199", - "value": "0x000000000000000000000000000000000000000000000000000000005e2f9796" - }, - { - "op": "set", - "key": "0xffd1d141c1bed8bb4f809292e9340a0209cd167484a24a5ca993fcac4b97086d894d1ea4867b18a8e6a71ba19163c8fe25b3e543a8e6ab30e503bcb8af1a9189b757", - "value": "0x00000000000000000000000000000000000000000000000000000000ed5bec78" - }, - { - "op": "delete", - "key": "0xff34bb4f3468340d5f72275da92420aed94db48bbc61823e92f29f14f51590373aeaa08ae14db4be9f284878fd074dce4d3ac11a41a69fde2feb4b3aaaa37da25b3a" - }, - { - "op": "set", - "key": "0x0046aeb3243f905d4e0eac7f60ee8962848c87e79dfbd019ece14fe45bd720ea6b7f", - "value": "0x00000000000000000000000000000000000000000000000000000000963e1ccc" - }, - { - "op": "set", - "key": "0xff412c4d14e5bde6ea4c898a1d0f911f12a2cf775a62e3cc4c0f28331d432e8f10fb3754ee233f7964e47e5e73dc4342ee28be4dcf5e0bb64009bcbfd0c440559c39", - "value": "0x000000000000000000000000000000000000000000000000000000000ba6fe9e" - }, - { - "op": "set", - "key": "0x005963897639cd11d686b199366a1249d76bc89e0f7161c9eca48d9a199435952df4", - "value": "0x0000000000000000000000000000000000000000000000000000000017844180" - }, - { - "op": "set", - "key": "0xffe2f41518226671332da4adddab1fdcb9903c7b895c7943b669067fe96b0baacfaa95ffdfb787027e946ffafdb95a26702b197af7fb203ed9c203a4c7faa5016b33", - "value": "0x000000000000000000000000000000000000000000000000000000003c4466d6" - }, - { - "op": "set", - "key": "0xff0861030791246737d706c0d0017de01a26278bbe4ce8a134f5e0cd6f7eb69bcfa71b8622781be6cace7f8de1f1aaa611f095226899a85a29a996b9cb36bdc5899a", - "value": "0x000000000000000000000000000000000000000000000000000000008e7018f2" - }, - { - "op": "set", - "key": "0x000ed33feae99b046218c56bec56750a98beb5fd6a51ec13218024cb2455586b439e", - "value": "0x000000000000000000000000000000000000000000000000000000009ffbe4be" - }, - { - "op": "delete", - "key": "0x003cbfd03e3f083b0987f841c4372031faf655a63472518a3613e2e30ecb542b5188" - }, - { - "op": "set", - "key": "0x002cebacb183e2f51a3b6cb9f7fe2bd11c399bf1bdb8ce95ed457d3e46190197a0b0", - "value": "0x00000000000000000000000000000000000000000000000000000000e03763ae" - }, - { - "op": "set", - "key": "0x00e57860ba0adcdd1294caf74da8b990a80a8a6861bc01b30dfe07450d25bab38f2f", - "value": "0x000000000000000000000000000000000000000000000000000000005b187368" - }, - { - "op": "set", - "key": "0x004c1eb4996f3098c89ee461d61bd5f177587b0f4363ea73f4387e6296c66a260fcf", - "value": "0x00000000000000000000000000000000000000000000000000000000a33c6a30" - }, - { - "op": "delete", - "key": "0xff5ac76a8f23131e4ca988c8afcc313130cc95e521d2691635fc26196d63e66a1f3e2bba64771273ebc79252548c60f34e55d39cd7d0084b6e45722cab3f7021e337" - }, - { - "op": "set", - "key": "0x0078a2812688114caf4452a5611322977e4b1000e59ef408d4c8e2c90028370bcb29", - "value": "0x00000000000000000000000000000000000000000000000000000000a3d74d25" - }, - { - "op": "delete", - "key": "0xff0861030791246737d706c0d0017de01a26278bbe4ce8a134f5e0cd6f7eb69bcfbf2761d1a4e27ad313f45a816d320676aa7ee1cb19958f32f02c32fa7ed16c3753" - }, - { - "op": "delete", - "key": "0xff412c4d14e5bde6ea4c898a1d0f911f12a2cf775a62e3cc4c0f28331d432e8f10fb3754ee233f7964e47e5e73dc4342ee28be4dcf5e0bb64009bcbfd0c440559c39" - }, - { - "op": "set", - "key": "0x00e57860ba0adcdd1294caf74da8b990a80a8a6861bc01b30dfe07450d25bab38f00", - "value": "0x0000000000000000000000000000000000000000000000000000000006ebdda1" - }, - { - "op": "set", - "key": "0x000fb23a930b05e3bdfbdd0e6694fa9df9c5059ceabc60618bc40931ddb379f1f698", - "value": "0x00000000000000000000000000000000000000000000000000000000451e0697" - }, - { - "op": "set", - "key": "0xffc901cfbb39e551189323bea0af6cf9e81008c90dce65cbc2ff4672e263d7bb87b6b9a84c441401ccd95da2860ed9d8280486af5808c4c4451016344482faa97969", - "value": "0x0000000000000000000000000000000000000000000000000000000092464772" - }, - { - "op": "set", - "key": "0x002eb90959edcd0b6a8a7922b8b8d37cd9664001c217b4c8e6d2df4b8ede329a7111", - "value": "0x00000000000000000000000000000000000000000000000000000000b715b9b5" - }, - { - "op": "delete", - "key": "0x002eb90959edcd0b6a8a7922b8b8d37cd9664001c217b4c8e6d2df4b8ede329a7199" - }, - { - "op": "set", - "key": "0xffd1d141c1bed8bb4f809292e9340a0209cd167484a24a5ca993fcac4b97086d89c3be5dbd9bb86ee98903d86eb770fe1c69b3d13353d7843180f2fe702bb019d631", - "value": "0x0000000000000000000000000000000000000000000000000000000002d25007" - }, - { - "op": "set", - "key": "0x0055a6923f7414ba482ae8a64b407f386a0bb0491b742bd4b22b5b4b44fca84e84c4", - "value": "0x00000000000000000000000000000000000000000000000000000000d8a2831d" - }, - { - "op": "set", - "key": "0x007b1eefd69ea014244937aaeaea4e8d4d296c04edd75ae47897e5612ef5a1a2521d", - "value": "0x00000000000000000000000000000000000000000000000000000000bf0e71f6" - }, - { - "op": "set", - "key": "0xffd1d141c1bed8bb4f809292e9340a0209cd167484a24a5ca993fcac4b97086d89c3be5dbd9bb86ee98903d86eb770fe1c69b3d13353d7843180f2fe702bb019d6cc", - "value": "0x0000000000000000000000000000000000000000000000000000000009603b04" - }, - { - "op": "set", - "key": "0xff1df77729bb76b139342cfc86aff011590bf6bb810f47e69a6babac30af96f9f5e7d6ce6d6a1c26b63a60e8b794be3719768baf49ed0d8a610f1fc3768675f31eff", - "value": "0x00000000000000000000000000000000000000000000000000000000967aea19" - }, - { - "op": "set", - "key": "0xff1df77729bb76b139342cfc86aff011590bf6bb810f47e69a6babac30af96f9f5ffa1e869c4566231d09357daec90e71aa93821a7347b29175ef4a1bc02a0b3819a", - "value": "0x000000000000000000000000000000000000000000000000000000004ed13c61" - }, - { - "op": "delete", - "key": "0xffd1d141c1bed8bb4f809292e9340a0209cd167484a24a5ca993fcac4b97086d89fc2fb3a4956876528a21d73d01500adb051b92012a19d7aadb3b3ef7900625d1f8" - } - ], - "roots_after": [ - "0xe4a4cc81bad5ec263e1470b6766f0e48c44cb6de9edfafe42ce6a742e7c0de35", - "0x628d4752f5a37f73d798da0fd84216c8c96d376e81ca8c011a1ef0e5b293037d", - "0xe4a4cc81bad5ec263e1470b6766f0e48c44cb6de9edfafe42ce6a742e7c0de35", - "0x0000000000000000000000000000000000000000000000000000000000000000", - "0x36d0ac2cbaa8daac84e3abb0711753ab2803d39fb7def6e34b95e607b5177c92", - "0xcdd2d3edb22a14874aa1b8adc01bad8e80dd4f6f0c603788bd28705981409ba7", - "0x0438e8ad308f977fe36353cde64fef7f1210dd3820b5275c3fffe712e342f125", - "0xe8514dc5fbf6794122aad2a058ad95df0835731e49061dfc5238fe8673b7cb57", - "0xfa1bed5d0c8187d176cdabee2a0b6d96ec72791326c1d9d00555e2752fa4ae06", - "0x388f561f8cd9b274f5c9cd3154ab49c7586c9cae7b28c0b7471be9be98cba3be", - "0x36fca86e1497e245753b5662e45cac10ce68278494adef2b1219b534d63411e2", - "0x98ebe09efe3ff35ee2f8b20732a3f30408119a25e620107c33f503cd39e55143", - "0xcd53e141a34d3c96464d66bc027b4cc0731739f1e645aced12c51072463f8fc4", - "0xbba122989fb7ec24899f02a1aa37fcf967d84f6c5207d843ac9a754b1eaa7ba3", - "0xb219a14dd80c8cc69152edbcec566fe773892d5e615e310b063e99def0f6aa79", - "0x3531deb75570f3a160ba9b471a10be04750c59ed54984186a46d4521e5058145", - "0xd3e835c31b1c00c8f864d7da97f035c6147452c3762a7f69b174751b815737a8", - "0xd74280c2db61bd2bd9d739ae4c38053ce6192f683907b95c6c0fed0ddf6b1b66", - "0x24a2abd1e5f37cbb1e104406be5ee8b6c4989c27bb6ec117ae4e18c0b66dfc9c", - "0x3a7c4099580d9b766538946c88846c95d7822fd4d6121a20e57e1791026600f5", - "0xf84fc76ce622a6933cebc9540fcb022e151a7c4c31a79eaa5be6ffd0d20cd68a", - "0x34cddc1590a8c17711ca84362e563a067b2b7ad70c1921f75fe2d220fd75d465", - "0xb3d8bde73b3332d3cb4ddc466011a4cceaae38bc39d1c33bad6995571047863b", - "0xf685cf0a94e0bac75afeb7397fb89635115e02166e94ce7d3219930ee684b7aa", - "0xdc07f0e78a0411352056d13b4ccd4b06745536c085eafecef9ddab91b9e6f0ec", - "0x43354ad010af4ae17379ccbad6e33039963b86efc10494cf4a7da5348a82cce3", - "0xf05ba16b614295a0687836cb157331aa02fbab9b6458277d9ed6d5659a586b77", - "0xd4f10dbd728c2a8ee6f4863d865d689d8ff229ff616c3d157d449667ea41cbef", - "0x6697b3f6f789029b04581af31b1c70925c38fa1ccfa3be1e0af3ef0be56ada02", - "0x0916316805ae030068246d81a7c9f83faba0393fdb158b2d3322d33e3b99b3f1", - "0x8f460c4cfa508a08de9f592ba68be56ef2f459944807d664ff4860055d341778", - "0x17b98089c48e35a0e2fad4732551346ab3720b6397fc94438ba2685184d5d5cf", - "0x10745294edf5a205225b306851c19f21a677d36875784526cf4c33fd04b5965d", - "0xe96e76f22ae166564705f77db0d1f67df31bf0cca73918e5aa7d510592e7d26c", - "0xb0059099dacec9e2eaa10817ef063d845f606b5bf3da9f6b2b95f38b2cb828f8", - "0x7d48bf3f3dadf2f5d299b5db3d4256e7af01acf2dd68ebd9fcc2c6e4dcb34931", - "0x00b6772ae1bedc77b377240ec00fd3cbbc2ab562c7cf043c5828b51495e9c8eb", - "0xa96401326fd9a4f0397988cb3170ca4b99c85eae5c50c9a373ae08684506ae6e", - "0x473dca4e608cc4103adbb59779dd4a2eb414dacd4da4043ee76a764dd4c2c4c1", - "0xa883f89ca7f388c82a0ef4a20dae822a2d9d486af8a57c690cef42d344e9d068", - "0x455d8f1b93c1a54715dd32179efef0efbde084e5253e4b4986f09581fe44aa34", - "0x5fe5322c6fb9a568924a518319e71a4dd8880d52e02d7714f870b713f1e44f19", - "0xf1660dee83406bd7010044d77d4906bc320c32efde101af9303fb2a8614c090d", - "0x1062401a0d50ea2216cbb77eb944a1c744dfa017b8b692b09b0c02e7d256fbc2", - "0x2c3710cf55cb465e6e5655d3f49ca4f0f114d63f5af052a1a09591dfc137ccf2", - "0xfe8bfaad67203350c7c7f848704424cb433454ef6a430871bd1826e44f587dfb", - "0x48705b693bc63fae4ec2591320c9136ea8b4716dbe12076463419e5276272091", - "0x73d7c247b742add3a39176f85a1bbb43cf935b938dc593f1a2261a37fff10fdd", - "0x0928445cf8ba29f383de766efab11ea09f43cc1b8b85488e4c6f9c735a95a505", - "0x2061ee6e89dd8cd77cade4361ba6b42f547b75a77cfcfd8ca24913374b45011a", - "0xcbbe2daa7666d29abacecdede49ec1a8493a1322f7858f54feebcdb783b983c0", - "0x309114c5600aefd10c61b468f20e8704e7348e8a0f81a9679c9c2feadf9fa045", - "0x78179d80cdd06fddd31c9564b6a05e9b61ce68af301c021691e38badb57439ba", - "0x5c56ebafe3115002fbd0407d0f68bfe5eaa11f59ae9c147db3c7dcbf58ed6933", - "0x74a61b583f5ed143c5fff4ad3f06c086dcfa4b3f81d8868ad3402a16e4e5ad61", - "0xc159413554e131b1b771a43ba0b81794cc0623f011847fee13fd0257b37e4bc9", - "0xa692607dd1c8d9d9c21a457256e717f4a8862e68d8829ab4bb30b1bde3ff77ad", - "0x66ad930c29af810ddfaed2fb9fab745c4cd8f275f24d8d5802306c83aaaaa187", - "0xfc5d2d911b277aad35c310352d8552c4c02285f78273899be5d362f57b24ff0f", - "0x6e1b8ec9bfd2348b47a1076da4af2bfd28d6a3f1fe440b77223fc02aff233e3b", - "0x0ab3b476e01d3d4a8d92e9e92cb21424aa79ff00c8aa67d66118df0853fd1aa8", - "0x02a179e8653fcd5c4a091af100a099f5a8daedf15829470ab42c93b1cd0052d0", - "0x26d58d5aee3469d83c2fab012feef23b0fc21cc1b1d8503fa3ee57aadf24fa27", - "0x5b4fb8fab835c618c266acd82f15f222808a0fdbc3a1b17710eb07c68ee77238", - "0x62a2efaf3e03b45251b3cd2e410f62479cf70e3eb76a8b8fd2e20ca435608f71", - "0x533163db2f23acfc392d7c5d4904506c8ec80bab454bffb8e6249025490c3c24", - "0x5940a36786e1918a942076cc9bf4aa7dd231f76fca798dbad3abc49a793e86b4", - "0x6ab82da71213fb78645123fa9640c75cf9cac72e4f875efc85b330b61e3ee457", - "0x74be56f45537f975b3f984dbe6b57d44d403171f58acc0381199374b12ccd716", - "0x800d6ad3bfd7dca5cb3221e7fce585981ff95828b169814ea19f00a6e857179a", - "0xb284fe5afd4f52bf43c3179955169afb24d8a971b240400029780db1666a30f8", - "0xf5680518cc857872694fb6d24a1b05f998a77e16a565fff86407ce47ca36e6e2", - "0x4254f5a644a680f16d3759c0cc009b8d72b3db268889684c8ef6142357ec0f37", - "0xb4589863140a51c6a79921cc67f1ebfe5d880d2e298da31f30164cb339ef120e", - "0xdf9b6039ad535a07df1d4ec7f98545cdfeb7be56e3fbc336734a64acd03756bb", - "0x28530cdd71a24116b5b491b85fadc81bdd2bf3bbcefc4524f92c8c79a3ff52c5", - "0xc59a155b824a2d1b748cc8598ba316f649c9e19f693199d053cd1ea13bc1739b", - "0xfd3134a639bf799aacd3da7699049a5d98e8f82b4a0ae77857cd95272375f45b", - "0xfa1f552ff3a234c78ced77795ab1cc9d21032cd441e4b32298a3413e6d9a7bd7", - "0x0f825e88b6c02f4c2861deafdece5b9ec2510c6f4cfbb7aa90ec69f3ac51bd8d", - "0xceb55f8bd19bf9be41e1191ac399ef5f3bb136b78a33cb3d79567ff58b880486", - "0x6800397b7a74844edcdf2a69660e8806952b1f985b87d1f91dc97a1b5b29e02f", - "0x55cd9f91c8afc3c515a56dd4b66dcebe61dabc180b543e84b9b304bde020bebe", - "0xa23cca1ebf002b00da78f1edca6222f29d3bb9362a802346f8be9f3805447c0b", - "0xf199828ecf1bfa0ea2e0fc155664044feb11d3b85584ce8d3914613dfacf7c63", - "0x8964f205fd6fc07ef01ef8c7054d10feb7e48a94dd32dbfe9399e5dadae98f2d", - "0x629157eae49544b71670b81dcefabcbf11bf4f36a678fb79f2c5f7aebcc2f6f2", - "0x9db599af3a0c0f67950a21c41e1b42f656fd8acb92468f66071a175239d75e1a", - "0x1429f012d2b3a739b3dd2d01b6588eec54156942bf7f15d803dc115e859df10a", - "0x82a88763b6f8be194a68db2f4a00d8851d64e5ff1f148db8c94628c23cdb383f", - "0x6e6e9c59bd19f242fb2c98088074174e5dba96273870a8c56e377bb0315625db", - "0xabb423d1467c7106349d6b49e55036a83c631f99f55a913973ef148bca0e2110", - "0x1a6acd3760f5e5034b9e07498c489c02beff79e53ffdfc0fd0ce4c80f79da243", - "0xde3e17da380803759ef54eccca593c1e34911b6fc94eef5c0d71d93e71c26239", - "0x6271971bf8389ef7325dcf1613409f5b0080473964f2f022132ffcb78e5888eb", - "0x4453ab7cb93660c10dfa508f853d08cc0ae48749d9b4117d5e3d9d25d055bad6", - "0x54cb98722c2e0d36fdcee552650c8dca3e24f9facf270f719d9d2a2929725ed7", - "0x19a55d1d6f124e490a3833b71f5f8e6612393af922475ee9ca61b9ccf9654d38", - "0x563dd892856e5a4ea1945c5c5d5796076e7021a50543d9f618716be5d31a3c78", - "0xb2c79611944dd7ba2517cd56bf20d23f95f9021bf3f554e83aa81d4e08f50cfc", - "0xedfdbc86c72543bacde2dabc8f6fd189d803eb500dfc973c6403132804cfd5ee", - "0x3614c14b8a9e85ced687272e3753a281b34557de3995edf5f1add5077fbe4b62", - "0xbc9e363bd3f39a9fda1a273101ce0f53f01543e066da803252b6840c1ec2af33", - "0x91e4b5c526dfe9fa0c2b77ae807b53b6ae668614879455278600fc6328f511c4", - "0x49e2520ba83d39ed15c04bd21ceaf86b22fd782589223cf6b289f9036fc212ff", - "0x7735f713872008b61a37dab679f3a86b817f9201d461d2c524307ac8473726ed", - "0x582de5014feebda942b686f32a846b74fc2c0439d1fa685544cb51b2ecf7caf2", - "0xaad81ca1c4469678e4bb6d427c7865000f7345b14a8b95dc9486eccd363452bb", - "0x5a682033d72dd9135286624f43e70346635b73cd69624d224311b5f9112f9ae8", - "0x0cff541bd8dfb5375ba7623de4d05bf3eef5bcd9869aebe3aff348daab8b460c", - "0xbb2d25d7656f789dd29b78132479e9975afef139ba6516c691b8e1525b70d8e5", - "0x73e5d4ad8e6d554f8f497c6a3489995f2c29b9ba028c837b5900ccd4b4aa2871", - "0x3be8d4b932299459a57f214970b120a85aa0c57ab9cd5e76f9320ac9a6dcc02a", - "0x17755de46c02d2e9750b7320c678cd08f84d4c89e075063f2be85c705fce00f0", - "0xc2a1c481fc1491eb4c7f4e59823f81ea704c04cd3e2da51f0e52a3516bc57f89", - "0xd2b40ce291f5b9216f391402c92a679c3fe0920f9042039fdae3ecee10e2a6e3", - "0xff016b6951ce61766f9ec1907176d3f58b0f9263d69469730a5b847b41740528", - "0x65aee0fbadc249e3a4384191a1ad5aba6d453a5de5bbf6101be7db9451203104", - "0x858540442e239355d7605797cdaf1ff9bb5daa18aa11ff05a638e8877b4d82bb", - "0x426144d7b7feba9b6d220fc8063c937bf23c17a67585ccb733ed811240af9c06" - ] - }, - { - "seed": 20260727, - "ops": [ - { - "op": "set", - "key": "0x002eb90959edcd0b6a8a7922b8b8d37cd9664001c217b4c8e6d2df4b8ede329a7170", - "value": "0x0000000000000000000000000000000000000000000000000000000068535e9a" - }, - { - "op": "set", - "key": "0xff3cbfd03e3f083b0987f841c4372031faf655a63472518a3613e2e30ecb542b5111589c57f4d8ad55d2c4ece4c6b63961c45be7479bfd08d04fab2d26d0acb7a091", - "value": "0x0000000000000000000000000000000000000000000000000000000056756dfe" - }, - { - "op": "set", - "key": "0x007b1eefd69ea014244937aaeaea4e8d4d296c04edd75ae47897e5612ef5a1a2521a", - "value": "0x000000000000000000000000000000000000000000000000000000005959a793" - }, - { - "op": "delete", - "key": "0x002eb90959edcd0b6a8a7922b8b8d37cd9664001c217b4c8e6d2df4b8ede329a7170" - }, - { - "op": "delete", - "key": "0x007b1eefd69ea014244937aaeaea4e8d4d296c04edd75ae47897e5612ef5a1a2521a" - }, - { - "op": "set", - "key": "0x0055a6923f7414ba482ae8a64b407f386a0bb0491b742bd4b22b5b4b44fca84e8468", - "value": "0x000000000000000000000000000000000000000000000000000000003c2b7202" - }, - { - "op": "delete", - "key": "0xff3cbfd03e3f083b0987f841c4372031faf655a63472518a3613e2e30ecb542b5111589c57f4d8ad55d2c4ece4c6b63961c45be7479bfd08d04fab2d26d0acb7a091" - }, - { - "op": "delete", - "key": "0x0055a6923f7414ba482ae8a64b407f386a0bb0491b742bd4b22b5b4b44fca84e8468" - }, - { - "op": "set", - "key": "0xffe60d91d1d11837912311124089c6b73c9d726c99f4df797eb48c2b7fec6e5e2c6066e8c722a574a900a59551ed956d32ca2b36e48771ed8b373da6b223bbbe9c8f", - "value": "0x000000000000000000000000000000000000000000000000000000009bb7df73" - }, - { - "op": "set", - "key": "0x009c847c439d6431d36ddeb326cb553da2e967778a24afbd1d23a62412186faf0130", - "value": "0x00000000000000000000000000000000000000000000000000000000def11b80" - }, - { - "op": "set", - "key": "0x007b1eefd69ea014244937aaeaea4e8d4d296c04edd75ae47897e5612ef5a1a2524d", - "value": "0x00000000000000000000000000000000000000000000000000000000f05708e7" - }, - { - "op": "set", - "key": "0x00998f08d7054351e5fa3ccadda32ab8d6c0c86a825dd360d57c0fe6b46612297cba", - "value": "0x00000000000000000000000000000000000000000000000000000000c433224b" - }, - { - "op": "set", - "key": "0xff5ac76a8f23131e4ca988c8afcc313130cc95e521d2691635fc26196d63e66a1f33b1e44125772918b9d44a12766eb0dad773e863d120780173c394789aa668e925", - "value": "0x00000000000000000000000000000000000000000000000000000000abbc594e" - }, - { - "op": "set", - "key": "0xff998f08d7054351e5fa3ccadda32ab8d6c0c86a825dd360d57c0fe6b46612297c36c8cce2938e709d43205827ddebecc780d7e631bb1acb24046af6e5b23e445aa2", - "value": "0x00000000000000000000000000000000000000000000000000000000219ea23a" - }, - { - "op": "set", - "key": "0x00d1d141c1bed8bb4f809292e9340a0209cd167484a24a5ca993fcac4b97086d89af", - "value": "0x00000000000000000000000000000000000000000000000000000000e015951e" - }, - { - "op": "delete", - "key": "0x00d1d141c1bed8bb4f809292e9340a0209cd167484a24a5ca993fcac4b97086d89af" - }, - { - "op": "set", - "key": "0xffc28c21982a56a6cecf595bfeb78297c7432abfb487cc75c09db2856a118f441df2a84c02b0cbaafcabc4cbb5f988d916f3287032f07e3f8bf51588b4311ed04c72", - "value": "0x00000000000000000000000000000000000000000000000000000000973ab40a" - }, - { - "op": "set", - "key": "0x00e2f41518226671332da4adddab1fdcb9903c7b895c7943b669067fe96b0baacfdb", - "value": "0x000000000000000000000000000000000000000000000000000000000c8a8e64" - }, - { - "op": "delete", - "key": "0x007b1eefd69ea014244937aaeaea4e8d4d296c04edd75ae47897e5612ef5a1a2524d" - }, - { - "op": "set", - "key": "0xff15c4813bb5b1499e0a7e0d46daab8e10c70940c008c566d5c68557b1523d1f29cac055e3c06fef0ce191be15524a246c11d1103158d4f8b49d31c629e552005366", - "value": "0x000000000000000000000000000000000000000000000000000000001a5e6148" - }, - { - "op": "delete", - "key": "0x00998f08d7054351e5fa3ccadda32ab8d6c0c86a825dd360d57c0fe6b46612297cba" - }, - { - "op": "set", - "key": "0xffe57860ba0adcdd1294caf74da8b990a80a8a6861bc01b30dfe07450d25bab38f63b4527cc539651483c1b056383959f91cb98fdbd3c1799a854a2781ddb3229037", - "value": "0x000000000000000000000000000000000000000000000000000000008734797f" - }, - { - "op": "set", - "key": "0x000861030791246737d706c0d0017de01a26278bbe4ce8a134f5e0cd6f7eb69bcff4", - "value": "0x00000000000000000000000000000000000000000000000000000000386408da" - }, - { - "op": "set", - "key": "0xff5963897639cd11d686b199366a1249d76bc89e0f7161c9eca48d9a199435952d359cb4900b950c37e7546543cb5a55b8a4e32316f9ddc7cb39743747f93009f45d", - "value": "0x00000000000000000000000000000000000000000000000000000000b10b1b96" - }, - { - "op": "set", - "key": "0x00543907253acb230f5dc96bbafd8ea0f37dd63b30e8a4a3675bb007673a31fbc571", - "value": "0x000000000000000000000000000000000000000000000000000000007bda4a3e" - }, - { - "op": "delete", - "key": "0xff15c4813bb5b1499e0a7e0d46daab8e10c70940c008c566d5c68557b1523d1f29cac055e3c06fef0ce191be15524a246c11d1103158d4f8b49d31c629e552005366" - }, - { - "op": "set", - "key": "0x00d1afd6427b13f2ce7beb82a2cd6bbfc58eac8b2d46bc87bfbc78fbf440e18a632f", - "value": "0x00000000000000000000000000000000000000000000000000000000381c8a4a" - }, - { - "op": "set", - "key": "0xff2cebacb183e2f51a3b6cb9f7fe2bd11c399bf1bdb8ce95ed457d3e46190197a0a0a0268860a87b73c5d023e0181059e1d49f8ed387a132e90d5ccf7b7589f952f2", - "value": "0x000000000000000000000000000000000000000000000000000000000c2e34b8" - }, - { - "op": "set", - "key": "0x0029e4e6bbd168fe5b46b7d34c34b628f95d54c4e75f9335a7e96282d1173b07dfc7", - "value": "0x00000000000000000000000000000000000000000000000000000000b4025b23" - }, - { - "op": "set", - "key": "0xff55a6923f7414ba482ae8a64b407f386a0bb0491b742bd4b22b5b4b44fca84e843a068760d000d05d6c87419289f5e5adb67a586f14948a8dac8873ed19f3d6ef65", - "value": "0x0000000000000000000000000000000000000000000000000000000055789247" - }, - { - "op": "delete", - "key": "0x009c847c439d6431d36ddeb326cb553da2e967778a24afbd1d23a62412186faf0130" - }, - { - "op": "set", - "key": "0xffe2f41518226671332da4adddab1fdcb9903c7b895c7943b669067fe96b0baacfed266f135d9b962dee7803d8894e76641c4aeb45c9d87a11b132682e19724ea737", - "value": "0x0000000000000000000000000000000000000000000000000000000076d452b6" - }, - { - "op": "delete", - "key": "0xffe2f41518226671332da4adddab1fdcb9903c7b895c7943b669067fe96b0baacfed266f135d9b962dee7803d8894e76641c4aeb45c9d87a11b132682e19724ea737" - }, - { - "op": "set", - "key": "0x000fb23a930b05e3bdfbdd0e6694fa9df9c5059ceabc60618bc40931ddb379f1f624", - "value": "0x0000000000000000000000000000000000000000000000000000000048332348" - }, - { - "op": "set", - "key": "0x00412c4d14e5bde6ea4c898a1d0f911f12a2cf775a62e3cc4c0f28331d432e8f100e", - "value": "0x0000000000000000000000000000000000000000000000000000000034e8d1dd" - }, - { - "op": "set", - "key": "0x001df77729bb76b139342cfc86aff011590bf6bb810f47e69a6babac30af96f9f5c2", - "value": "0x000000000000000000000000000000000000000000000000000000001904c8c3" - }, - { - "op": "set", - "key": "0xffe60d91d1d11837912311124089c6b73c9d726c99f4df797eb48c2b7fec6e5e2c6066e8c722a574a900a59551ed956d32ca2b36e48771ed8b373da6b223bbbe9c2f", - "value": "0x00000000000000000000000000000000000000000000000000000000d1033f40" - }, - { - "op": "set", - "key": "0x00998f08d7054351e5fa3ccadda32ab8d6c0c86a825dd360d57c0fe6b46612297c20", - "value": "0x00000000000000000000000000000000000000000000000000000000d11ac616" - }, - { - "op": "set", - "key": "0x00c28c21982a56a6cecf595bfeb78297c7432abfb487cc75c09db2856a118f441d01", - "value": "0x000000000000000000000000000000000000000000000000000000009e8da098" - }, - { - "op": "delete", - "key": "0x00e2f41518226671332da4adddab1fdcb9903c7b895c7943b669067fe96b0baacfdb" - }, - { - "op": "set", - "key": "0x007b1eefd69ea014244937aaeaea4e8d4d296c04edd75ae47897e5612ef5a1a252fc", - "value": "0x000000000000000000000000000000000000000000000000000000003e436d7f" - }, - { - "op": "set", - "key": "0x00e57860ba0adcdd1294caf74da8b990a80a8a6861bc01b30dfe07450d25bab38f0c", - "value": "0x000000000000000000000000000000000000000000000000000000002c32286a" - }, - { - "op": "delete", - "key": "0xffe60d91d1d11837912311124089c6b73c9d726c99f4df797eb48c2b7fec6e5e2c6066e8c722a574a900a59551ed956d32ca2b36e48771ed8b373da6b223bbbe9c8f" - }, - { - "op": "delete", - "key": "0x00d1afd6427b13f2ce7beb82a2cd6bbfc58eac8b2d46bc87bfbc78fbf440e18a632f" - }, - { - "op": "set", - "key": "0xffc28c21982a56a6cecf595bfeb78297c7432abfb487cc75c09db2856a118f441dc855e1f0f687b464e5d31a72fec3317fe80974db611da9609c42c92987525ea901", - "value": "0x00000000000000000000000000000000000000000000000000000000381c77f7" - }, - { - "op": "set", - "key": "0x00998f08d7054351e5fa3ccadda32ab8d6c0c86a825dd360d57c0fe6b46612297ce2", - "value": "0x0000000000000000000000000000000000000000000000000000000017ab1191" - }, - { - "op": "set", - "key": "0x00c28c21982a56a6cecf595bfeb78297c7432abfb487cc75c09db2856a118f441d5f", - "value": "0x000000000000000000000000000000000000000000000000000000008c47c001" - }, - { - "op": "set", - "key": "0xff18e25b9b05a714bd17ede1445ee7c785983a0194a3ed103783662bdc401dd7ff361b271b7deb81ee7027749296cc9369ae699ca1ef815575a5c0bd3b5b0b0cf8a6", - "value": "0x0000000000000000000000000000000000000000000000000000000078038801" - }, - { - "op": "set", - "key": "0xffd1afd6427b13f2ce7beb82a2cd6bbfc58eac8b2d46bc87bfbc78fbf440e18a6338206d654c672ec71e308a2a76d9c171e5b436fa022e52e5b908a23b352437094e", - "value": "0x00000000000000000000000000000000000000000000000000000000b1a30ba3" - }, - { - "op": "set", - "key": "0xffc901cfbb39e551189323bea0af6cf9e81008c90dce65cbc2ff4672e263d7bb87b6b9a84c441401ccd95da2860ed9d8280486af5808c4c4451016344482faa9794b", - "value": "0x000000000000000000000000000000000000000000000000000000001000fb6e" - }, - { - "op": "set", - "key": "0x004c1eb4996f3098c89ee461d61bd5f177587b0f4363ea73f4387e6296c66a260fc7", - "value": "0x0000000000000000000000000000000000000000000000000000000086cb5d38" - }, - { - "op": "set", - "key": "0x000ed33feae99b046218c56bec56750a98beb5fd6a51ec13218024cb2455586b43ff", - "value": "0x00000000000000000000000000000000000000000000000000000000aae2734a" - }, - { - "op": "set", - "key": "0x00c28c21982a56a6cecf595bfeb78297c7432abfb487cc75c09db2856a118f441d99", - "value": "0x00000000000000000000000000000000000000000000000000000000d35b8891" - }, - { - "op": "set", - "key": "0x00ec64b2de1fc1d53b795f49a1c84ca4172bc3292472c3e244e4f8c225fa786f561c", - "value": "0x00000000000000000000000000000000000000000000000000000000dd9bc05c" - }, - { - "op": "set", - "key": "0x0034bb4f3468340d5f72275da92420aed94db48bbc61823e92f29f14f51590373a00", - "value": "0x000000000000000000000000000000000000000000000000000000007fe863ca" - }, - { - "op": "set", - "key": "0x00a43fb2d7beb12b34786a837467f62b7d6c5dab4ab7eecc89c0b67db76a68292754", - "value": "0x0000000000000000000000000000000000000000000000000000000085af5a94" - }, - { - "op": "delete", - "key": "0x00998f08d7054351e5fa3ccadda32ab8d6c0c86a825dd360d57c0fe6b46612297ce2" - }, - { - "op": "delete", - "key": "0x00c28c21982a56a6cecf595bfeb78297c7432abfb487cc75c09db2856a118f441d5f" - }, - { - "op": "set", - "key": "0xff5963897639cd11d686b199366a1249d76bc89e0f7161c9eca48d9a199435952d4a28b184a4e022d38844c7967da6fbdf879ffafc6cfabf88c929ec9c9ff9004096", - "value": "0x000000000000000000000000000000000000000000000000000000009ac44455" - }, - { - "op": "delete", - "key": "0x0034bb4f3468340d5f72275da92420aed94db48bbc61823e92f29f14f51590373a00" - }, - { - "op": "set", - "key": "0x00e2f41518226671332da4adddab1fdcb9903c7b895c7943b669067fe96b0baacfda", - "value": "0x0000000000000000000000000000000000000000000000000000000042bbddff" - }, - { - "op": "set", - "key": "0x000fb23a930b05e3bdfbdd0e6694fa9df9c5059ceabc60618bc40931ddb379f1f6aa", - "value": "0x0000000000000000000000000000000000000000000000000000000045686f32" - }, - { - "op": "set", - "key": "0x001df77729bb76b139342cfc86aff011590bf6bb810f47e69a6babac30af96f9f57e", - "value": "0x00000000000000000000000000000000000000000000000000000000c7e4a178" - }, - { - "op": "set", - "key": "0x000ed33feae99b046218c56bec56750a98beb5fd6a51ec13218024cb2455586b4307", - "value": "0x00000000000000000000000000000000000000000000000000000000c6550c69" - }, - { - "op": "set", - "key": "0xff4c1eb4996f3098c89ee461d61bd5f177587b0f4363ea73f4387e6296c66a260fed383baf24ff33220d52e65d0501f90eee41f67e456df12e8379bb13167e30a6e7", - "value": "0x00000000000000000000000000000000000000000000000000000000ca881fef" - }, - { - "op": "set", - "key": "0x000fb23a930b05e3bdfbdd0e6694fa9df9c5059ceabc60618bc40931ddb379f1f6bc", - "value": "0x00000000000000000000000000000000000000000000000000000000a0d8701e" - }, - { - "op": "set", - "key": "0x009c847c439d6431d36ddeb326cb553da2e967778a24afbd1d23a62412186faf018c", - "value": "0x00000000000000000000000000000000000000000000000000000000ccb22b18" - }, - { - "op": "set", - "key": "0x007b1eefd69ea014244937aaeaea4e8d4d296c04edd75ae47897e5612ef5a1a252e2", - "value": "0x00000000000000000000000000000000000000000000000000000000a0d0e3b1" - }, - { - "op": "set", - "key": "0xff1df77729bb76b139342cfc86aff011590bf6bb810f47e69a6babac30af96f9f5ffa1e869c4566231d09357daec90e71aa93821a7347b29175ef4a1bc02a0b38152", - "value": "0x00000000000000000000000000000000000000000000000000000000ddb5065b" - }, - { - "op": "set", - "key": "0xffe2f41518226671332da4adddab1fdcb9903c7b895c7943b669067fe96b0baacfab2dcf341b3549396a13ac48963cf4aac70fda23d99a2c2a599e0efe17278e795a", - "value": "0x00000000000000000000000000000000000000000000000000000000f5d11d62" - }, - { - "op": "set", - "key": "0xffbded9122e6171a5dad9ce2aaebd933abf4718e9bf12448cf113064c9fd8e6ad064269e16de5ef26ae08b5660f7eaa784399543edd7bf8fbe04c13ac444cb4e61a6", - "value": "0x00000000000000000000000000000000000000000000000000000000d83e7fab" - }, - { - "op": "set", - "key": "0x004c3a8541d9ec369a9fd4bcdad885cc3b166da70ed6a83066924727d9ff147fce26", - "value": "0x000000000000000000000000000000000000000000000000000000004ef6829a" - }, - { - "op": "delete", - "key": "0xffd1afd6427b13f2ce7beb82a2cd6bbfc58eac8b2d46bc87bfbc78fbf440e18a6338206d654c672ec71e308a2a76d9c171e5b436fa022e52e5b908a23b352437094e" - }, - { - "op": "set", - "key": "0xff0ed33feae99b046218c56bec56750a98beb5fd6a51ec13218024cb2455586b437ef4a13c0f00718e5b1d19138c7f9db57ed5b04d19dca824d5e61fc17dc47ff923", - "value": "0x000000000000000000000000000000000000000000000000000000005743f9a8" - }, - { - "op": "delete", - "key": "0x00c28c21982a56a6cecf595bfeb78297c7432abfb487cc75c09db2856a118f441d01" - }, - { - "op": "delete", - "key": "0x000ed33feae99b046218c56bec56750a98beb5fd6a51ec13218024cb2455586b43ff" - }, - { - "op": "set", - "key": "0xff0861030791246737d706c0d0017de01a26278bbe4ce8a134f5e0cd6f7eb69bcfa71b8622781be6cace7f8de1f1aaa611f095226899a85a29a996b9cb36bdc5892c", - "value": "0x000000000000000000000000000000000000000000000000000000005f7bb192" - }, - { - "op": "delete", - "key": "0x00e2f41518226671332da4adddab1fdcb9903c7b895c7943b669067fe96b0baacfda" - }, - { - "op": "set", - "key": "0xff1df77729bb76b139342cfc86aff011590bf6bb810f47e69a6babac30af96f9f5ffa1e869c4566231d09357daec90e71aa93821a7347b29175ef4a1bc02a0b38114", - "value": "0x00000000000000000000000000000000000000000000000000000000c885cdce" - }, - { - "op": "set", - "key": "0x007293ce391f5cd652216ca8ea31069a06a82f393028dfc8786aa5efd7be734b2c7e", - "value": "0x00000000000000000000000000000000000000000000000000000000403292a8" - }, - { - "op": "set", - "key": "0x0018e25b9b05a714bd17ede1445ee7c785983a0194a3ed103783662bdc401dd7ffc3", - "value": "0x00000000000000000000000000000000000000000000000000000000f61897b9" - }, - { - "op": "delete", - "key": "0x009c847c439d6431d36ddeb326cb553da2e967778a24afbd1d23a62412186faf018c" - }, - { - "op": "set", - "key": "0xff0ed33feae99b046218c56bec56750a98beb5fd6a51ec13218024cb2455586b437ef4a13c0f00718e5b1d19138c7f9db57ed5b04d19dca824d5e61fc17dc47ff971", - "value": "0x000000000000000000000000000000000000000000000000000000002c85a3c9" - }, - { - "op": "delete", - "key": "0x00412c4d14e5bde6ea4c898a1d0f911f12a2cf775a62e3cc4c0f28331d432e8f100e" - }, - { - "op": "delete", - "key": "0xff4c1eb4996f3098c89ee461d61bd5f177587b0f4363ea73f4387e6296c66a260fed383baf24ff33220d52e65d0501f90eee41f67e456df12e8379bb13167e30a6e7" - }, - { - "op": "set", - "key": "0x0034bb4f3468340d5f72275da92420aed94db48bbc61823e92f29f14f51590373a4b", - "value": "0x000000000000000000000000000000000000000000000000000000005a0e27c1" - }, - { - "op": "set", - "key": "0x00e6b6d8d98be54d4c00cee378bedf964f755b5257bf1eabf52cf39aa987686fa725", - "value": "0x00000000000000000000000000000000000000000000000000000000831d5b5f" - }, - { - "op": "set", - "key": "0x0055bbfb5b05f80543bd662a7b7a74980a2992612798087fa24e1fc86b8e8508e4f2", - "value": "0x000000000000000000000000000000000000000000000000000000005f00f860" - }, - { - "op": "set", - "key": "0x00e57860ba0adcdd1294caf74da8b990a80a8a6861bc01b30dfe07450d25bab38fd7", - "value": "0x000000000000000000000000000000000000000000000000000000000e8e281a" - }, - { - "op": "set", - "key": "0x00e57860ba0adcdd1294caf74da8b990a80a8a6861bc01b30dfe07450d25bab38f0e", - "value": "0x0000000000000000000000000000000000000000000000000000000069f1fc26" - }, - { - "op": "delete", - "key": "0x00e6b6d8d98be54d4c00cee378bedf964f755b5257bf1eabf52cf39aa987686fa725" - }, - { - "op": "set", - "key": "0x003cbfd03e3f083b0987f841c4372031faf655a63472518a3613e2e30ecb542b51c0", - "value": "0x000000000000000000000000000000000000000000000000000000001200f8f6" - }, - { - "op": "set", - "key": "0xff46aeb3243f905d4e0eac7f60ee8962848c87e79dfbd019ece14fe45bd720ea6b61214a55d06d98ad4c6ea565d0f88368bc538d91abad58d513e9bcf56ceb156575", - "value": "0x0000000000000000000000000000000000000000000000000000000076a8cd24" - }, - { - "op": "delete", - "key": "0x000ed33feae99b046218c56bec56750a98beb5fd6a51ec13218024cb2455586b4307" - }, - { - "op": "set", - "key": "0xff34bb4f3468340d5f72275da92420aed94db48bbc61823e92f29f14f51590373a648ee142c16b327f9c3d09cc065560a309b3819c017803778f2b8c150cfe486a5e", - "value": "0x00000000000000000000000000000000000000000000000000000000032c6329" - }, - { - "op": "set", - "key": "0xff4b83f061efb2f4708114e46b5ef50a255528c28c80bbe5fc6ad88e892bd4c4ea37491f84eb77d352d7d8fca0484b856ceab7236e2f773d157b3fbd6d0586de7f96", - "value": "0x0000000000000000000000000000000000000000000000000000000073ed547b" - }, - { - "op": "delete", - "key": "0xffe2f41518226671332da4adddab1fdcb9903c7b895c7943b669067fe96b0baacfab2dcf341b3549396a13ac48963cf4aac70fda23d99a2c2a599e0efe17278e795a" - }, - { - "op": "delete", - "key": "0xff5963897639cd11d686b199366a1249d76bc89e0f7161c9eca48d9a199435952d359cb4900b950c37e7546543cb5a55b8a4e32316f9ddc7cb39743747f93009f45d" - }, - { - "op": "set", - "key": "0xff4c1eb4996f3098c89ee461d61bd5f177587b0f4363ea73f4387e6296c66a260f84715f048d0204439e2d98c4c2e5cb144730a98fb2ee40b399d12f783c1fbd31e7", - "value": "0x0000000000000000000000000000000000000000000000000000000038aa3fcb" - }, - { - "op": "set", - "key": "0x00ec64b2de1fc1d53b795f49a1c84ca4172bc3292472c3e244e4f8c225fa786f56e4", - "value": "0x000000000000000000000000000000000000000000000000000000000a42e584" - }, - { - "op": "set", - "key": "0x005ac76a8f23131e4ca988c8afcc313130cc95e521d2691635fc26196d63e66a1fc6", - "value": "0x00000000000000000000000000000000000000000000000000000000a828d5b7" - }, - { - "op": "delete", - "key": "0xffbded9122e6171a5dad9ce2aaebd933abf4718e9bf12448cf113064c9fd8e6ad064269e16de5ef26ae08b5660f7eaa784399543edd7bf8fbe04c13ac444cb4e61a6" - }, - { - "op": "set", - "key": "0x007b1eefd69ea014244937aaeaea4e8d4d296c04edd75ae47897e5612ef5a1a25230", - "value": "0x00000000000000000000000000000000000000000000000000000000297aaea4" - }, - { - "op": "set", - "key": "0x0055a6923f7414ba482ae8a64b407f386a0bb0491b742bd4b22b5b4b44fca84e8403", - "value": "0x000000000000000000000000000000000000000000000000000000001943ef67" - }, - { - "op": "set", - "key": "0xff543907253acb230f5dc96bbafd8ea0f37dd63b30e8a4a3675bb007673a31fbc5047362ff01233f0839180435b0ef58ba27ec43176ea05859f1535020e7ea58cbac", - "value": "0x00000000000000000000000000000000000000000000000000000000e22b6692" - }, - { - "op": "set", - "key": "0xff5ac76a8f23131e4ca988c8afcc313130cc95e521d2691635fc26196d63e66a1f6f3ed9ab4dceab32c8dee7d62338019a5f29043f855967151e0dc6468ab0237da4", - "value": "0x00000000000000000000000000000000000000000000000000000000b6c69c67" - }, - { - "op": "delete", - "key": "0x00e57860ba0adcdd1294caf74da8b990a80a8a6861bc01b30dfe07450d25bab38f0c" - }, - { - "op": "set", - "key": "0x00a43fb2d7beb12b34786a837467f62b7d6c5dab4ab7eecc89c0b67db76a6829270d", - "value": "0x0000000000000000000000000000000000000000000000000000000090cd692a" - }, - { - "op": "set", - "key": "0x005963897639cd11d686b199366a1249d76bc89e0f7161c9eca48d9a199435952d26", - "value": "0x00000000000000000000000000000000000000000000000000000000dfd40afd" - }, - { - "op": "set", - "key": "0xffe57860ba0adcdd1294caf74da8b990a80a8a6861bc01b30dfe07450d25bab38ff0e402bb5b5f30e2141ac064e95a9b5d86bbeab68daeb62a188811213392891f31", - "value": "0x000000000000000000000000000000000000000000000000000000001f5f4611" - }, - { - "op": "set", - "key": "0xff18e25b9b05a714bd17ede1445ee7c785983a0194a3ed103783662bdc401dd7ffd358410b57fde52d1120a72e241687991ced2ec523b82168713b13b557349ab84a", - "value": "0x00000000000000000000000000000000000000000000000000000000cf2387d4" - }, - { - "op": "set", - "key": "0x00e57860ba0adcdd1294caf74da8b990a80a8a6861bc01b30dfe07450d25bab38f3f", - "value": "0x00000000000000000000000000000000000000000000000000000000a55c3e7f" - }, - { - "op": "set", - "key": "0xff5963897639cd11d686b199366a1249d76bc89e0f7161c9eca48d9a199435952dfaf15a1ae0460906134a099130644594d04402f7e11b9ce07e2994acd12b870257", - "value": "0x000000000000000000000000000000000000000000000000000000008292d374" - }, - { - "op": "delete", - "key": "0x00e57860ba0adcdd1294caf74da8b990a80a8a6861bc01b30dfe07450d25bab38f0e" - }, - { - "op": "set", - "key": "0x00998f08d7054351e5fa3ccadda32ab8d6c0c86a825dd360d57c0fe6b46612297cc2", - "value": "0x000000000000000000000000000000000000000000000000000000004e4778f9" - }, - { - "op": "delete", - "key": "0x00e57860ba0adcdd1294caf74da8b990a80a8a6861bc01b30dfe07450d25bab38fd7" - }, - { - "op": "set", - "key": "0x004c1eb4996f3098c89ee461d61bd5f177587b0f4363ea73f4387e6296c66a260ffc", - "value": "0x0000000000000000000000000000000000000000000000000000000042d5f040" - }, - { - "op": "set", - "key": "0xff0ed33feae99b046218c56bec56750a98beb5fd6a51ec13218024cb2455586b437c0a8ed54087e88eba7c638f4ae6c57295dafe6c27063966f0c3238dae958d77d6", - "value": "0x000000000000000000000000000000000000000000000000000000007759aa37" - }, - { - "op": "set", - "key": "0x00ec64b2de1fc1d53b795f49a1c84ca4172bc3292472c3e244e4f8c225fa786f56ca", - "value": "0x0000000000000000000000000000000000000000000000000000000014f8a61a" - }, - { - "op": "delete", - "key": "0xff2cebacb183e2f51a3b6cb9f7fe2bd11c399bf1bdb8ce95ed457d3e46190197a0a0a0268860a87b73c5d023e0181059e1d49f8ed387a132e90d5ccf7b7589f952f2" - } - ], - "roots_after": [ - "0x8a7b32f0179f8591d8d29bbc5ce478d2d1cca5ceebddb5c6880e6682fd1a7458", - "0x63e670849972d58347a67cb04d30b369145ea59ac06fd411c231b46c05b534c8", - "0xeedfc71a50202763dc37c74f994b52783a5e3ac19ff7c3d818c9bd2c95b562f7", - "0x1cfbe1c8b87bb9ecafd506ee003ddaeef688eaa24a564cd7147270130425709e", - "0xf8daed5ef73e34987561ea33d980ed96600f48700aa75ea5706c74f79babdb3d", - "0xcf348977fb91b672c061404e5bc6f063a444d19478b4aba7450531d48ae5b8f8", - "0xfa4116de24b5dce1e1ebb9736c78a00ec1eb957aaee7e9f9af3b74f68e43a73e", - "0x0000000000000000000000000000000000000000000000000000000000000000", - "0x5f2fc0f08e034ab42e6b4b8eec8038618707b8f2ae9722e14a6048d1935a438c", - "0xbc0812ac0dbf193721b4e90a5f23d702399df82d1e1da571c9e2388155b5409f", - "0xe105f7395ae6845ec524e9dcdf9a33600d7eece3b9dacaf7fee43deca2ad0957", - "0x5f30927f1062975cf9ad72910c99386ffc609b4dac5e1633a0a7a051ff221ca1", - "0x6f8469d0d9572b492c6c170c43f734aea5714da7d2e47ac9c10a01938439a1cb", - "0x7a75f5784c851040e436de70bafabe4abddc0baab0db46583879fdc1a7bcbd01", - "0xaa03aca82638a0c50233cb98152c122b0c21c4efb2a47035a690b5a65a2e54ed", - "0x7a75f5784c851040e436de70bafabe4abddc0baab0db46583879fdc1a7bcbd01", - "0xf6d984c0ed4b562ad15789d556f759f72b44627fb197780e88d36f450c60cd4e", - "0x52ae36bcfdb7559ffaf993e3d23b7727ada4f1403f12a9f16ba0324610852a8e", - "0x7d48f61dc2656ca3fc52efbb16bf80b4806cf43aaef56397c9d526d739ab2ad4", - "0x867a92986423254fd9f362a682695d3eb986bd5106cadabbd72dd0faf70c0cf9", - "0xdd8d43ad115e1d5602e4a0f7cbfccd29da88f2284373e37278ac2ce5f28131e2", - "0x51ac92480dcdb4b73e8719cc66828fb8432c4f3e95b46bfd6188c95a57274841", - "0x14009bff462d71b74de902e736e1778e890b278e4431efe45214929624bb481d", - "0xced79329ddc4ae49289ee09b8f62294f1f58ad05d083bf234ab13940aebb9107", - "0x917f45ace41a685469e386217064d3e0f0a3a032cea58693662477712677db41", - "0x41ee495051942cc30f2917f35bd8c47e6410db43a72349251bb28c07bcc8f7c0", - "0x5386f094b07d2f66e2ba710c22adf75d674e5480ce8eb7d9ce00562b7ccdbbda", - "0x932786f0f35419fa30390e38d105809200011124fa0b6d5f8637b9b94487d9c3", - "0xb2b13dd966509e731383fd5e711ef659424c6a3a2be5e5340106030c9f519d5f", - "0x2462473775c4fdaef9a43b9cfdb52de90adbbc1bbe9b83f52486ac21730668bc", - "0xdf310f60d7c0f7098478fdc1555ff8da49e7a8f8c2a72d5f8c131413f7f1e3d3", - "0x2020b692ff595ae9314cde1600fc0bc2b873005510b9f08c8d337d6722d70a0e", - "0xdf310f60d7c0f7098478fdc1555ff8da49e7a8f8c2a72d5f8c131413f7f1e3d3", - "0xc7228f7de8004b8a65ca83153be3e81879f06d2d6cd892cba1b1de9aba11614b", - "0x3feeb22432b94e3606acede5ca9746bef4f66e9cdefb20f33f74067127b0a410", - "0xd34ed045b9a3328b8ec8eec5ebdada6a7fd96f53a440f8936b8dd04b03af5cc6", - "0x1b678937a3a948e9ce99fade1a5d0dc150940113738943156f10774ae45f47ce", - "0x1fe1978bef4bc11e93d0a05934465817ebc45300017f2932193522245de21654", - "0x171f50e2cefe8e1be7b3a23f4a20784399dbe23a1c739f3969949601f1aae309", - "0xdb90b4c3ce181ad457acb4793acf3c96948787b70af018dafa0c7bab14f39a8a", - "0x1322106c7281a64177a3dac41747ca47cd91b4a38a8ea205b1fb2194e0fe7ca3", - "0x0221eb75c0dc23bf40b5188be6b92d19a0e53caf128844124a9172d80f1b2725", - "0xb20b2aba4193b97f68469528a50b13a5f7a144b58b76cd264fbb6126eea66caa", - "0x4be94b29b40ae37b08e286933b5750eb17386823de830d42f7fd7ae82fb80212", - "0x3739fae016f7cbdd579fa2e23272918307447402ef643d81498743db90d313b2", - "0x842465452bd071e6e1f4e41e4552c5612705e7410f81fd17cc63f328a344bc0d", - "0xf746f4b36fb573b026e8bd423db2cfb6e6e7f2ab90991e0f9c1107d673bf7c47", - "0xf65214fc8cb48d01e283effa100f78cfb3047c184fcf22747844163b5ca68f35", - "0xac4073afc5233967fa44dbf0d717c61bf5c646593432c00ec7bef7de47ef5148", - "0x577a76c8fe61bbf59c1b75f4094b4d46014963bf3e3158ec626448f8478d8a76", - "0x5f7af52cb7684df6c01564adab0c0156b71c1a38b3ea56588227e72e3a163487", - "0xfb0618f4c70280db25a8701cebca666b38789966de14153a7110a421c2b48ff1", - "0xbf53d3b4f608fb5526a6a3b2efa79bac939c7b3103fb37e6a5902f3c78e35139", - "0x5facbaf4086e047085733d0b9c7fdb06d3480ccce1644b6fa9f05f857fafe11d", - "0x59f709233c06f71ecd6d8cf51c19016a61c7f6f292fdddc07d6cbd8f0d94fa4f", - "0xf983bc0c064b283f60e9271be36e08eb9a5bb571b6a782b16e40437cf1b277bb", - "0x1caa8a43868e55fdc665d578049ae2366bbd0f318b7ebc2767fc126db9bb2201", - "0x75afded3b32a06e02374a73111c3b5ade73cc4c70aac934063249d347415b4fd", - "0xfa8575b9894f1680e02b02a8c61f36e2e52b891d465cf024b7781333bebd84c5", - "0xa9d5eaa996a13f1e5bc624c337f7b074f88e429a8f3e6bea8a6103dfc7297310", - "0x29d66013eb0e1d6d1019bb1f6c17b94c4b2601c5a31c074581a001dcb1b3de5f", - "0xea4ae741440e0ae8d17c6fb256c866a81d8ef5ffe5b3879d7a0f2cad6090bc83", - "0x91f0332b7b3c046dbdcf5df5d2fde7b2fcf396d066c301ac168136984d900acf", - "0xde834346818b07a71de99175727b41e806510a7c047f55b8ae9454523edef70f", - "0x6af4615582e45a60cc1202dfb98da0b93bdd20629741120fa617c5999886ed85", - "0x9867e8f7bcf9017543ddaf6d048fc64c7428678278dffe7524fb825ce926e483", - "0x01e68be3c4441bd42d76e9bdbeaed3e4487160f1dcc48bd6300032945a41178d", - "0x91abb96562994918dafac8a092d5980c5441d5c37ddaf2352ab13dd21a7c85d3", - "0xdfb62a7c0cfc7411122412d434d5b94ed73a2727f8ee78f1d58acb5288efa174", - "0xafed8a54526bd8f6ce3495ea0f7fdee4c19df5840d5161cc11f9168dc941c21a", - "0xfc2ad7e489591b7543d5649beb07a5d5b2d2ca3fe038eaabbf3f8a247f556830", - "0x9bfc12a4cc0111a48bebf15b2c6562df93ed27eaf10387e34f416f34db7bbf49", - "0x82dc9cd4de2d35630b703499f115a14ddc70abcd86308eaced3aa5346e941f9b", - "0xa9d39a8860d003f370ee04708164638fe0a1f63735b6c7061edb4548affd065a", - "0xdab9d0b5901745fa368b0817baa8b4073220412dad4c366363433adc01dabe9a", - "0x030c73731e872cfe146a7f65f571064aedcc68580b4e9dd5b8175651ca15812f", - "0xf21f6d27d9e74362225ebedfda1757f45e9f7fd2753020e00cd0624f372add26", - "0xc236e92ae41f64008b3859ca59e886509ac20edba5160f6f8d86f8a177dd49b6", - "0x67fc41ed6bd66bbd47633607fa5efbd7011f041bdde718c52031d654fb1ff151", - "0xed616c4e45d6daf19f41f6f8e6be1e4a6e78d5482cb2349627cfed27b40220d7", - "0x5920100f2b7d3fdd8daaaf382632bf947ac1778883d29dc52d7b3b27b8322ca0", - "0x8e532a0ddfaf3275cf424330e9e28d127c7d4b7ba85336e4d7670d8b6a51d89e", - "0x6408ac7fb35a580b444a9eca3a0cdaf0ea80f9d28c6f2fa2b36bd5645dc934ca", - "0xe086cb5fcd0296229f346e58643654260380a1b5d2669099527d82b1a2c887e5", - "0xa655d19ec4e965e2ee228b99106cceedf07003a6becd8d9aad9c998a99fa287f", - "0x0e42e29aeb5388d65880a46e5c2fed00a2e295a7940b4fd4c8dd1201fe4021aa", - "0xc3ccd5e787d5068a6a7bc3ade783f4d00d3dc327f460c65b862514bb6bca7c5f", - "0x594af2c7d5cbc00a2fad3e8f18fa9388a7fbb67eb2437cc30b53f81d1c93e6b2", - "0x2ed46b1eaf76e89f682b9dfbc25418293d0fc20e31f6a8e8aa2e5845a00d1950", - "0xe399fc2d7afdee74b6df09f3efa449e58161fc519827ce443b8d3ac57c44b8d3", - "0x798e93dd492fc434a20c4d308971053a422dcd0b4bc9edd89b975de463267f2f", - "0xd9bb71ae1746bd82e10cca262fdac1faeda58873c0d9de075a9c3c1f383bc858", - "0xbbd2b74b988517a668ac462803252cbcaffde0db93bcefab724f8402dffba5bd", - "0x4570cab3731f10a05c2716de55913730dbc26d6f6feedd6d5cdc972fd6dd2cbc", - "0x23388945017b27a621948d87d133b093552c07bd3b6f2b8e3c6ca7835968114f", - "0x260cb6770d72773f4bc459d1667ac3b61c0f369da18020873a09341c9dec46b9", - "0x197ca877b5028bb013c9c2fb6148d67952c84ba735229575884542e851f3b7e7", - "0xbe6fc89c1a1d2d8168902b4df0048f1131d55f5005b995acbcf6520cc56588b0", - "0xdcc783c7a321e4a25ff1c01dc76f4cd181a51bdf63da44482a4cbf558ec685c3", - "0x47b4242152028429ee980756d40f0a95cfba0a74ff81feb5eb2fd51c4dedba39", - "0xa75f30073712bf5cfeb82c970cd31c36786957208cdb523319508de0698d9a6b", - "0xc6768062ef64adc6c13ec45533cc572b1bc5e0a53d8c7de89c278a835eaf084b", - "0xcb8be0789b86fd77269317ca0ab45433e21a34ff93a7ccb40ea574321a0cd2c0", - "0x97f6b18446e834d19f4497767121f2958c4c0ac339032d5e22b5d06aff83f95e", - "0x1ca598ac8d7443f59616a6c63f8de685b0669e5c6235a2eb3f47a70eeff645de", - "0x167aea5319ec96368f15c71072f452ed9015d596181fd49bf08fa0f47eb10d85", - "0xb9b580ab6672e02107f34acc6e0eb7bae701d2983ceac909173e266e66e8259a", - "0xd9a234e59234c79087c138af5f3eb61d4cb4170efd38635d37c8e759041e73fe", - "0x5b5a60d80b39452f9becb79c199ab04c54bcd261c2e2d1ebc44922518611d60d", - "0xe6c6cab4030aff1b796a1c5d6ae059e1d5f236cc8ce5fafa0866618a136fccbd", - "0xc0d78b67dfb2f0b86ce05922a83a4098b57efd8eb7ef5f05b70cb079437f4647", - "0x25ea70ac1e50d4896bc15b3645f48bef2d704e4017ad25134bd332dcf3646ca3", - "0xe74f0e7d2643c724a0cbacb67e3c19921b32e5708363125c1482b8bee682282c", - "0x70d1b0558b82f83937605a190f1f291470cd311048b66073549200dd6e63406c", - "0xfd5dfa769c89b01540599dc1eac8aae8954a3f5661409bb4b93e793595704ec3", - "0x089c9d9d68a5eacde9efe25c09222683403ee921b8b2aabd8479ec7c12c69106", - "0x26f25564073873edf0f929acc35900b5564acbbfeeae417ea18ef1954a067d17", - "0x8be2cd628bdfac918e03e2201586ffe6528bd810dfbe45f6ede931e540c22700", - "0x8a5b325fc93b1919769121e118c05a4915122554089554497f01e2b66026cf6d", - "0x43fecbe443116f26fbbe022f4474d977467d6cece3cbc53b579ca840f6cd4f67" - ] - } - ], - "embedding_vectors": { - "address": "0xaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa", - "basic_data_key": "0x00d9ae2d236f8713a5bf808cda488167a56cc97e4b83006f42b1c06c0c3f053bbf00", - "code_hash_key": "0x00d9ae2d236f8713a5bf808cda488167a56cc97e4b83006f42b1c06c0c3f053bbf01", - "slots": [ - { - "slot": 0, - "key": "0x00d9ae2d236f8713a5bf808cda488167a56cc97e4b83006f42b1c06c0c3f053bbf40" - }, - { - "slot": 5, - "key": "0x00d9ae2d236f8713a5bf808cda488167a56cc97e4b83006f42b1c06c0c3f053bbf45" - }, - { - "slot": 63, - "key": "0x00d9ae2d236f8713a5bf808cda488167a56cc97e4b83006f42b1c06c0c3f053bbf7f" - }, - { - "slot": 64, - "key": "0xffd9ae2d236f8713a5bf808cda488167a56cc97e4b83006f42b1c06c0c3f053bbf181495ef75f8d2005a4a5bd3c5139cf6b5cdd1a02574832253cc9932687b332a40" - }, - { - "slot": 255, - "key": "0xffd9ae2d236f8713a5bf808cda488167a56cc97e4b83006f42b1c06c0c3f053bbf181495ef75f8d2005a4a5bd3c5139cf6b5cdd1a02574832253cc9932687b332aff" - }, - { - "slot": 256, - "key": "0xffd9ae2d236f8713a5bf808cda488167a56cc97e4b83006f42b1c06c0c3f053bbfe717382bfeaa9f4c014431fb7bbc3c6946dd510a82d49c925d9df7735c26b16f00" - }, - { - "slot": 1000, - "key": "0xffd9ae2d236f8713a5bf808cda488167a56cc97e4b83006f42b1c06c0c3f053bbf2b1a6c18962d993b9da5ed300ce5696bdcad6e09f08b9c0735fa749d417650f9e8" - }, - { - "slot": 57896044618658097711785492504343953926634992332820282019728792003956564819968, - "key": "0xffd9ae2d236f8713a5bf808cda488167a56cc97e4b83006f42b1c06c0c3f053bbf23d37150867b80a99fea951c62d43f974e7d2f53089c5f5683b4fc1d4387286c00" - } - ], - "chunks": [ - { - "chunk": 0, - "key": "0x00d9ae2d236f8713a5bf808cda488167a56cc97e4b83006f42b1c06c0c3f053bbf80" - }, - { - "chunk": 5, - "key": "0x00d9ae2d236f8713a5bf808cda488167a56cc97e4b83006f42b1c06c0c3f053bbf85" - }, - { - "chunk": 127, - "key": "0x00d9ae2d236f8713a5bf808cda488167a56cc97e4b83006f42b1c06c0c3f053bbfff" - }, - { - "chunk": 128, - "key": "0x01465f911ccfe7707c51a877fc621cffa8e700a5828cf92df27b142de919c0f6c800" - }, - { - "chunk": 300, - "key": "0x01465f911ccfe7707c51a877fc621cffa8e700a5828cf92df27b142de919c0f6c8ac" - }, - { - "chunk": 383, - "key": "0x01465f911ccfe7707c51a877fc621cffa8e700a5828cf92df27b142de919c0f6c8ff" - }, - { - "chunk": 384, - "key": "0x0171ad9e407a8d200ab2858bcf828d9016a0b4bc54e6e39dd3e31847b6220a63ce00" - } - ] - }, - "basic_data_vectors": [ - { - "code_size": 0, - "nonce": 0, - "balance": "0", - "value": "0x0000000000000000000000000000000000000000000000000000000000000000" - }, - { - "code_size": 0, - "nonce": 1, - "balance": "1000000000000000000", - "value": "0x0000000000000000000000000000000100000000000000000de0b6b3a7640000" - }, - { - "code_size": 287454020, - "nonce": 6153737369425722316, - "balance": "1512366075204170929049582354406559215", - "value": "0x00000000112233445566778899aabbcc0123456789abcdef0123456789abcdef" - }, - { - "code_size": 24576, - "nonce": 1, - "balance": "1", - "value": "0x0000000000006000000000000000000100000000000000000000000000000001" - } - ], - "chunkify_vectors": [ - { - "name": "empty", - "code": "0x", - "chunks": [] - }, - { - "name": "short", - "code": "0x6001", - "chunks": [ - "0x0060010000000000000000000000000000000000000000000000000000000000" - ] - }, - { - "name": "push_boundary", - "code": "0x6060606060606060606060606060606060606060606060606060606060606060606060606060606060606060606060606060606060606060606060606060", - "chunks": [ - "0x0060606060606060606060606060606060606060606060606060606060606060", - "0x0160606060606060606060606060606060606060606060606060606060606060" - ] - }, - { - "name": "push32_tail", - "code": "0x7feeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeee", - "chunks": [ - "0x007feeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeee", - "0x02eeeeeeeeeeeeeeeeeeee000000000000000000000000000000000000000000" - ] - }, - { - "name": "zeros62", - "code": "0x0000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000", - "chunks": [ - "0x0000000000000000000000000000000000000000000000000000000000000000", - "0x0000000000000000000000000000000000000000000000000000000000000000" - ] - } - ] -} \ No newline at end of file +{"meta":{"source":"execution-specs@ec412acfd (branch eip-8297-tests)","hasher":"blake3","generator":"export_vectors.py"},"empty_root":"0x0000000000000000000000000000000000000000000000000000000000000000","trie_vectors":[{"name":"empty","entries":[],"root":"0x0000000000000000000000000000000000000000000000000000000000000000"},{"name":"single_account_leaf","entries":[{"key":"0x0055bbfb5b05f80543bd662a7b7a74980a2992612798087fa24e1fc86b8e8508e400","value":"0x0000000000000000000000000000000000000000000000000000000000000007"}],"root":"0x22da077ee89a0e6e6259303169fb6c7a3133fafa3033515a355af9c4f9ed5ea0"},{"name":"one_header_stem_two_leaves","entries":[{"key":"0x0055bbfb5b05f80543bd662a7b7a74980a2992612798087fa24e1fc86b8e8508e400","value":"0x0000000000000000000000000000000000000000000000000000000000000007"},{"key":"0x0055bbfb5b05f80543bd662a7b7a74980a2992612798087fa24e1fc86b8e8508e401","value":"0x0000000000000000000000000000000000000000000000000000000000000009"}],"root":"0x3599d1dd23fef634f3845fd935375be8a42169ecc90906632aa8afa2fa380812"},{"name":"two_accounts","entries":[{"key":"0x0055bbfb5b05f80543bd662a7b7a74980a2992612798087fa24e1fc86b8e8508e400","value":"0x0000000000000000000000000000000000000000000000000000000000000007"},{"key":"0x0034bb4f3468340d5f72275da92420aed94db48bbc61823e92f29f14f51590373a00","value":"0x0000000000000000000000000000000000000000000000000000000000000008"}],"root":"0xbcca03773c89779e136f983eba516339660c732d5b0a1ccfc2c725dca29eefbc"},{"name":"cross_zone_small","entries":[{"key":"0x0055bbfb5b05f80543bd662a7b7a74980a2992612798087fa24e1fc86b8e8508e400","value":"0x0000000000000000000000000000000000000000000000000000000000000001"},{"key":"0x0055bbfb5b05f80543bd662a7b7a74980a2992612798087fa24e1fc86b8e8508e401","value":"0x0000000000000000000000000000000000000000000000000000000000000002"},{"key":"0xff55bbfb5b05f80543bd662a7b7a74980a2992612798087fa24e1fc86b8e8508e406f5e5117ba26652e3cbef5ea24cc46f42709eb47be3c46f3a923b68a67f44c264","value":"0x0000000000000000000000000000000000000000000000000000000000000003"},{"key":"0xff55bbfb5b05f80543bd662a7b7a74980a2992612798087fa24e1fc86b8e8508e4f1ce240e3efd0b0855a335ac6b58ea33be1b2afb193608d6d21fd91308dd74bc64","value":"0x0000000000000000000000000000000000000000000000000000000000000004"},{"key":"0x0034bb4f3468340d5f72275da92420aed94db48bbc61823e92f29f14f51590373a00","value":"0x0000000000000000000000000000000000000000000000000000000000000005"}],"root":"0x3fc0f35560e81b2b1bc349decef42eb48a614d8497646e401e7b85abc0b16a30"},{"name":"zero_value_present","entries":[{"key":"0x00a43fb2d7beb12b34786a837467f62b7d6c5dab4ab7eecc89c0b67db76a68292700","value":"0x0000000000000000000000000000000000000000000000000000000000000000"}],"root":"0x343a84978f71225f27f6dbdd2e0dd603a2ae3b83028a907ae0f8f4db262c9d13"},{"name":"full_header_stem","entries":[{"key":"0x004c3a8541d9ec369a9fd4bcdad885cc3b166da70ed6a83066924727d9ff147fce00","value":"0x0000000000000000000000000000000000000000000000000000000000000001"},{"key":"0x004c3a8541d9ec369a9fd4bcdad885cc3b166da70ed6a83066924727d9ff147fce01","value":"0x0000000000000000000000000000000000000000000000000000000000000002"},{"key":"0x004c3a8541d9ec369a9fd4bcdad885cc3b166da70ed6a83066924727d9ff147fce02","value":"0x0000000000000000000000000000000000000000000000000000000000000003"},{"key":"0x004c3a8541d9ec369a9fd4bcdad885cc3b166da70ed6a83066924727d9ff147fce03","value":"0x0000000000000000000000000000000000000000000000000000000000000004"},{"key":"0x004c3a8541d9ec369a9fd4bcdad885cc3b166da70ed6a83066924727d9ff147fce04","value":"0x0000000000000000000000000000000000000000000000000000000000000005"},{"key":"0x004c3a8541d9ec369a9fd4bcdad885cc3b166da70ed6a83066924727d9ff147fce05","value":"0x0000000000000000000000000000000000000000000000000000000000000006"},{"key":"0x004c3a8541d9ec369a9fd4bcdad885cc3b166da70ed6a83066924727d9ff147fce06","value":"0x0000000000000000000000000000000000000000000000000000000000000007"},{"key":"0x004c3a8541d9ec369a9fd4bcdad885cc3b166da70ed6a83066924727d9ff147fce07","value":"0x0000000000000000000000000000000000000000000000000000000000000008"},{"key":"0x004c3a8541d9ec369a9fd4bcdad885cc3b166da70ed6a83066924727d9ff147fce08","value":"0x0000000000000000000000000000000000000000000000000000000000000009"},{"key":"0x004c3a8541d9ec369a9fd4bcdad885cc3b166da70ed6a83066924727d9ff147fce09","value":"0x000000000000000000000000000000000000000000000000000000000000000a"},{"key":"0x004c3a8541d9ec369a9fd4bcdad885cc3b166da70ed6a83066924727d9ff147fce0a","value":"0x000000000000000000000000000000000000000000000000000000000000000b"},{"key":"0x004c3a8541d9ec369a9fd4bcdad885cc3b166da70ed6a83066924727d9ff147fce0b","value":"0x000000000000000000000000000000000000000000000000000000000000000c"},{"key":"0x004c3a8541d9ec369a9fd4bcdad885cc3b166da70ed6a83066924727d9ff147fce0c","value":"0x000000000000000000000000000000000000000000000000000000000000000d"},{"key":"0x004c3a8541d9ec369a9fd4bcdad885cc3b166da70ed6a83066924727d9ff147fce0d","value":"0x000000000000000000000000000000000000000000000000000000000000000e"},{"key":"0x004c3a8541d9ec369a9fd4bcdad885cc3b166da70ed6a83066924727d9ff147fce0e","value":"0x000000000000000000000000000000000000000000000000000000000000000f"},{"key":"0x004c3a8541d9ec369a9fd4bcdad885cc3b166da70ed6a83066924727d9ff147fce0f","value":"0x0000000000000000000000000000000000000000000000000000000000000010"},{"key":"0x004c3a8541d9ec369a9fd4bcdad885cc3b166da70ed6a83066924727d9ff147fce10","value":"0x0000000000000000000000000000000000000000000000000000000000000011"},{"key":"0x004c3a8541d9ec369a9fd4bcdad885cc3b166da70ed6a83066924727d9ff147fce11","value":"0x0000000000000000000000000000000000000000000000000000000000000012"},{"key":"0x004c3a8541d9ec369a9fd4bcdad885cc3b166da70ed6a83066924727d9ff147fce12","value":"0x0000000000000000000000000000000000000000000000000000000000000013"},{"key":"0x004c3a8541d9ec369a9fd4bcdad885cc3b166da70ed6a83066924727d9ff147fce13","value":"0x0000000000000000000000000000000000000000000000000000000000000014"},{"key":"0x004c3a8541d9ec369a9fd4bcdad885cc3b166da70ed6a83066924727d9ff147fce14","value":"0x0000000000000000000000000000000000000000000000000000000000000015"},{"key":"0x004c3a8541d9ec369a9fd4bcdad885cc3b166da70ed6a83066924727d9ff147fce15","value":"0x0000000000000000000000000000000000000000000000000000000000000016"},{"key":"0x004c3a8541d9ec369a9fd4bcdad885cc3b166da70ed6a83066924727d9ff147fce16","value":"0x0000000000000000000000000000000000000000000000000000000000000017"},{"key":"0x004c3a8541d9ec369a9fd4bcdad885cc3b166da70ed6a83066924727d9ff147fce17","value":"0x0000000000000000000000000000000000000000000000000000000000000018"},{"key":"0x004c3a8541d9ec369a9fd4bcdad885cc3b166da70ed6a83066924727d9ff147fce18","value":"0x0000000000000000000000000000000000000000000000000000000000000019"},{"key":"0x004c3a8541d9ec369a9fd4bcdad885cc3b166da70ed6a83066924727d9ff147fce19","value":"0x000000000000000000000000000000000000000000000000000000000000001a"},{"key":"0x004c3a8541d9ec369a9fd4bcdad885cc3b166da70ed6a83066924727d9ff147fce1a","value":"0x000000000000000000000000000000000000000000000000000000000000001b"},{"key":"0x004c3a8541d9ec369a9fd4bcdad885cc3b166da70ed6a83066924727d9ff147fce1b","value":"0x000000000000000000000000000000000000000000000000000000000000001c"},{"key":"0x004c3a8541d9ec369a9fd4bcdad885cc3b166da70ed6a83066924727d9ff147fce1c","value":"0x000000000000000000000000000000000000000000000000000000000000001d"},{"key":"0x004c3a8541d9ec369a9fd4bcdad885cc3b166da70ed6a83066924727d9ff147fce1d","value":"0x000000000000000000000000000000000000000000000000000000000000001e"},{"key":"0x004c3a8541d9ec369a9fd4bcdad885cc3b166da70ed6a83066924727d9ff147fce1e","value":"0x000000000000000000000000000000000000000000000000000000000000001f"},{"key":"0x004c3a8541d9ec369a9fd4bcdad885cc3b166da70ed6a83066924727d9ff147fce1f","value":"0x0000000000000000000000000000000000000000000000000000000000000020"},{"key":"0x004c3a8541d9ec369a9fd4bcdad885cc3b166da70ed6a83066924727d9ff147fce20","value":"0x0000000000000000000000000000000000000000000000000000000000000021"},{"key":"0x004c3a8541d9ec369a9fd4bcdad885cc3b166da70ed6a83066924727d9ff147fce21","value":"0x0000000000000000000000000000000000000000000000000000000000000022"},{"key":"0x004c3a8541d9ec369a9fd4bcdad885cc3b166da70ed6a83066924727d9ff147fce22","value":"0x0000000000000000000000000000000000000000000000000000000000000023"},{"key":"0x004c3a8541d9ec369a9fd4bcdad885cc3b166da70ed6a83066924727d9ff147fce23","value":"0x0000000000000000000000000000000000000000000000000000000000000024"},{"key":"0x004c3a8541d9ec369a9fd4bcdad885cc3b166da70ed6a83066924727d9ff147fce24","value":"0x0000000000000000000000000000000000000000000000000000000000000025"},{"key":"0x004c3a8541d9ec369a9fd4bcdad885cc3b166da70ed6a83066924727d9ff147fce25","value":"0x0000000000000000000000000000000000000000000000000000000000000026"},{"key":"0x004c3a8541d9ec369a9fd4bcdad885cc3b166da70ed6a83066924727d9ff147fce26","value":"0x0000000000000000000000000000000000000000000000000000000000000027"},{"key":"0x004c3a8541d9ec369a9fd4bcdad885cc3b166da70ed6a83066924727d9ff147fce27","value":"0x0000000000000000000000000000000000000000000000000000000000000028"},{"key":"0x004c3a8541d9ec369a9fd4bcdad885cc3b166da70ed6a83066924727d9ff147fce28","value":"0x0000000000000000000000000000000000000000000000000000000000000029"},{"key":"0x004c3a8541d9ec369a9fd4bcdad885cc3b166da70ed6a83066924727d9ff147fce29","value":"0x000000000000000000000000000000000000000000000000000000000000002a"},{"key":"0x004c3a8541d9ec369a9fd4bcdad885cc3b166da70ed6a83066924727d9ff147fce2a","value":"0x000000000000000000000000000000000000000000000000000000000000002b"},{"key":"0x004c3a8541d9ec369a9fd4bcdad885cc3b166da70ed6a83066924727d9ff147fce2b","value":"0x000000000000000000000000000000000000000000000000000000000000002c"},{"key":"0x004c3a8541d9ec369a9fd4bcdad885cc3b166da70ed6a83066924727d9ff147fce2c","value":"0x000000000000000000000000000000000000000000000000000000000000002d"},{"key":"0x004c3a8541d9ec369a9fd4bcdad885cc3b166da70ed6a83066924727d9ff147fce2d","value":"0x000000000000000000000000000000000000000000000000000000000000002e"},{"key":"0x004c3a8541d9ec369a9fd4bcdad885cc3b166da70ed6a83066924727d9ff147fce2e","value":"0x000000000000000000000000000000000000000000000000000000000000002f"},{"key":"0x004c3a8541d9ec369a9fd4bcdad885cc3b166da70ed6a83066924727d9ff147fce2f","value":"0x0000000000000000000000000000000000000000000000000000000000000030"},{"key":"0x004c3a8541d9ec369a9fd4bcdad885cc3b166da70ed6a83066924727d9ff147fce30","value":"0x0000000000000000000000000000000000000000000000000000000000000031"},{"key":"0x004c3a8541d9ec369a9fd4bcdad885cc3b166da70ed6a83066924727d9ff147fce31","value":"0x0000000000000000000000000000000000000000000000000000000000000032"},{"key":"0x004c3a8541d9ec369a9fd4bcdad885cc3b166da70ed6a83066924727d9ff147fce32","value":"0x0000000000000000000000000000000000000000000000000000000000000033"},{"key":"0x004c3a8541d9ec369a9fd4bcdad885cc3b166da70ed6a83066924727d9ff147fce33","value":"0x0000000000000000000000000000000000000000000000000000000000000034"},{"key":"0x004c3a8541d9ec369a9fd4bcdad885cc3b166da70ed6a83066924727d9ff147fce34","value":"0x0000000000000000000000000000000000000000000000000000000000000035"},{"key":"0x004c3a8541d9ec369a9fd4bcdad885cc3b166da70ed6a83066924727d9ff147fce35","value":"0x0000000000000000000000000000000000000000000000000000000000000036"},{"key":"0x004c3a8541d9ec369a9fd4bcdad885cc3b166da70ed6a83066924727d9ff147fce36","value":"0x0000000000000000000000000000000000000000000000000000000000000037"},{"key":"0x004c3a8541d9ec369a9fd4bcdad885cc3b166da70ed6a83066924727d9ff147fce37","value":"0x0000000000000000000000000000000000000000000000000000000000000038"},{"key":"0x004c3a8541d9ec369a9fd4bcdad885cc3b166da70ed6a83066924727d9ff147fce38","value":"0x0000000000000000000000000000000000000000000000000000000000000039"},{"key":"0x004c3a8541d9ec369a9fd4bcdad885cc3b166da70ed6a83066924727d9ff147fce39","value":"0x000000000000000000000000000000000000000000000000000000000000003a"},{"key":"0x004c3a8541d9ec369a9fd4bcdad885cc3b166da70ed6a83066924727d9ff147fce3a","value":"0x000000000000000000000000000000000000000000000000000000000000003b"},{"key":"0x004c3a8541d9ec369a9fd4bcdad885cc3b166da70ed6a83066924727d9ff147fce3b","value":"0x000000000000000000000000000000000000000000000000000000000000003c"},{"key":"0x004c3a8541d9ec369a9fd4bcdad885cc3b166da70ed6a83066924727d9ff147fce3c","value":"0x000000000000000000000000000000000000000000000000000000000000003d"},{"key":"0x004c3a8541d9ec369a9fd4bcdad885cc3b166da70ed6a83066924727d9ff147fce3d","value":"0x000000000000000000000000000000000000000000000000000000000000003e"},{"key":"0x004c3a8541d9ec369a9fd4bcdad885cc3b166da70ed6a83066924727d9ff147fce3e","value":"0x000000000000000000000000000000000000000000000000000000000000003f"},{"key":"0x004c3a8541d9ec369a9fd4bcdad885cc3b166da70ed6a83066924727d9ff147fce3f","value":"0x0000000000000000000000000000000000000000000000000000000000000040"},{"key":"0x004c3a8541d9ec369a9fd4bcdad885cc3b166da70ed6a83066924727d9ff147fce40","value":"0x0000000000000000000000000000000000000000000000000000000000000041"},{"key":"0x004c3a8541d9ec369a9fd4bcdad885cc3b166da70ed6a83066924727d9ff147fce41","value":"0x0000000000000000000000000000000000000000000000000000000000000042"},{"key":"0x004c3a8541d9ec369a9fd4bcdad885cc3b166da70ed6a83066924727d9ff147fce42","value":"0x0000000000000000000000000000000000000000000000000000000000000043"},{"key":"0x004c3a8541d9ec369a9fd4bcdad885cc3b166da70ed6a83066924727d9ff147fce43","value":"0x0000000000000000000000000000000000000000000000000000000000000044"},{"key":"0x004c3a8541d9ec369a9fd4bcdad885cc3b166da70ed6a83066924727d9ff147fce44","value":"0x0000000000000000000000000000000000000000000000000000000000000045"},{"key":"0x004c3a8541d9ec369a9fd4bcdad885cc3b166da70ed6a83066924727d9ff147fce45","value":"0x0000000000000000000000000000000000000000000000000000000000000046"},{"key":"0x004c3a8541d9ec369a9fd4bcdad885cc3b166da70ed6a83066924727d9ff147fce46","value":"0x0000000000000000000000000000000000000000000000000000000000000047"},{"key":"0x004c3a8541d9ec369a9fd4bcdad885cc3b166da70ed6a83066924727d9ff147fce47","value":"0x0000000000000000000000000000000000000000000000000000000000000048"},{"key":"0x004c3a8541d9ec369a9fd4bcdad885cc3b166da70ed6a83066924727d9ff147fce48","value":"0x0000000000000000000000000000000000000000000000000000000000000049"},{"key":"0x004c3a8541d9ec369a9fd4bcdad885cc3b166da70ed6a83066924727d9ff147fce49","value":"0x000000000000000000000000000000000000000000000000000000000000004a"},{"key":"0x004c3a8541d9ec369a9fd4bcdad885cc3b166da70ed6a83066924727d9ff147fce4a","value":"0x000000000000000000000000000000000000000000000000000000000000004b"},{"key":"0x004c3a8541d9ec369a9fd4bcdad885cc3b166da70ed6a83066924727d9ff147fce4b","value":"0x000000000000000000000000000000000000000000000000000000000000004c"},{"key":"0x004c3a8541d9ec369a9fd4bcdad885cc3b166da70ed6a83066924727d9ff147fce4c","value":"0x000000000000000000000000000000000000000000000000000000000000004d"},{"key":"0x004c3a8541d9ec369a9fd4bcdad885cc3b166da70ed6a83066924727d9ff147fce4d","value":"0x000000000000000000000000000000000000000000000000000000000000004e"},{"key":"0x004c3a8541d9ec369a9fd4bcdad885cc3b166da70ed6a83066924727d9ff147fce4e","value":"0x000000000000000000000000000000000000000000000000000000000000004f"},{"key":"0x004c3a8541d9ec369a9fd4bcdad885cc3b166da70ed6a83066924727d9ff147fce4f","value":"0x0000000000000000000000000000000000000000000000000000000000000050"},{"key":"0x004c3a8541d9ec369a9fd4bcdad885cc3b166da70ed6a83066924727d9ff147fce50","value":"0x0000000000000000000000000000000000000000000000000000000000000051"},{"key":"0x004c3a8541d9ec369a9fd4bcdad885cc3b166da70ed6a83066924727d9ff147fce51","value":"0x0000000000000000000000000000000000000000000000000000000000000052"},{"key":"0x004c3a8541d9ec369a9fd4bcdad885cc3b166da70ed6a83066924727d9ff147fce52","value":"0x0000000000000000000000000000000000000000000000000000000000000053"},{"key":"0x004c3a8541d9ec369a9fd4bcdad885cc3b166da70ed6a83066924727d9ff147fce53","value":"0x0000000000000000000000000000000000000000000000000000000000000054"},{"key":"0x004c3a8541d9ec369a9fd4bcdad885cc3b166da70ed6a83066924727d9ff147fce54","value":"0x0000000000000000000000000000000000000000000000000000000000000055"},{"key":"0x004c3a8541d9ec369a9fd4bcdad885cc3b166da70ed6a83066924727d9ff147fce55","value":"0x0000000000000000000000000000000000000000000000000000000000000056"},{"key":"0x004c3a8541d9ec369a9fd4bcdad885cc3b166da70ed6a83066924727d9ff147fce56","value":"0x0000000000000000000000000000000000000000000000000000000000000057"},{"key":"0x004c3a8541d9ec369a9fd4bcdad885cc3b166da70ed6a83066924727d9ff147fce57","value":"0x0000000000000000000000000000000000000000000000000000000000000058"},{"key":"0x004c3a8541d9ec369a9fd4bcdad885cc3b166da70ed6a83066924727d9ff147fce58","value":"0x0000000000000000000000000000000000000000000000000000000000000059"},{"key":"0x004c3a8541d9ec369a9fd4bcdad885cc3b166da70ed6a83066924727d9ff147fce59","value":"0x000000000000000000000000000000000000000000000000000000000000005a"},{"key":"0x004c3a8541d9ec369a9fd4bcdad885cc3b166da70ed6a83066924727d9ff147fce5a","value":"0x000000000000000000000000000000000000000000000000000000000000005b"},{"key":"0x004c3a8541d9ec369a9fd4bcdad885cc3b166da70ed6a83066924727d9ff147fce5b","value":"0x000000000000000000000000000000000000000000000000000000000000005c"},{"key":"0x004c3a8541d9ec369a9fd4bcdad885cc3b166da70ed6a83066924727d9ff147fce5c","value":"0x000000000000000000000000000000000000000000000000000000000000005d"},{"key":"0x004c3a8541d9ec369a9fd4bcdad885cc3b166da70ed6a83066924727d9ff147fce5d","value":"0x000000000000000000000000000000000000000000000000000000000000005e"},{"key":"0x004c3a8541d9ec369a9fd4bcdad885cc3b166da70ed6a83066924727d9ff147fce5e","value":"0x000000000000000000000000000000000000000000000000000000000000005f"},{"key":"0x004c3a8541d9ec369a9fd4bcdad885cc3b166da70ed6a83066924727d9ff147fce5f","value":"0x0000000000000000000000000000000000000000000000000000000000000060"},{"key":"0x004c3a8541d9ec369a9fd4bcdad885cc3b166da70ed6a83066924727d9ff147fce60","value":"0x0000000000000000000000000000000000000000000000000000000000000061"},{"key":"0x004c3a8541d9ec369a9fd4bcdad885cc3b166da70ed6a83066924727d9ff147fce61","value":"0x0000000000000000000000000000000000000000000000000000000000000062"},{"key":"0x004c3a8541d9ec369a9fd4bcdad885cc3b166da70ed6a83066924727d9ff147fce62","value":"0x0000000000000000000000000000000000000000000000000000000000000063"},{"key":"0x004c3a8541d9ec369a9fd4bcdad885cc3b166da70ed6a83066924727d9ff147fce63","value":"0x0000000000000000000000000000000000000000000000000000000000000064"},{"key":"0x004c3a8541d9ec369a9fd4bcdad885cc3b166da70ed6a83066924727d9ff147fce64","value":"0x0000000000000000000000000000000000000000000000000000000000000065"},{"key":"0x004c3a8541d9ec369a9fd4bcdad885cc3b166da70ed6a83066924727d9ff147fce65","value":"0x0000000000000000000000000000000000000000000000000000000000000066"},{"key":"0x004c3a8541d9ec369a9fd4bcdad885cc3b166da70ed6a83066924727d9ff147fce66","value":"0x0000000000000000000000000000000000000000000000000000000000000067"},{"key":"0x004c3a8541d9ec369a9fd4bcdad885cc3b166da70ed6a83066924727d9ff147fce67","value":"0x0000000000000000000000000000000000000000000000000000000000000068"},{"key":"0x004c3a8541d9ec369a9fd4bcdad885cc3b166da70ed6a83066924727d9ff147fce68","value":"0x0000000000000000000000000000000000000000000000000000000000000069"},{"key":"0x004c3a8541d9ec369a9fd4bcdad885cc3b166da70ed6a83066924727d9ff147fce69","value":"0x000000000000000000000000000000000000000000000000000000000000006a"},{"key":"0x004c3a8541d9ec369a9fd4bcdad885cc3b166da70ed6a83066924727d9ff147fce6a","value":"0x000000000000000000000000000000000000000000000000000000000000006b"},{"key":"0x004c3a8541d9ec369a9fd4bcdad885cc3b166da70ed6a83066924727d9ff147fce6b","value":"0x000000000000000000000000000000000000000000000000000000000000006c"},{"key":"0x004c3a8541d9ec369a9fd4bcdad885cc3b166da70ed6a83066924727d9ff147fce6c","value":"0x000000000000000000000000000000000000000000000000000000000000006d"},{"key":"0x004c3a8541d9ec369a9fd4bcdad885cc3b166da70ed6a83066924727d9ff147fce6d","value":"0x000000000000000000000000000000000000000000000000000000000000006e"},{"key":"0x004c3a8541d9ec369a9fd4bcdad885cc3b166da70ed6a83066924727d9ff147fce6e","value":"0x000000000000000000000000000000000000000000000000000000000000006f"},{"key":"0x004c3a8541d9ec369a9fd4bcdad885cc3b166da70ed6a83066924727d9ff147fce6f","value":"0x0000000000000000000000000000000000000000000000000000000000000070"},{"key":"0x004c3a8541d9ec369a9fd4bcdad885cc3b166da70ed6a83066924727d9ff147fce70","value":"0x0000000000000000000000000000000000000000000000000000000000000071"},{"key":"0x004c3a8541d9ec369a9fd4bcdad885cc3b166da70ed6a83066924727d9ff147fce71","value":"0x0000000000000000000000000000000000000000000000000000000000000072"},{"key":"0x004c3a8541d9ec369a9fd4bcdad885cc3b166da70ed6a83066924727d9ff147fce72","value":"0x0000000000000000000000000000000000000000000000000000000000000073"},{"key":"0x004c3a8541d9ec369a9fd4bcdad885cc3b166da70ed6a83066924727d9ff147fce73","value":"0x0000000000000000000000000000000000000000000000000000000000000074"},{"key":"0x004c3a8541d9ec369a9fd4bcdad885cc3b166da70ed6a83066924727d9ff147fce74","value":"0x0000000000000000000000000000000000000000000000000000000000000075"},{"key":"0x004c3a8541d9ec369a9fd4bcdad885cc3b166da70ed6a83066924727d9ff147fce75","value":"0x0000000000000000000000000000000000000000000000000000000000000076"},{"key":"0x004c3a8541d9ec369a9fd4bcdad885cc3b166da70ed6a83066924727d9ff147fce76","value":"0x0000000000000000000000000000000000000000000000000000000000000077"},{"key":"0x004c3a8541d9ec369a9fd4bcdad885cc3b166da70ed6a83066924727d9ff147fce77","value":"0x0000000000000000000000000000000000000000000000000000000000000078"},{"key":"0x004c3a8541d9ec369a9fd4bcdad885cc3b166da70ed6a83066924727d9ff147fce78","value":"0x0000000000000000000000000000000000000000000000000000000000000079"},{"key":"0x004c3a8541d9ec369a9fd4bcdad885cc3b166da70ed6a83066924727d9ff147fce79","value":"0x000000000000000000000000000000000000000000000000000000000000007a"},{"key":"0x004c3a8541d9ec369a9fd4bcdad885cc3b166da70ed6a83066924727d9ff147fce7a","value":"0x000000000000000000000000000000000000000000000000000000000000007b"},{"key":"0x004c3a8541d9ec369a9fd4bcdad885cc3b166da70ed6a83066924727d9ff147fce7b","value":"0x000000000000000000000000000000000000000000000000000000000000007c"},{"key":"0x004c3a8541d9ec369a9fd4bcdad885cc3b166da70ed6a83066924727d9ff147fce7c","value":"0x000000000000000000000000000000000000000000000000000000000000007d"},{"key":"0x004c3a8541d9ec369a9fd4bcdad885cc3b166da70ed6a83066924727d9ff147fce7d","value":"0x000000000000000000000000000000000000000000000000000000000000007e"},{"key":"0x004c3a8541d9ec369a9fd4bcdad885cc3b166da70ed6a83066924727d9ff147fce7e","value":"0x000000000000000000000000000000000000000000000000000000000000007f"},{"key":"0x004c3a8541d9ec369a9fd4bcdad885cc3b166da70ed6a83066924727d9ff147fce7f","value":"0x0000000000000000000000000000000000000000000000000000000000000080"},{"key":"0x004c3a8541d9ec369a9fd4bcdad885cc3b166da70ed6a83066924727d9ff147fce80","value":"0x0000000000000000000000000000000000000000000000000000000000000081"},{"key":"0x004c3a8541d9ec369a9fd4bcdad885cc3b166da70ed6a83066924727d9ff147fce81","value":"0x0000000000000000000000000000000000000000000000000000000000000082"},{"key":"0x004c3a8541d9ec369a9fd4bcdad885cc3b166da70ed6a83066924727d9ff147fce82","value":"0x0000000000000000000000000000000000000000000000000000000000000083"},{"key":"0x004c3a8541d9ec369a9fd4bcdad885cc3b166da70ed6a83066924727d9ff147fce83","value":"0x0000000000000000000000000000000000000000000000000000000000000084"},{"key":"0x004c3a8541d9ec369a9fd4bcdad885cc3b166da70ed6a83066924727d9ff147fce84","value":"0x0000000000000000000000000000000000000000000000000000000000000085"},{"key":"0x004c3a8541d9ec369a9fd4bcdad885cc3b166da70ed6a83066924727d9ff147fce85","value":"0x0000000000000000000000000000000000000000000000000000000000000086"},{"key":"0x004c3a8541d9ec369a9fd4bcdad885cc3b166da70ed6a83066924727d9ff147fce86","value":"0x0000000000000000000000000000000000000000000000000000000000000087"},{"key":"0x004c3a8541d9ec369a9fd4bcdad885cc3b166da70ed6a83066924727d9ff147fce87","value":"0x0000000000000000000000000000000000000000000000000000000000000088"},{"key":"0x004c3a8541d9ec369a9fd4bcdad885cc3b166da70ed6a83066924727d9ff147fce88","value":"0x0000000000000000000000000000000000000000000000000000000000000089"},{"key":"0x004c3a8541d9ec369a9fd4bcdad885cc3b166da70ed6a83066924727d9ff147fce89","value":"0x000000000000000000000000000000000000000000000000000000000000008a"},{"key":"0x004c3a8541d9ec369a9fd4bcdad885cc3b166da70ed6a83066924727d9ff147fce8a","value":"0x000000000000000000000000000000000000000000000000000000000000008b"},{"key":"0x004c3a8541d9ec369a9fd4bcdad885cc3b166da70ed6a83066924727d9ff147fce8b","value":"0x000000000000000000000000000000000000000000000000000000000000008c"},{"key":"0x004c3a8541d9ec369a9fd4bcdad885cc3b166da70ed6a83066924727d9ff147fce8c","value":"0x000000000000000000000000000000000000000000000000000000000000008d"},{"key":"0x004c3a8541d9ec369a9fd4bcdad885cc3b166da70ed6a83066924727d9ff147fce8d","value":"0x000000000000000000000000000000000000000000000000000000000000008e"},{"key":"0x004c3a8541d9ec369a9fd4bcdad885cc3b166da70ed6a83066924727d9ff147fce8e","value":"0x000000000000000000000000000000000000000000000000000000000000008f"},{"key":"0x004c3a8541d9ec369a9fd4bcdad885cc3b166da70ed6a83066924727d9ff147fce8f","value":"0x0000000000000000000000000000000000000000000000000000000000000090"},{"key":"0x004c3a8541d9ec369a9fd4bcdad885cc3b166da70ed6a83066924727d9ff147fce90","value":"0x0000000000000000000000000000000000000000000000000000000000000091"},{"key":"0x004c3a8541d9ec369a9fd4bcdad885cc3b166da70ed6a83066924727d9ff147fce91","value":"0x0000000000000000000000000000000000000000000000000000000000000092"},{"key":"0x004c3a8541d9ec369a9fd4bcdad885cc3b166da70ed6a83066924727d9ff147fce92","value":"0x0000000000000000000000000000000000000000000000000000000000000093"},{"key":"0x004c3a8541d9ec369a9fd4bcdad885cc3b166da70ed6a83066924727d9ff147fce93","value":"0x0000000000000000000000000000000000000000000000000000000000000094"},{"key":"0x004c3a8541d9ec369a9fd4bcdad885cc3b166da70ed6a83066924727d9ff147fce94","value":"0x0000000000000000000000000000000000000000000000000000000000000095"},{"key":"0x004c3a8541d9ec369a9fd4bcdad885cc3b166da70ed6a83066924727d9ff147fce95","value":"0x0000000000000000000000000000000000000000000000000000000000000096"},{"key":"0x004c3a8541d9ec369a9fd4bcdad885cc3b166da70ed6a83066924727d9ff147fce96","value":"0x0000000000000000000000000000000000000000000000000000000000000097"},{"key":"0x004c3a8541d9ec369a9fd4bcdad885cc3b166da70ed6a83066924727d9ff147fce97","value":"0x0000000000000000000000000000000000000000000000000000000000000098"},{"key":"0x004c3a8541d9ec369a9fd4bcdad885cc3b166da70ed6a83066924727d9ff147fce98","value":"0x0000000000000000000000000000000000000000000000000000000000000099"},{"key":"0x004c3a8541d9ec369a9fd4bcdad885cc3b166da70ed6a83066924727d9ff147fce99","value":"0x000000000000000000000000000000000000000000000000000000000000009a"},{"key":"0x004c3a8541d9ec369a9fd4bcdad885cc3b166da70ed6a83066924727d9ff147fce9a","value":"0x000000000000000000000000000000000000000000000000000000000000009b"},{"key":"0x004c3a8541d9ec369a9fd4bcdad885cc3b166da70ed6a83066924727d9ff147fce9b","value":"0x000000000000000000000000000000000000000000000000000000000000009c"},{"key":"0x004c3a8541d9ec369a9fd4bcdad885cc3b166da70ed6a83066924727d9ff147fce9c","value":"0x000000000000000000000000000000000000000000000000000000000000009d"},{"key":"0x004c3a8541d9ec369a9fd4bcdad885cc3b166da70ed6a83066924727d9ff147fce9d","value":"0x000000000000000000000000000000000000000000000000000000000000009e"},{"key":"0x004c3a8541d9ec369a9fd4bcdad885cc3b166da70ed6a83066924727d9ff147fce9e","value":"0x000000000000000000000000000000000000000000000000000000000000009f"},{"key":"0x004c3a8541d9ec369a9fd4bcdad885cc3b166da70ed6a83066924727d9ff147fce9f","value":"0x00000000000000000000000000000000000000000000000000000000000000a0"},{"key":"0x004c3a8541d9ec369a9fd4bcdad885cc3b166da70ed6a83066924727d9ff147fcea0","value":"0x00000000000000000000000000000000000000000000000000000000000000a1"},{"key":"0x004c3a8541d9ec369a9fd4bcdad885cc3b166da70ed6a83066924727d9ff147fcea1","value":"0x00000000000000000000000000000000000000000000000000000000000000a2"},{"key":"0x004c3a8541d9ec369a9fd4bcdad885cc3b166da70ed6a83066924727d9ff147fcea2","value":"0x00000000000000000000000000000000000000000000000000000000000000a3"},{"key":"0x004c3a8541d9ec369a9fd4bcdad885cc3b166da70ed6a83066924727d9ff147fcea3","value":"0x00000000000000000000000000000000000000000000000000000000000000a4"},{"key":"0x004c3a8541d9ec369a9fd4bcdad885cc3b166da70ed6a83066924727d9ff147fcea4","value":"0x00000000000000000000000000000000000000000000000000000000000000a5"},{"key":"0x004c3a8541d9ec369a9fd4bcdad885cc3b166da70ed6a83066924727d9ff147fcea5","value":"0x00000000000000000000000000000000000000000000000000000000000000a6"},{"key":"0x004c3a8541d9ec369a9fd4bcdad885cc3b166da70ed6a83066924727d9ff147fcea6","value":"0x00000000000000000000000000000000000000000000000000000000000000a7"},{"key":"0x004c3a8541d9ec369a9fd4bcdad885cc3b166da70ed6a83066924727d9ff147fcea7","value":"0x00000000000000000000000000000000000000000000000000000000000000a8"},{"key":"0x004c3a8541d9ec369a9fd4bcdad885cc3b166da70ed6a83066924727d9ff147fcea8","value":"0x00000000000000000000000000000000000000000000000000000000000000a9"},{"key":"0x004c3a8541d9ec369a9fd4bcdad885cc3b166da70ed6a83066924727d9ff147fcea9","value":"0x00000000000000000000000000000000000000000000000000000000000000aa"},{"key":"0x004c3a8541d9ec369a9fd4bcdad885cc3b166da70ed6a83066924727d9ff147fceaa","value":"0x00000000000000000000000000000000000000000000000000000000000000ab"},{"key":"0x004c3a8541d9ec369a9fd4bcdad885cc3b166da70ed6a83066924727d9ff147fceab","value":"0x00000000000000000000000000000000000000000000000000000000000000ac"},{"key":"0x004c3a8541d9ec369a9fd4bcdad885cc3b166da70ed6a83066924727d9ff147fceac","value":"0x00000000000000000000000000000000000000000000000000000000000000ad"},{"key":"0x004c3a8541d9ec369a9fd4bcdad885cc3b166da70ed6a83066924727d9ff147fcead","value":"0x00000000000000000000000000000000000000000000000000000000000000ae"},{"key":"0x004c3a8541d9ec369a9fd4bcdad885cc3b166da70ed6a83066924727d9ff147fceae","value":"0x00000000000000000000000000000000000000000000000000000000000000af"},{"key":"0x004c3a8541d9ec369a9fd4bcdad885cc3b166da70ed6a83066924727d9ff147fceaf","value":"0x00000000000000000000000000000000000000000000000000000000000000b0"},{"key":"0x004c3a8541d9ec369a9fd4bcdad885cc3b166da70ed6a83066924727d9ff147fceb0","value":"0x00000000000000000000000000000000000000000000000000000000000000b1"},{"key":"0x004c3a8541d9ec369a9fd4bcdad885cc3b166da70ed6a83066924727d9ff147fceb1","value":"0x00000000000000000000000000000000000000000000000000000000000000b2"},{"key":"0x004c3a8541d9ec369a9fd4bcdad885cc3b166da70ed6a83066924727d9ff147fceb2","value":"0x00000000000000000000000000000000000000000000000000000000000000b3"},{"key":"0x004c3a8541d9ec369a9fd4bcdad885cc3b166da70ed6a83066924727d9ff147fceb3","value":"0x00000000000000000000000000000000000000000000000000000000000000b4"},{"key":"0x004c3a8541d9ec369a9fd4bcdad885cc3b166da70ed6a83066924727d9ff147fceb4","value":"0x00000000000000000000000000000000000000000000000000000000000000b5"},{"key":"0x004c3a8541d9ec369a9fd4bcdad885cc3b166da70ed6a83066924727d9ff147fceb5","value":"0x00000000000000000000000000000000000000000000000000000000000000b6"},{"key":"0x004c3a8541d9ec369a9fd4bcdad885cc3b166da70ed6a83066924727d9ff147fceb6","value":"0x00000000000000000000000000000000000000000000000000000000000000b7"},{"key":"0x004c3a8541d9ec369a9fd4bcdad885cc3b166da70ed6a83066924727d9ff147fceb7","value":"0x00000000000000000000000000000000000000000000000000000000000000b8"},{"key":"0x004c3a8541d9ec369a9fd4bcdad885cc3b166da70ed6a83066924727d9ff147fceb8","value":"0x00000000000000000000000000000000000000000000000000000000000000b9"},{"key":"0x004c3a8541d9ec369a9fd4bcdad885cc3b166da70ed6a83066924727d9ff147fceb9","value":"0x00000000000000000000000000000000000000000000000000000000000000ba"},{"key":"0x004c3a8541d9ec369a9fd4bcdad885cc3b166da70ed6a83066924727d9ff147fceba","value":"0x00000000000000000000000000000000000000000000000000000000000000bb"},{"key":"0x004c3a8541d9ec369a9fd4bcdad885cc3b166da70ed6a83066924727d9ff147fcebb","value":"0x00000000000000000000000000000000000000000000000000000000000000bc"},{"key":"0x004c3a8541d9ec369a9fd4bcdad885cc3b166da70ed6a83066924727d9ff147fcebc","value":"0x00000000000000000000000000000000000000000000000000000000000000bd"},{"key":"0x004c3a8541d9ec369a9fd4bcdad885cc3b166da70ed6a83066924727d9ff147fcebd","value":"0x00000000000000000000000000000000000000000000000000000000000000be"},{"key":"0x004c3a8541d9ec369a9fd4bcdad885cc3b166da70ed6a83066924727d9ff147fcebe","value":"0x00000000000000000000000000000000000000000000000000000000000000bf"},{"key":"0x004c3a8541d9ec369a9fd4bcdad885cc3b166da70ed6a83066924727d9ff147fcebf","value":"0x00000000000000000000000000000000000000000000000000000000000000c0"},{"key":"0x004c3a8541d9ec369a9fd4bcdad885cc3b166da70ed6a83066924727d9ff147fcec0","value":"0x00000000000000000000000000000000000000000000000000000000000000c1"},{"key":"0x004c3a8541d9ec369a9fd4bcdad885cc3b166da70ed6a83066924727d9ff147fcec1","value":"0x00000000000000000000000000000000000000000000000000000000000000c2"},{"key":"0x004c3a8541d9ec369a9fd4bcdad885cc3b166da70ed6a83066924727d9ff147fcec2","value":"0x00000000000000000000000000000000000000000000000000000000000000c3"},{"key":"0x004c3a8541d9ec369a9fd4bcdad885cc3b166da70ed6a83066924727d9ff147fcec3","value":"0x00000000000000000000000000000000000000000000000000000000000000c4"},{"key":"0x004c3a8541d9ec369a9fd4bcdad885cc3b166da70ed6a83066924727d9ff147fcec4","value":"0x00000000000000000000000000000000000000000000000000000000000000c5"},{"key":"0x004c3a8541d9ec369a9fd4bcdad885cc3b166da70ed6a83066924727d9ff147fcec5","value":"0x00000000000000000000000000000000000000000000000000000000000000c6"},{"key":"0x004c3a8541d9ec369a9fd4bcdad885cc3b166da70ed6a83066924727d9ff147fcec6","value":"0x00000000000000000000000000000000000000000000000000000000000000c7"},{"key":"0x004c3a8541d9ec369a9fd4bcdad885cc3b166da70ed6a83066924727d9ff147fcec7","value":"0x00000000000000000000000000000000000000000000000000000000000000c8"},{"key":"0x004c3a8541d9ec369a9fd4bcdad885cc3b166da70ed6a83066924727d9ff147fcec8","value":"0x00000000000000000000000000000000000000000000000000000000000000c9"},{"key":"0x004c3a8541d9ec369a9fd4bcdad885cc3b166da70ed6a83066924727d9ff147fcec9","value":"0x00000000000000000000000000000000000000000000000000000000000000ca"},{"key":"0x004c3a8541d9ec369a9fd4bcdad885cc3b166da70ed6a83066924727d9ff147fceca","value":"0x00000000000000000000000000000000000000000000000000000000000000cb"},{"key":"0x004c3a8541d9ec369a9fd4bcdad885cc3b166da70ed6a83066924727d9ff147fcecb","value":"0x00000000000000000000000000000000000000000000000000000000000000cc"},{"key":"0x004c3a8541d9ec369a9fd4bcdad885cc3b166da70ed6a83066924727d9ff147fcecc","value":"0x00000000000000000000000000000000000000000000000000000000000000cd"},{"key":"0x004c3a8541d9ec369a9fd4bcdad885cc3b166da70ed6a83066924727d9ff147fcecd","value":"0x00000000000000000000000000000000000000000000000000000000000000ce"},{"key":"0x004c3a8541d9ec369a9fd4bcdad885cc3b166da70ed6a83066924727d9ff147fcece","value":"0x00000000000000000000000000000000000000000000000000000000000000cf"},{"key":"0x004c3a8541d9ec369a9fd4bcdad885cc3b166da70ed6a83066924727d9ff147fcecf","value":"0x00000000000000000000000000000000000000000000000000000000000000d0"},{"key":"0x004c3a8541d9ec369a9fd4bcdad885cc3b166da70ed6a83066924727d9ff147fced0","value":"0x00000000000000000000000000000000000000000000000000000000000000d1"},{"key":"0x004c3a8541d9ec369a9fd4bcdad885cc3b166da70ed6a83066924727d9ff147fced1","value":"0x00000000000000000000000000000000000000000000000000000000000000d2"},{"key":"0x004c3a8541d9ec369a9fd4bcdad885cc3b166da70ed6a83066924727d9ff147fced2","value":"0x00000000000000000000000000000000000000000000000000000000000000d3"},{"key":"0x004c3a8541d9ec369a9fd4bcdad885cc3b166da70ed6a83066924727d9ff147fced3","value":"0x00000000000000000000000000000000000000000000000000000000000000d4"},{"key":"0x004c3a8541d9ec369a9fd4bcdad885cc3b166da70ed6a83066924727d9ff147fced4","value":"0x00000000000000000000000000000000000000000000000000000000000000d5"},{"key":"0x004c3a8541d9ec369a9fd4bcdad885cc3b166da70ed6a83066924727d9ff147fced5","value":"0x00000000000000000000000000000000000000000000000000000000000000d6"},{"key":"0x004c3a8541d9ec369a9fd4bcdad885cc3b166da70ed6a83066924727d9ff147fced6","value":"0x00000000000000000000000000000000000000000000000000000000000000d7"},{"key":"0x004c3a8541d9ec369a9fd4bcdad885cc3b166da70ed6a83066924727d9ff147fced7","value":"0x00000000000000000000000000000000000000000000000000000000000000d8"},{"key":"0x004c3a8541d9ec369a9fd4bcdad885cc3b166da70ed6a83066924727d9ff147fced8","value":"0x00000000000000000000000000000000000000000000000000000000000000d9"},{"key":"0x004c3a8541d9ec369a9fd4bcdad885cc3b166da70ed6a83066924727d9ff147fced9","value":"0x00000000000000000000000000000000000000000000000000000000000000da"},{"key":"0x004c3a8541d9ec369a9fd4bcdad885cc3b166da70ed6a83066924727d9ff147fceda","value":"0x00000000000000000000000000000000000000000000000000000000000000db"},{"key":"0x004c3a8541d9ec369a9fd4bcdad885cc3b166da70ed6a83066924727d9ff147fcedb","value":"0x00000000000000000000000000000000000000000000000000000000000000dc"},{"key":"0x004c3a8541d9ec369a9fd4bcdad885cc3b166da70ed6a83066924727d9ff147fcedc","value":"0x00000000000000000000000000000000000000000000000000000000000000dd"},{"key":"0x004c3a8541d9ec369a9fd4bcdad885cc3b166da70ed6a83066924727d9ff147fcedd","value":"0x00000000000000000000000000000000000000000000000000000000000000de"},{"key":"0x004c3a8541d9ec369a9fd4bcdad885cc3b166da70ed6a83066924727d9ff147fcede","value":"0x00000000000000000000000000000000000000000000000000000000000000df"},{"key":"0x004c3a8541d9ec369a9fd4bcdad885cc3b166da70ed6a83066924727d9ff147fcedf","value":"0x00000000000000000000000000000000000000000000000000000000000000e0"},{"key":"0x004c3a8541d9ec369a9fd4bcdad885cc3b166da70ed6a83066924727d9ff147fcee0","value":"0x00000000000000000000000000000000000000000000000000000000000000e1"},{"key":"0x004c3a8541d9ec369a9fd4bcdad885cc3b166da70ed6a83066924727d9ff147fcee1","value":"0x00000000000000000000000000000000000000000000000000000000000000e2"},{"key":"0x004c3a8541d9ec369a9fd4bcdad885cc3b166da70ed6a83066924727d9ff147fcee2","value":"0x00000000000000000000000000000000000000000000000000000000000000e3"},{"key":"0x004c3a8541d9ec369a9fd4bcdad885cc3b166da70ed6a83066924727d9ff147fcee3","value":"0x00000000000000000000000000000000000000000000000000000000000000e4"},{"key":"0x004c3a8541d9ec369a9fd4bcdad885cc3b166da70ed6a83066924727d9ff147fcee4","value":"0x00000000000000000000000000000000000000000000000000000000000000e5"},{"key":"0x004c3a8541d9ec369a9fd4bcdad885cc3b166da70ed6a83066924727d9ff147fcee5","value":"0x00000000000000000000000000000000000000000000000000000000000000e6"},{"key":"0x004c3a8541d9ec369a9fd4bcdad885cc3b166da70ed6a83066924727d9ff147fcee6","value":"0x00000000000000000000000000000000000000000000000000000000000000e7"},{"key":"0x004c3a8541d9ec369a9fd4bcdad885cc3b166da70ed6a83066924727d9ff147fcee7","value":"0x00000000000000000000000000000000000000000000000000000000000000e8"},{"key":"0x004c3a8541d9ec369a9fd4bcdad885cc3b166da70ed6a83066924727d9ff147fcee8","value":"0x00000000000000000000000000000000000000000000000000000000000000e9"},{"key":"0x004c3a8541d9ec369a9fd4bcdad885cc3b166da70ed6a83066924727d9ff147fcee9","value":"0x00000000000000000000000000000000000000000000000000000000000000ea"},{"key":"0x004c3a8541d9ec369a9fd4bcdad885cc3b166da70ed6a83066924727d9ff147fceea","value":"0x00000000000000000000000000000000000000000000000000000000000000eb"},{"key":"0x004c3a8541d9ec369a9fd4bcdad885cc3b166da70ed6a83066924727d9ff147fceeb","value":"0x00000000000000000000000000000000000000000000000000000000000000ec"},{"key":"0x004c3a8541d9ec369a9fd4bcdad885cc3b166da70ed6a83066924727d9ff147fceec","value":"0x00000000000000000000000000000000000000000000000000000000000000ed"},{"key":"0x004c3a8541d9ec369a9fd4bcdad885cc3b166da70ed6a83066924727d9ff147fceed","value":"0x00000000000000000000000000000000000000000000000000000000000000ee"},{"key":"0x004c3a8541d9ec369a9fd4bcdad885cc3b166da70ed6a83066924727d9ff147fceee","value":"0x00000000000000000000000000000000000000000000000000000000000000ef"},{"key":"0x004c3a8541d9ec369a9fd4bcdad885cc3b166da70ed6a83066924727d9ff147fceef","value":"0x00000000000000000000000000000000000000000000000000000000000000f0"},{"key":"0x004c3a8541d9ec369a9fd4bcdad885cc3b166da70ed6a83066924727d9ff147fcef0","value":"0x00000000000000000000000000000000000000000000000000000000000000f1"},{"key":"0x004c3a8541d9ec369a9fd4bcdad885cc3b166da70ed6a83066924727d9ff147fcef1","value":"0x00000000000000000000000000000000000000000000000000000000000000f2"},{"key":"0x004c3a8541d9ec369a9fd4bcdad885cc3b166da70ed6a83066924727d9ff147fcef2","value":"0x00000000000000000000000000000000000000000000000000000000000000f3"},{"key":"0x004c3a8541d9ec369a9fd4bcdad885cc3b166da70ed6a83066924727d9ff147fcef3","value":"0x00000000000000000000000000000000000000000000000000000000000000f4"},{"key":"0x004c3a8541d9ec369a9fd4bcdad885cc3b166da70ed6a83066924727d9ff147fcef4","value":"0x00000000000000000000000000000000000000000000000000000000000000f5"},{"key":"0x004c3a8541d9ec369a9fd4bcdad885cc3b166da70ed6a83066924727d9ff147fcef5","value":"0x00000000000000000000000000000000000000000000000000000000000000f6"},{"key":"0x004c3a8541d9ec369a9fd4bcdad885cc3b166da70ed6a83066924727d9ff147fcef6","value":"0x00000000000000000000000000000000000000000000000000000000000000f7"},{"key":"0x004c3a8541d9ec369a9fd4bcdad885cc3b166da70ed6a83066924727d9ff147fcef7","value":"0x00000000000000000000000000000000000000000000000000000000000000f8"},{"key":"0x004c3a8541d9ec369a9fd4bcdad885cc3b166da70ed6a83066924727d9ff147fcef8","value":"0x00000000000000000000000000000000000000000000000000000000000000f9"},{"key":"0x004c3a8541d9ec369a9fd4bcdad885cc3b166da70ed6a83066924727d9ff147fcef9","value":"0x00000000000000000000000000000000000000000000000000000000000000fa"},{"key":"0x004c3a8541d9ec369a9fd4bcdad885cc3b166da70ed6a83066924727d9ff147fcefa","value":"0x00000000000000000000000000000000000000000000000000000000000000fb"},{"key":"0x004c3a8541d9ec369a9fd4bcdad885cc3b166da70ed6a83066924727d9ff147fcefb","value":"0x00000000000000000000000000000000000000000000000000000000000000fc"},{"key":"0x004c3a8541d9ec369a9fd4bcdad885cc3b166da70ed6a83066924727d9ff147fcefc","value":"0x00000000000000000000000000000000000000000000000000000000000000fd"},{"key":"0x004c3a8541d9ec369a9fd4bcdad885cc3b166da70ed6a83066924727d9ff147fcefd","value":"0x00000000000000000000000000000000000000000000000000000000000000fe"},{"key":"0x004c3a8541d9ec369a9fd4bcdad885cc3b166da70ed6a83066924727d9ff147fcefe","value":"0x00000000000000000000000000000000000000000000000000000000000000ff"},{"key":"0x004c3a8541d9ec369a9fd4bcdad885cc3b166da70ed6a83066924727d9ff147fceff","value":"0x0000000000000000000000000000000000000000000000000000000000000100"}],"root":"0x090ef773023b99997f3c8c72e3fa1fbd9b0b62833ffb662e457b60530b358721"}],"sequence_vectors":[{"seed":8297,"ops":[{"op":"set","key":"0x0015c4813bb5b1499e0a7e0d46daab8e10c70940c008c566d5c68557b1523d1f2999","value":"0x00000000000000000000000000000000000000000000000000000000362952bd"},{"op":"set","key":"0xff2eb90959edcd0b6a8a7922b8b8d37cd9664001c217b4c8e6d2df4b8ede329a7191c71427661efcc7f991940e68aae22bede52be877c5e2b9b4e0c22051e2c61706","value":"0x000000000000000000000000000000000000000000000000000000005912e971"},{"op":"delete","key":"0xff2eb90959edcd0b6a8a7922b8b8d37cd9664001c217b4c8e6d2df4b8ede329a7191c71427661efcc7f991940e68aae22bede52be877c5e2b9b4e0c22051e2c61706"},{"op":"set","key":"0xff9c847c439d6431d36ddeb326cb553da2e967778a24afbd1d23a62412186faf016d21858cec8b9c323663e513206217295d0693bb4eb042fd7e5e863612ec45fe5c","value":"0x000000000000000000000000000000000000000000000000000000009e92aea6"},{"op":"set","key":"0x00998f08d7054351e5fa3ccadda32ab8d6c0c86a825dd360d57c0fe6b46612297c21","value":"0x0000000000000000000000000000000000000000000000000000000037f3974d"},{"op":"set","key":"0x00e61994d31ceae39cae5feb02303eb90e0d4ae90f5b0755c3c26adc81f360ec8516","value":"0x0000000000000000000000000000000000000000000000000000000091546180"},{"op":"set","key":"0xffd1afd6427b13f2ce7beb82a2cd6bbfc58eac8b2d46bc87bfbc78fbf440e18a635bd91a1bb3fea09cd07c1f6f0b5b0928ec465c3d5409c3dc4d9f3b384fd276b0df","value":"0x00000000000000000000000000000000000000000000000000000000d560d2d0"},{"op":"set","key":"0x002eb90959edcd0b6a8a7922b8b8d37cd9664001c217b4c8e6d2df4b8ede329a717c","value":"0x00000000000000000000000000000000000000000000000000000000b7e649ff"},{"op":"delete","key":"0xff9c847c439d6431d36ddeb326cb553da2e967778a24afbd1d23a62412186faf016d21858cec8b9c323663e513206217295d0693bb4eb042fd7e5e863612ec45fe5c"},{"op":"set","key":"0xffe60d91d1d11837912311124089c6b73c9d726c99f4df797eb48c2b7fec6e5e2cedb6b011186812600d2c38950eb3aa1e9e39d11456e96ac38d3ec9cb37e4816783","value":"0x0000000000000000000000000000000000000000000000000000000015716296"},{"op":"set","key":"0x0078a2812688114caf4452a5611322977e4b1000e59ef408d4c8e2c90028370bcbf3","value":"0x00000000000000000000000000000000000000000000000000000000d566656c"},{"op":"set","key":"0x000861030791246737d706c0d0017de01a26278bbe4ce8a134f5e0cd6f7eb69bcf6b","value":"0x00000000000000000000000000000000000000000000000000000000c6f30fd3"},{"op":"set","key":"0x0068e867f5f899fc01d641a00cea2c42d58f3cc061797135198a85301d1e2903062a","value":"0x00000000000000000000000000000000000000000000000000000000308a8072"},{"op":"set","key":"0xff46aeb3243f905d4e0eac7f60ee8962848c87e79dfbd019ece14fe45bd720ea6bc7d37b238b059b3f39fb34f03f189fc8c596ad3bc45f7231d1a8e723510074014d","value":"0x00000000000000000000000000000000000000000000000000000000fd6c27f9"},{"op":"set","key":"0x00a43fb2d7beb12b34786a837467f62b7d6c5dab4ab7eecc89c0b67db76a682927c1","value":"0x000000000000000000000000000000000000000000000000000000000b5daa14"},{"op":"set","key":"0x00543907253acb230f5dc96bbafd8ea0f37dd63b30e8a4a3675bb007673a31fbc5d7","value":"0x00000000000000000000000000000000000000000000000000000000ef86c437"},{"op":"set","key":"0xff0fb23a930b05e3bdfbdd0e6694fa9df9c5059ceabc60618bc40931ddb379f1f64c1111b166c73d061477381d77db7818b678c5a5595df51af307fccfda674238c3","value":"0x000000000000000000000000000000000000000000000000000000008687ece2"},{"op":"set","key":"0x0046aeb3243f905d4e0eac7f60ee8962848c87e79dfbd019ece14fe45bd720ea6b69","value":"0x000000000000000000000000000000000000000000000000000000008d81d15d"},{"op":"set","key":"0xff998f08d7054351e5fa3ccadda32ab8d6c0c86a825dd360d57c0fe6b46612297c216fd3a73b07a45409a19b5e6d545779e55b6900fddc4443dcb06e3d97e25e1908","value":"0x000000000000000000000000000000000000000000000000000000008f91e546"},{"op":"set","key":"0x002cebacb183e2f51a3b6cb9f7fe2bd11c399bf1bdb8ce95ed457d3e46190197a0fd","value":"0x00000000000000000000000000000000000000000000000000000000b638fa76"}],"roots_after":["0xcf6ca0dad8d69c45120da214b9052e614649a640e52560d0f47487073325e93d","0xa7247b2d9c8c6e49a18add897d235a5e72c292a5144008676972983ebd5e624e","0xcf6ca0dad8d69c45120da214b9052e614649a640e52560d0f47487073325e93d","0x9b659884c441cf90291fe8e2336c65d34cb0da60709da8a6165486c1a00829e4","0x1b5adc55d232b1c9b93c8bae8c1e736a2e596efabd3cc466de195d38ecbe9746","0xa604a44382e79cbca1ae6140d150d84980510447609e2a2356dce852861b3217","0x67994f2a47248a4677f1691706ec0ad76d479894fb7015f06bda6d4af4d46a55","0x99b2ce17b9b779b3f63f68d218182a59b928c80c1ac8d8bcb716cd3c202a20b3","0xda86ba5028d29db7d6006f7cf340f6af5203506cbcc76e72ba27302933a4943f","0xaa845405281cd62dc569bf03f8a6c1bd35609cd2c1860db56a9478254f8cb25e","0x1708bbd96a838a1f0816667f74a2d2a1a01b8c5499bcb533be4a45cf684b8bef","0xf8850e79c7adbebb051eb0ea11e3ca9866c11f660cb2b7d2c3892beb20246f26","0xc6c221bffc3947d15ad0e994e150fa0e654897bde5039e88bcda287cf24dc827","0xf1036ba9ebb2780554ab2596581122a999fe301b33446a3bf13c623dd8796f96","0x61cd8daf48607b812e063019741d2ae0d77bf9193b2b5f5996b756a22a488b1f","0x73aaf9a0a5b4f179bacf1f1c2fbcb630163797d02cdc5115977d26e8ea9f48ff","0xff83d59bd08841abbfc5cf10620dd334ec2fbb9bd0e579aec30905b4e40ccab9","0x44b55ce7592e1ba9aa3ea9c465cdadee3f7e09abccbec49aec6290eab112d0a4","0xe1d9ef033ebdf35275483848038f858fa43063805c1cc1b5ea96654745cc3b94","0xcff0d627850d4d4ea368e66ff2349c0caf454129e387d23df3c8dcf71534768f"]},{"seed":11832,"ops":[{"op":"set","key":"0xff1df77729bb76b139342cfc86aff011590bf6bb810f47e69a6babac30af96f9f5ffa1e869c4566231d09357daec90e71aa93821a7347b29175ef4a1bc02a0b38120","value":"0x0000000000000000000000000000000000000000000000000000000079b57838"},{"op":"set","key":"0xff3cbfd03e3f083b0987f841c4372031faf655a63472518a3613e2e30ecb542b51ef2c19e3ed459de7a67968b7004609b54e8e34fef08d7f9ed3c6eedcc980488255","value":"0x00000000000000000000000000000000000000000000000000000000449c8b5d"},{"op":"set","key":"0x007b1eefd69ea014244937aaeaea4e8d4d296c04edd75ae47897e5612ef5a1a25289","value":"0x00000000000000000000000000000000000000000000000000000000b5b13d29"},{"op":"set","key":"0xffa43fb2d7beb12b34786a837467f62b7d6c5dab4ab7eecc89c0b67db76a6829272094bf023796a3d4bdc16643cddf5395ab048ba346a4f50f1cc64da78ad7dd5266","value":"0x000000000000000000000000000000000000000000000000000000008cc69019"},{"op":"set","key":"0xff5963897639cd11d686b199366a1249d76bc89e0f7161c9eca48d9a199435952d4a28b184a4e022d38844c7967da6fbdf879ffafc6cfabf88c929ec9c9ff90040d1","value":"0x00000000000000000000000000000000000000000000000000000000af9bbd7d"},{"op":"set","key":"0x00e61994d31ceae39cae5feb02303eb90e0d4ae90f5b0755c3c26adc81f360ec85bf","value":"0x000000000000000000000000000000000000000000000000000000005dde837c"},{"op":"delete","key":"0xffa43fb2d7beb12b34786a837467f62b7d6c5dab4ab7eecc89c0b67db76a6829272094bf023796a3d4bdc16643cddf5395ab048ba346a4f50f1cc64da78ad7dd5266"},{"op":"delete","key":"0xff1df77729bb76b139342cfc86aff011590bf6bb810f47e69a6babac30af96f9f5ffa1e869c4566231d09357daec90e71aa93821a7347b29175ef4a1bc02a0b38120"},{"op":"delete","key":"0xff3cbfd03e3f083b0987f841c4372031faf655a63472518a3613e2e30ecb542b51ef2c19e3ed459de7a67968b7004609b54e8e34fef08d7f9ed3c6eedcc980488255"},{"op":"delete","key":"0x00e61994d31ceae39cae5feb02303eb90e0d4ae90f5b0755c3c26adc81f360ec85bf"},{"op":"set","key":"0xff0fb23a930b05e3bdfbdd0e6694fa9df9c5059ceabc60618bc40931ddb379f1f6065b90a89bb317ff71b1f933493a08b55f152abe9f5ae5b58400664d141c512119","value":"0x000000000000000000000000000000000000000000000000000000000a082d85"},{"op":"set","key":"0xff998f08d7054351e5fa3ccadda32ab8d6c0c86a825dd360d57c0fe6b46612297c725184c1ce99f155151a19aa14a427e1a784778319e2b193ac66bd6374b328cdff","value":"0x00000000000000000000000000000000000000000000000000000000a3ea3eb4"},{"op":"set","key":"0xff78a2812688114caf4452a5611322977e4b1000e59ef408d4c8e2c90028370bcb71e2e06bd4204550cd67cae560e42f8afcdc75e454a14f00ed2a8ec7c885869e40","value":"0x000000000000000000000000000000000000000000000000000000007435a9e4"},{"op":"delete","key":"0xff0fb23a930b05e3bdfbdd0e6694fa9df9c5059ceabc60618bc40931ddb379f1f6065b90a89bb317ff71b1f933493a08b55f152abe9f5ae5b58400664d141c512119"},{"op":"set","key":"0x000fb23a930b05e3bdfbdd0e6694fa9df9c5059ceabc60618bc40931ddb379f1f668","value":"0x000000000000000000000000000000000000000000000000000000000275abc8"},{"op":"delete","key":"0xff5963897639cd11d686b199366a1249d76bc89e0f7161c9eca48d9a199435952d4a28b184a4e022d38844c7967da6fbdf879ffafc6cfabf88c929ec9c9ff90040d1"},{"op":"set","key":"0x007293ce391f5cd652216ca8ea31069a06a82f393028dfc8786aa5efd7be734b2cbe","value":"0x0000000000000000000000000000000000000000000000000000000094f87f55"},{"op":"set","key":"0xff4c3a8541d9ec369a9fd4bcdad885cc3b166da70ed6a83066924727d9ff147fce963472d01342d91a0a268bdfee2ecf01a6d30f9021151faf2e2e6719120bf40f0a","value":"0x00000000000000000000000000000000000000000000000000000000fdac9fff"},{"op":"set","key":"0xff1df77729bb76b139342cfc86aff011590bf6bb810f47e69a6babac30af96f9f5ffa1e869c4566231d09357daec90e71aa93821a7347b29175ef4a1bc02a0b38181","value":"0x00000000000000000000000000000000000000000000000000000000e4d876b8"},{"op":"set","key":"0x00e2f41518226671332da4adddab1fdcb9903c7b895c7943b669067fe96b0baacfd5","value":"0x0000000000000000000000000000000000000000000000000000000019be8821"}],"roots_after":["0x5cdba549646b7612059692efa58d33f90059dfa9df06433f4438510c1b32e049","0x6ae6cd7fe0f7f9dfbff03d1d6b88c55f8c9f247584c70427a2a53612cdf202e6","0xcbe8e97d34728b819654a46096d4fe13abcdeed3e31cf088be21de915051259a","0x155969c29e159077b8a9997325ac9962fee077d64c9467f1ccd68618c9abbcdb","0x2439229c4decda037eb0a692540821cdd6c77df8ba51d478291d905e5de4f5e4","0xc252ede6498166ca351dc0346fdd1ee5df6694e3acf7f5714f2e80db687ccc79","0x7fcfd5973d297879b65209bb9cf1ca74c30ee717e13350d2c0447675165cb80b","0xdbb4e48dda8ae1f9287abccd62637085bb171f5897ad42e5019500209e89202d","0x14712da3d0ce63185cae268f29bf2593295476a83a8e95b3b4daf01a83274d6f","0x51098829074dc7618bdca7c079f0ac0577e0a08a0df5c92dcd104bf6cad43ea6","0x2643869090c9ca916157631cac68996dd01de67c66639eeb443f9b93ebe7df1c","0x9793adae9d275d569398dc631e09da40464320a0dc0c644d36d79f97d34b7288","0xef2bd1092bc3404855dd94e9452f97cebeb425d8027e7a56dc1c5fc0e93c38f6","0x85170f6f2bed9f200e8835f9d2c9bd13cb9267d77e834dcd73d43d1f897cc346","0x71c46fa0b2804adc2012db0d399c477429071b47f13fc31ddc125bc599e8fafb","0xeba1b27a56ff60fd186ede1c5ee445c59b2733f527c6ccc30b765b6917491720","0x2d0dcb278bc7892ad37afe2c523b73d18a60e76b1dc41d04469896c3b355d490","0x5aadbab832910092a62a49e78c5d76b10ffe26f3452a0bd47fd049e36eb31d63","0x53b8d1dab39520c882f5666b092ab47f59f4f3084c9471eefbb15d6a0c58ad5d","0x52ff67405b74b808b1649b64764de113df17e70ea5aebf5489bc1db7aad52422"]},{"seed":3102,"ops":[{"op":"set","key":"0xff2cebacb183e2f51a3b6cb9f7fe2bd11c399bf1bdb8ce95ed457d3e46190197a02a6a55293936c1e1b6894fbb3b2873c993bd580ec835889bf0f211410e368fc199","value":"0x000000000000000000000000000000000000000000000000000000002e422f9a"},{"op":"delete","key":"0xff2cebacb183e2f51a3b6cb9f7fe2bd11c399bf1bdb8ce95ed457d3e46190197a02a6a55293936c1e1b6894fbb3b2873c993bd580ec835889bf0f211410e368fc199"},{"op":"set","key":"0xff29e4e6bbd168fe5b46b7d34c34b628f95d54c4e75f9335a7e96282d1173b07dfd0f9925799d37553e0f34b2d7a49a135fc34a90b6fb99bfe70f2e43c28e758854d","value":"0x000000000000000000000000000000000000000000000000000000002ecaa733"},{"op":"set","key":"0x00d1afd6427b13f2ce7beb82a2cd6bbfc58eac8b2d46bc87bfbc78fbf440e18a635f","value":"0x0000000000000000000000000000000000000000000000000000000076fe3750"},{"op":"set","key":"0x000861030791246737d706c0d0017de01a26278bbe4ce8a134f5e0cd6f7eb69bcff9","value":"0x0000000000000000000000000000000000000000000000000000000035fd5ae2"},{"op":"set","key":"0xff998f08d7054351e5fa3ccadda32ab8d6c0c86a825dd360d57c0fe6b46612297cc290a8b5ac7a453c9cec5724a8a85ad4002fb6dc9c73e86aedfa05133317c0402e","value":"0x00000000000000000000000000000000000000000000000000000000be9e2390"},{"op":"set","key":"0x005963897639cd11d686b199366a1249d76bc89e0f7161c9eca48d9a199435952d33","value":"0x00000000000000000000000000000000000000000000000000000000b3e90b26"},{"op":"set","key":"0xffe57860ba0adcdd1294caf74da8b990a80a8a6861bc01b30dfe07450d25bab38f20483a0da833c6de846c07a8214bee6ee90bd1da5d9b9553f74784602fdd83dfb9","value":"0x0000000000000000000000000000000000000000000000000000000051dcd3af"},{"op":"set","key":"0x00e6b6d8d98be54d4c00cee378bedf964f755b5257bf1eabf52cf39aa987686fa743","value":"0x0000000000000000000000000000000000000000000000000000000083a3dad3"},{"op":"set","key":"0xff0fb23a930b05e3bdfbdd0e6694fa9df9c5059ceabc60618bc40931ddb379f1f6065b90a89bb317ff71b1f933493a08b55f152abe9f5ae5b58400664d141c512161","value":"0x00000000000000000000000000000000000000000000000000000000939e31a5"},{"op":"set","key":"0xff2cebacb183e2f51a3b6cb9f7fe2bd11c399bf1bdb8ce95ed457d3e46190197a018eb952465e3dd252f35c719bb6d7d14f62bf363e0afa74100d3046f81539e08b9","value":"0x000000000000000000000000000000000000000000000000000000002da16542"},{"op":"set","key":"0xff4b83f061efb2f4708114e46b5ef50a255528c28c80bbe5fc6ad88e892bd4c4ea37491f84eb77d352d7d8fca0484b856ceab7236e2f773d157b3fbd6d0586de7f48","value":"0x000000000000000000000000000000000000000000000000000000003b1510f6"},{"op":"set","key":"0x00c901cfbb39e551189323bea0af6cf9e81008c90dce65cbc2ff4672e263d7bb87f3","value":"0x0000000000000000000000000000000000000000000000000000000087d0f3c4"},{"op":"set","key":"0x0055a6923f7414ba482ae8a64b407f386a0bb0491b742bd4b22b5b4b44fca84e848f","value":"0x000000000000000000000000000000000000000000000000000000008cfbc63e"},{"op":"set","key":"0xff7b1eefd69ea014244937aaeaea4e8d4d296c04edd75ae47897e5612ef5a1a25254d51e5b726c0f3cfd1b03f07f25907b18767b99fef9eb727475858c458e1c7087","value":"0x00000000000000000000000000000000000000000000000000000000af70ae1b"},{"op":"set","key":"0xff29e4e6bbd168fe5b46b7d34c34b628f95d54c4e75f9335a7e96282d1173b07df754b0f807e1dc46306d4cb12e85e18d008678fa80cd9e6fa118c34e179bd412a87","value":"0x00000000000000000000000000000000000000000000000000000000d15c3b16"},{"op":"set","key":"0xff29e4e6bbd168fe5b46b7d34c34b628f95d54c4e75f9335a7e96282d1173b07df754b0f807e1dc46306d4cb12e85e18d008678fa80cd9e6fa118c34e179bd412a57","value":"0x000000000000000000000000000000000000000000000000000000003e5f6e17"},{"op":"set","key":"0x0055bbfb5b05f80543bd662a7b7a74980a2992612798087fa24e1fc86b8e8508e4b6","value":"0x000000000000000000000000000000000000000000000000000000002a25f39d"},{"op":"delete","key":"0xff0fb23a930b05e3bdfbdd0e6694fa9df9c5059ceabc60618bc40931ddb379f1f6065b90a89bb317ff71b1f933493a08b55f152abe9f5ae5b58400664d141c512161"},{"op":"set","key":"0x00e61994d31ceae39cae5feb02303eb90e0d4ae90f5b0755c3c26adc81f360ec85de","value":"0x00000000000000000000000000000000000000000000000000000000fd3f724c"}],"roots_after":["0xdd16f2b8e1c012d8ed988df1cd9e31fc06bceee33304ec90fdee4c9e0d6f2522","0x0000000000000000000000000000000000000000000000000000000000000000","0xe7c887160ebfbf178105a04157a5b67d669ac6899fbf632526152fc5cb82cd80","0xd77cc71cfc52b7a272f47bb93cf6e3b5ae4a2d7fc991e4e2b76586018620570b","0xfb180e479837451381e4ebbf7a21be6937dbab9b830eeb798cf1854b48bc82ad","0x663475566fe0b6afa6fb71c73802d98b37948ccc16237229d8722a66b31c48f0","0xe26dac98c3f044695b7bda1e7bab4e9e0422d41b367b3fb873d50734d15f8de8","0x90263ca13708230b2ae2d6413b76995a7c177ed239310a9628fe4aac1e2d51fe","0xd7893e542d99ae8c18bd7a495190a58e2e1e25252d4fa9378ef2694db0e751ee","0x4df5042f26d44caf9d655bda9bb46284498628761474d302392daff333e5b610","0x1113ff21db5505e243f390d5df0fc11549690813ff7a25e8bc60b5209d39ed72","0xe14995d380c1552d2901cacc440b51fed58732d5eb6b6adb202e891f3f53b6d6","0x5d3fd815f8282caf95aab8105212fb98b6f5516beb9fba2ed4f4ee502b85ed74","0x09b380d8286220693a4a9e4b85e074ea94cf0dadbccacd05d6f8dc5470438087","0x957d9940952d058f2523c52603f8cc27537ee0e40b8c452d9691e28ff0f9d782","0x7cc8b1cbe418de642947381e3fd9251730dff6165bb7d48de4388cce893d8aea","0x5ea579760671e854abc2adfcb037bd6f1470c26e5f4ba6e9392d05e1af830b6d","0xb4281adb36825d1f7dd6b7f0536a81ad9950fb727124828f0a94a79c32885052","0xa90bdbe39f8833c02f914c2ca37a5d5b91be83ba6a4721923b1ffe8899c40d9e","0x8c3fff51ed5749c6147c9bb35b5ac0f20f0f0ff92604b0d7cd0f83bcaecf52b4"]},{"seed":90210,"ops":[{"op":"set","key":"0xffd1afd6427b13f2ce7beb82a2cd6bbfc58eac8b2d46bc87bfbc78fbf440e18a63aeb0aca39c8f6ba440835a8e371b8b2719dec5b24a57f1da0e7519c3cd74beff52","value":"0x00000000000000000000000000000000000000000000000000000000cec06895"},{"op":"set","key":"0x00e61994d31ceae39cae5feb02303eb90e0d4ae90f5b0755c3c26adc81f360ec8587","value":"0x0000000000000000000000000000000000000000000000000000000026a125de"},{"op":"delete","key":"0x00e61994d31ceae39cae5feb02303eb90e0d4ae90f5b0755c3c26adc81f360ec8587"},{"op":"delete","key":"0xffd1afd6427b13f2ce7beb82a2cd6bbfc58eac8b2d46bc87bfbc78fbf440e18a63aeb0aca39c8f6ba440835a8e371b8b2719dec5b24a57f1da0e7519c3cd74beff52"},{"op":"set","key":"0x00c901cfbb39e551189323bea0af6cf9e81008c90dce65cbc2ff4672e263d7bb871a","value":"0x0000000000000000000000000000000000000000000000000000000038f9aacc"},{"op":"set","key":"0x005963897639cd11d686b199366a1249d76bc89e0f7161c9eca48d9a199435952d0f","value":"0x0000000000000000000000000000000000000000000000000000000053b3bca6"},{"op":"set","key":"0x0068e867f5f899fc01d641a00cea2c42d58f3cc061797135198a85301d1e290306c3","value":"0x0000000000000000000000000000000000000000000000000000000058e273d9"},{"op":"set","key":"0x00e61994d31ceae39cae5feb02303eb90e0d4ae90f5b0755c3c26adc81f360ec85b3","value":"0x000000000000000000000000000000000000000000000000000000008debe84f"},{"op":"delete","key":"0x00c901cfbb39e551189323bea0af6cf9e81008c90dce65cbc2ff4672e263d7bb871a"},{"op":"set","key":"0x0055bbfb5b05f80543bd662a7b7a74980a2992612798087fa24e1fc86b8e8508e4b2","value":"0x0000000000000000000000000000000000000000000000000000000052fbeee9"},{"op":"set","key":"0xff68e867f5f899fc01d641a00cea2c42d58f3cc061797135198a85301d1e290306efdc84373704529ffcbc3d4ed318de0ec2cdf5f61dd800c143f44a741af2f838a0","value":"0x0000000000000000000000000000000000000000000000000000000012acb6e5"},{"op":"set","key":"0xffe60d91d1d11837912311124089c6b73c9d726c99f4df797eb48c2b7fec6e5e2c3b9034b692f2597d8c9c9ad6921ecd112d2f69cc50a6e065034e2ace540e8928ab","value":"0x0000000000000000000000000000000000000000000000000000000088a67fe9"},{"op":"set","key":"0xffe60d91d1d11837912311124089c6b73c9d726c99f4df797eb48c2b7fec6e5e2c9c921b846417391ff7e9525580b53876b30e3f6cf563e5849525f6fa88bf230f1a","value":"0x0000000000000000000000000000000000000000000000000000000075b67af1"},{"op":"set","key":"0xff78a2812688114caf4452a5611322977e4b1000e59ef408d4c8e2c90028370bcb279b454b0f4a6026943322996543a3cbb6c41d7c467067d9bc010242feef3c2c4b","value":"0x00000000000000000000000000000000000000000000000000000000fd6d065d"},{"op":"set","key":"0x00bded9122e6171a5dad9ce2aaebd933abf4718e9bf12448cf113064c9fd8e6ad0a4","value":"0x000000000000000000000000000000000000000000000000000000001c92d573"},{"op":"delete","key":"0xffe60d91d1d11837912311124089c6b73c9d726c99f4df797eb48c2b7fec6e5e2c9c921b846417391ff7e9525580b53876b30e3f6cf563e5849525f6fa88bf230f1a"},{"op":"set","key":"0x003cbfd03e3f083b0987f841c4372031faf655a63472518a3613e2e30ecb542b514a","value":"0x00000000000000000000000000000000000000000000000000000000617ad32c"},{"op":"set","key":"0xffec64b2de1fc1d53b795f49a1c84ca4172bc3292472c3e244e4f8c225fa786f56b7bce92ed9ae70f9ca2c10d21fe857f18cd2b64d54c70193d8a0dd772967c2580d","value":"0x00000000000000000000000000000000000000000000000000000000565e29f9"},{"op":"delete","key":"0xff78a2812688114caf4452a5611322977e4b1000e59ef408d4c8e2c90028370bcb279b454b0f4a6026943322996543a3cbb6c41d7c467067d9bc010242feef3c2c4b"},{"op":"set","key":"0xffd1afd6427b13f2ce7beb82a2cd6bbfc58eac8b2d46bc87bfbc78fbf440e18a635bd91a1bb3fea09cd07c1f6f0b5b0928ec465c3d5409c3dc4d9f3b384fd276b0e3","value":"0x00000000000000000000000000000000000000000000000000000000f03eb650"}],"roots_after":["0xe4a4cc81bad5ec263e1470b6766f0e48c44cb6de9edfafe42ce6a742e7c0de35","0x628d4752f5a37f73d798da0fd84216c8c96d376e81ca8c011a1ef0e5b293037d","0xe4a4cc81bad5ec263e1470b6766f0e48c44cb6de9edfafe42ce6a742e7c0de35","0x0000000000000000000000000000000000000000000000000000000000000000","0x36d0ac2cbaa8daac84e3abb0711753ab2803d39fb7def6e34b95e607b5177c92","0xcdd2d3edb22a14874aa1b8adc01bad8e80dd4f6f0c603788bd28705981409ba7","0x0438e8ad308f977fe36353cde64fef7f1210dd3820b5275c3fffe712e342f125","0xe8514dc5fbf6794122aad2a058ad95df0835731e49061dfc5238fe8673b7cb57","0xfa1bed5d0c8187d176cdabee2a0b6d96ec72791326c1d9d00555e2752fa4ae06","0x388f561f8cd9b274f5c9cd3154ab49c7586c9cae7b28c0b7471be9be98cba3be","0x36fca86e1497e245753b5662e45cac10ce68278494adef2b1219b534d63411e2","0x98ebe09efe3ff35ee2f8b20732a3f30408119a25e620107c33f503cd39e55143","0xcd53e141a34d3c96464d66bc027b4cc0731739f1e645aced12c51072463f8fc4","0xbba122989fb7ec24899f02a1aa37fcf967d84f6c5207d843ac9a754b1eaa7ba3","0xb219a14dd80c8cc69152edbcec566fe773892d5e615e310b063e99def0f6aa79","0x3531deb75570f3a160ba9b471a10be04750c59ed54984186a46d4521e5058145","0xd3e835c31b1c00c8f864d7da97f035c6147452c3762a7f69b174751b815737a8","0xd74280c2db61bd2bd9d739ae4c38053ce6192f683907b95c6c0fed0ddf6b1b66","0x24a2abd1e5f37cbb1e104406be5ee8b6c4989c27bb6ec117ae4e18c0b66dfc9c","0x3a7c4099580d9b766538946c88846c95d7822fd4d6121a20e57e1791026600f5"]},{"seed":20260727,"ops":[{"op":"set","key":"0x002eb90959edcd0b6a8a7922b8b8d37cd9664001c217b4c8e6d2df4b8ede329a7170","value":"0x0000000000000000000000000000000000000000000000000000000068535e9a"},{"op":"set","key":"0xff3cbfd03e3f083b0987f841c4372031faf655a63472518a3613e2e30ecb542b5111589c57f4d8ad55d2c4ece4c6b63961c45be7479bfd08d04fab2d26d0acb7a091","value":"0x0000000000000000000000000000000000000000000000000000000056756dfe"},{"op":"set","key":"0x007b1eefd69ea014244937aaeaea4e8d4d296c04edd75ae47897e5612ef5a1a2521a","value":"0x000000000000000000000000000000000000000000000000000000005959a793"},{"op":"delete","key":"0x002eb90959edcd0b6a8a7922b8b8d37cd9664001c217b4c8e6d2df4b8ede329a7170"},{"op":"delete","key":"0x007b1eefd69ea014244937aaeaea4e8d4d296c04edd75ae47897e5612ef5a1a2521a"},{"op":"set","key":"0x0055a6923f7414ba482ae8a64b407f386a0bb0491b742bd4b22b5b4b44fca84e8468","value":"0x000000000000000000000000000000000000000000000000000000003c2b7202"},{"op":"delete","key":"0xff3cbfd03e3f083b0987f841c4372031faf655a63472518a3613e2e30ecb542b5111589c57f4d8ad55d2c4ece4c6b63961c45be7479bfd08d04fab2d26d0acb7a091"},{"op":"delete","key":"0x0055a6923f7414ba482ae8a64b407f386a0bb0491b742bd4b22b5b4b44fca84e8468"},{"op":"set","key":"0xffe60d91d1d11837912311124089c6b73c9d726c99f4df797eb48c2b7fec6e5e2c6066e8c722a574a900a59551ed956d32ca2b36e48771ed8b373da6b223bbbe9c8f","value":"0x000000000000000000000000000000000000000000000000000000009bb7df73"},{"op":"set","key":"0x009c847c439d6431d36ddeb326cb553da2e967778a24afbd1d23a62412186faf0130","value":"0x00000000000000000000000000000000000000000000000000000000def11b80"},{"op":"set","key":"0x007b1eefd69ea014244937aaeaea4e8d4d296c04edd75ae47897e5612ef5a1a2524d","value":"0x00000000000000000000000000000000000000000000000000000000f05708e7"},{"op":"set","key":"0x00998f08d7054351e5fa3ccadda32ab8d6c0c86a825dd360d57c0fe6b46612297cba","value":"0x00000000000000000000000000000000000000000000000000000000c433224b"},{"op":"set","key":"0xff5ac76a8f23131e4ca988c8afcc313130cc95e521d2691635fc26196d63e66a1f33b1e44125772918b9d44a12766eb0dad773e863d120780173c394789aa668e925","value":"0x00000000000000000000000000000000000000000000000000000000abbc594e"},{"op":"set","key":"0xff998f08d7054351e5fa3ccadda32ab8d6c0c86a825dd360d57c0fe6b46612297c36c8cce2938e709d43205827ddebecc780d7e631bb1acb24046af6e5b23e445aa2","value":"0x00000000000000000000000000000000000000000000000000000000219ea23a"},{"op":"set","key":"0x00d1d141c1bed8bb4f809292e9340a0209cd167484a24a5ca993fcac4b97086d89af","value":"0x00000000000000000000000000000000000000000000000000000000e015951e"},{"op":"delete","key":"0x00d1d141c1bed8bb4f809292e9340a0209cd167484a24a5ca993fcac4b97086d89af"},{"op":"set","key":"0xffc28c21982a56a6cecf595bfeb78297c7432abfb487cc75c09db2856a118f441df2a84c02b0cbaafcabc4cbb5f988d916f3287032f07e3f8bf51588b4311ed04c72","value":"0x00000000000000000000000000000000000000000000000000000000973ab40a"},{"op":"set","key":"0x00e2f41518226671332da4adddab1fdcb9903c7b895c7943b669067fe96b0baacfdb","value":"0x000000000000000000000000000000000000000000000000000000000c8a8e64"},{"op":"delete","key":"0x007b1eefd69ea014244937aaeaea4e8d4d296c04edd75ae47897e5612ef5a1a2524d"},{"op":"set","key":"0xff15c4813bb5b1499e0a7e0d46daab8e10c70940c008c566d5c68557b1523d1f29cac055e3c06fef0ce191be15524a246c11d1103158d4f8b49d31c629e552005366","value":"0x000000000000000000000000000000000000000000000000000000001a5e6148"}],"roots_after":["0x8a7b32f0179f8591d8d29bbc5ce478d2d1cca5ceebddb5c6880e6682fd1a7458","0x63e670849972d58347a67cb04d30b369145ea59ac06fd411c231b46c05b534c8","0xeedfc71a50202763dc37c74f994b52783a5e3ac19ff7c3d818c9bd2c95b562f7","0x1cfbe1c8b87bb9ecafd506ee003ddaeef688eaa24a564cd7147270130425709e","0xf8daed5ef73e34987561ea33d980ed96600f48700aa75ea5706c74f79babdb3d","0xcf348977fb91b672c061404e5bc6f063a444d19478b4aba7450531d48ae5b8f8","0xfa4116de24b5dce1e1ebb9736c78a00ec1eb957aaee7e9f9af3b74f68e43a73e","0x0000000000000000000000000000000000000000000000000000000000000000","0x5f2fc0f08e034ab42e6b4b8eec8038618707b8f2ae9722e14a6048d1935a438c","0xbc0812ac0dbf193721b4e90a5f23d702399df82d1e1da571c9e2388155b5409f","0xe105f7395ae6845ec524e9dcdf9a33600d7eece3b9dacaf7fee43deca2ad0957","0x5f30927f1062975cf9ad72910c99386ffc609b4dac5e1633a0a7a051ff221ca1","0x6f8469d0d9572b492c6c170c43f734aea5714da7d2e47ac9c10a01938439a1cb","0x7a75f5784c851040e436de70bafabe4abddc0baab0db46583879fdc1a7bcbd01","0xaa03aca82638a0c50233cb98152c122b0c21c4efb2a47035a690b5a65a2e54ed","0x7a75f5784c851040e436de70bafabe4abddc0baab0db46583879fdc1a7bcbd01","0xf6d984c0ed4b562ad15789d556f759f72b44627fb197780e88d36f450c60cd4e","0x52ae36bcfdb7559ffaf993e3d23b7727ada4f1403f12a9f16ba0324610852a8e","0x7d48f61dc2656ca3fc52efbb16bf80b4806cf43aaef56397c9d526d739ab2ad4","0x867a92986423254fd9f362a682695d3eb986bd5106cadabbd72dd0faf70c0cf9"]}],"embedding_vectors":{"address":"0xaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa","basic_data_key":"0x00d9ae2d236f8713a5bf808cda488167a56cc97e4b83006f42b1c06c0c3f053bbf00","code_hash_key":"0x00d9ae2d236f8713a5bf808cda488167a56cc97e4b83006f42b1c06c0c3f053bbf01","slots":[{"slot":0,"key":"0x00d9ae2d236f8713a5bf808cda488167a56cc97e4b83006f42b1c06c0c3f053bbf40"},{"slot":5,"key":"0x00d9ae2d236f8713a5bf808cda488167a56cc97e4b83006f42b1c06c0c3f053bbf45"},{"slot":63,"key":"0x00d9ae2d236f8713a5bf808cda488167a56cc97e4b83006f42b1c06c0c3f053bbf7f"},{"slot":64,"key":"0xffd9ae2d236f8713a5bf808cda488167a56cc97e4b83006f42b1c06c0c3f053bbf181495ef75f8d2005a4a5bd3c5139cf6b5cdd1a02574832253cc9932687b332a40"},{"slot":255,"key":"0xffd9ae2d236f8713a5bf808cda488167a56cc97e4b83006f42b1c06c0c3f053bbf181495ef75f8d2005a4a5bd3c5139cf6b5cdd1a02574832253cc9932687b332aff"},{"slot":256,"key":"0xffd9ae2d236f8713a5bf808cda488167a56cc97e4b83006f42b1c06c0c3f053bbfe717382bfeaa9f4c014431fb7bbc3c6946dd510a82d49c925d9df7735c26b16f00"},{"slot":1000,"key":"0xffd9ae2d236f8713a5bf808cda488167a56cc97e4b83006f42b1c06c0c3f053bbf2b1a6c18962d993b9da5ed300ce5696bdcad6e09f08b9c0735fa749d417650f9e8"},{"slot":57896044618658097711785492504343953926634992332820282019728792003956564819968,"key":"0xffd9ae2d236f8713a5bf808cda488167a56cc97e4b83006f42b1c06c0c3f053bbf23d37150867b80a99fea951c62d43f974e7d2f53089c5f5683b4fc1d4387286c00"}],"chunks":[{"chunk":0,"key":"0x00d9ae2d236f8713a5bf808cda488167a56cc97e4b83006f42b1c06c0c3f053bbf80"},{"chunk":5,"key":"0x00d9ae2d236f8713a5bf808cda488167a56cc97e4b83006f42b1c06c0c3f053bbf85"},{"chunk":127,"key":"0x00d9ae2d236f8713a5bf808cda488167a56cc97e4b83006f42b1c06c0c3f053bbfff"},{"chunk":128,"key":"0x01465f911ccfe7707c51a877fc621cffa8e700a5828cf92df27b142de919c0f6c800"},{"chunk":300,"key":"0x01465f911ccfe7707c51a877fc621cffa8e700a5828cf92df27b142de919c0f6c8ac"},{"chunk":383,"key":"0x01465f911ccfe7707c51a877fc621cffa8e700a5828cf92df27b142de919c0f6c8ff"},{"chunk":384,"key":"0x0171ad9e407a8d200ab2858bcf828d9016a0b4bc54e6e39dd3e31847b6220a63ce00"}]},"basic_data_vectors":[{"code_size":0,"nonce":0,"balance":"0","value":"0x0000000000000000000000000000000000000000000000000000000000000000"},{"code_size":0,"nonce":1,"balance":"1000000000000000000","value":"0x0000000000000000000000000000000100000000000000000de0b6b3a7640000"},{"code_size":287454020,"nonce":6153737369425722316,"balance":"1512366075204170929049582354406559215","value":"0x00000000112233445566778899aabbcc0123456789abcdef0123456789abcdef"},{"code_size":24576,"nonce":1,"balance":"1","value":"0x0000000000006000000000000000000100000000000000000000000000000001"}],"chunkify_vectors":[{"name":"empty","code":"0x","chunks":[]},{"name":"short","code":"0x6001","chunks":["0x0060010000000000000000000000000000000000000000000000000000000000"]},{"name":"push_boundary","code":"0x6060606060606060606060606060606060606060606060606060606060606060606060606060606060606060606060606060606060606060606060606060","chunks":["0x0060606060606060606060606060606060606060606060606060606060606060","0x0160606060606060606060606060606060606060606060606060606060606060"]},{"name":"push32_tail","code":"0x7feeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeee","chunks":["0x007feeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeee","0x02eeeeeeeeeeeeeeeeeeee000000000000000000000000000000000000000000"]},{"name":"zeros62","code":"0x0000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000","chunks":["0x0000000000000000000000000000000000000000000000000000000000000000","0x0000000000000000000000000000000000000000000000000000000000000000"]}]} From 0bae7581da98ea567a398adfa2d47f79f07cd05d Mon Sep 17 00:00:00 2001 From: awskii Date: Mon, 10 Aug 2026 16:21:00 +0700 Subject: [PATCH 49/56] execution/protocol/params: apply the revised EIP-8038 gas values execution-specs 7b6aed29c revised the Amsterdam state-access schedule. These constants still held the pre-revision values, so every Amsterdam fixture diverged on gas used and, through it, on the receipt root. COLD_STORAGE_ACCESS 3000 -> 2100 ACCOUNT_WRITE 8000 -> 9000 CREATE_ACCESS now pairs ACCOUNT_WRITE with COLD_ACCOUNT_ACCESS rather than COLD_STORAGE_ACCESS (11000 -> 12000) REFUND_STORAGE_CLEAR 12480 -> 11616, recomputed from the new COLD_STORAGE_ACCESS CALL_VALUE and the access-list storage cost are already expressed in terms of the two changed constants and follow on their own. EIP-8038 is gated on IsAmsterdam and no chainspec schedules Amsterdam, so no live network reaches this schedule. --- execution/protocol/params/protocol.go | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/execution/protocol/params/protocol.go b/execution/protocol/params/protocol.go index 20d6ddcafc0..35c71c2a135 100644 --- a/execution/protocol/params/protocol.go +++ b/execution/protocol/params/protocol.go @@ -237,12 +237,12 @@ const ( // costs and adds the execution-gas write components (ACCOUNT_WRITE, STORAGE_WRITE) // that the EIP-8037 state-gas model is charged alongside. ColdAccountAccessCostEIP8038 = uint64(3000) // COLD_ACCOUNT_ACCESS (EIP-2929: 2600) - ColdStorageAccessCostEIP8038 = uint64(3000) // COLD_STORAGE_ACCESS (EIP-2929 cold SLOAD: 2100) - AccountWriteCostEIP8038 = uint64(8000) // ACCOUNT_WRITE: account balance-leaf write + ColdStorageAccessCostEIP8038 = uint64(2100) // COLD_STORAGE_ACCESS (EIP-2929 cold SLOAD: 2100) + AccountWriteCostEIP8038 = uint64(9000) // ACCOUNT_WRITE: account balance-leaf write StorageWriteCostEIP8038 = uint64(10000) // STORAGE_WRITE: first write to a slot in the txn - CallValueTransferGasEIP8038 = AccountWriteCostEIP8038 + CallStipend // CALL_VALUE = 10300 - CreateAccessEIP8038 = AccountWriteCostEIP8038 + ColdStorageAccessCostEIP8038 // CREATE_ACCESS = 11000 - SstoreClearsScheduleRefundEIP8038 = uint64(12480) // REFUND_STORAGE_CLEAR = (STORAGE_WRITE+COLD_STORAGE_ACCESS)*4800/5000 + CallValueTransferGasEIP8038 = AccountWriteCostEIP8038 + CallStipend // CALL_VALUE = 11300 + CreateAccessEIP8038 = AccountWriteCostEIP8038 + ColdAccountAccessCostEIP8038 // CREATE_ACCESS = 12000 + SstoreClearsScheduleRefundEIP8038 = uint64(11616) // REFUND_STORAGE_CLEAR = (STORAGE_WRITE+COLD_STORAGE_ACCESS)*4800/5000 TxAccessListAddressGasEIP8038 = ColdAccountAccessCostEIP8038 // ACCESS_LIST_ADDRESS_COST TxAccessListStorageKeyGasEIP8038 = ColdStorageAccessCostEIP8038 // ACCESS_LIST_STORAGE_KEY_COST ExtCodeWarmAccessGasEIP8038 = 2 * WarmStorageReadCostEIP2929 // EXTCODESIZE/EXTCODECOPY: account access + second read for the code From 3bd1726e20a9dda7a04b1e9a281db35ea6c1df3b Mon Sep 17 00:00:00 2001 From: awskii Date: Mon, 10 Aug 2026 16:27:10 +0700 Subject: [PATCH 50/56] rpc/jsonrpc: debug_executionWitness for the binary trie (#23136) `debug_executionWitness` and `eth_getProof` had only a hex-trie path, so on a binary-tree chain they had nothing to serve. ## Changes - Witness capture for `PBinPatriciaHashed`: a tracer on the hasher records the nodes the fold emits, and `PBinWitnessNodesForKeys` prunes that superset down to what a verifier needs for the proved keys. - `RecordingState` wiring so the RPC re-executes the block and collects the touched keys. - Spec catch-up carried alongside: all code chunks moved into the content-addressed code zone (execution-specs#3310), and the EIP-7702 delegation indicator given its own header leaf at sub-index 2 (execution-specs#3324). Changes to the trie engine itself are limited to the tracer field, `Release` hygiene, and splitting `followAndUpdate` into `seek` + `updateCell`. Against the EIP-8297 fixtures filled from execution-specs `e4d7865b6` (227 cases), the blockchain corpus passes 78/82 with this branch merged into the base. Four remain: three receipt-root mismatches (`contract_creating_transaction`, `selfdestruct_same_transaction_leaves_no_account`, `extcodecopy_from_initcode_clones_chunked_code`) and one gas gap in `initcode_size_limit_boundary[at_max]` that does not track the EIP-8038 constants. The 63 state-test fixtures are not a signal either way: `selectCommitmentVariant` is wired only into the blocktest path, so they execute on the hex trie and compare against binary-tree roots. --- AGENTS.md | 6 + common/dbg/experiments.go | 14 + db/state/erigondb_settings.go | 15 +- db/state/execctx/options.go | 4 +- docs/pbin-encoding.md | 901 ++++++++ execution/commitment/commitment.go | 4 + execution/commitment/commitment_test.go | 18 + .../commitmentdb/commitment_context.go | 47 +- .../commitmentdb/pbin_witness_test.go | 255 +++ .../commitment/parallel_patricia_hashed.go | 3 + .../parallel_patricia_hashed_test.go | 17 + execution/commitment/pbin_adversarial_test.go | 129 ++ execution/commitment/pbin_code.go | 8 +- execution/commitment/pbin_code_test.go | 147 +- execution/commitment/pbin_conformance_test.go | 277 +++ execution/commitment/pbin_delegation_test.go | 143 ++ execution/commitment/pbin_domainwrite_test.go | 42 + execution/commitment/pbin_fuzz_test.go | 117 +- execution/commitment/pbin_hash.go | 36 +- execution/commitment/pbin_hash_test.go | 4 +- execution/commitment/pbin_hashsuite_test.go | 2 +- execution/commitment/pbin_hazard_test.go | 2 + execution/commitment/pbin_keys.go | 74 +- execution/commitment/pbin_keys_test.go | 24 +- execution/commitment/pbin_oracle_test.go | 13 +- execution/commitment/pbin_overflow_test.go | 131 +- execution/commitment/pbin_pathlimit_test.go | 87 + execution/commitment/pbin_patricia_hashed.go | 205 +- execution/commitment/pbin_process_test.go | 140 +- execution/commitment/pbin_reclaim_test.go | 125 ++ execution/commitment/pbin_specengine_test.go | 2 +- execution/commitment/pbin_specroots_test.go | 6 +- .../commitment/pbin_storage_layout_test.go | 172 ++ execution/commitment/pbin_unfold_test.go | 2 +- execution/commitment/pbin_update_stream.go | 234 ++- execution/commitment/pbin_values.go | 28 +- execution/commitment/pbin_verify_test.go | 69 +- execution/commitment/pbin_witness.go | 126 ++ .../commitment/pbin_witness_codezone_test.go | 314 +++ execution/commitment/pbin_witness_context.go | 292 +++ .../commitment/pbin_witness_context_test.go | 197 ++ execution/commitment/pbin_witness_decode.go | 202 ++ .../commitment/pbin_witness_decode_test.go | 325 +++ execution/commitment/pbin_witness_prune.go | 118 ++ .../commitment/pbin_witness_prune_test.go | 371 ++++ execution/commitment/pbin_witness_state.go | 317 +++ execution/commitment/pbin_witness_test.go | 389 ++++ execution/commitment/pbin_zerovalue_test.go | 167 +- .../testdata/binary_trie_vectors.json | 777 +++++++ .../commitment/testdata/eip8297_vectors.json | 1859 ++++++++++++++++- execution/stagedsync/exec3.go | 18 +- execution/tests/testutil/block_test_util.go | 97 +- .../tests/testutil/block_test_util_test.go | 68 + node/eth/backend.go | 14 +- rpc/jsonrpc/debug_execution_witness.go | 290 ++- rpc/jsonrpc/debug_execution_witness_test.go | 43 +- rpc/jsonrpc/eth_call.go | 4 +- rpc/jsonrpc/pbin_witness_altspec_test.go | 240 +++ rpc/jsonrpc/pbin_witness_bytesplit_test.go | 103 + rpc/jsonrpc/pbin_witness_clone_test.go | 201 ++ rpc/jsonrpc/pbin_witness_deploy_test.go | 83 + rpc/jsonrpc/pbin_witness_e2e_test.go | 362 ++++ rpc/jsonrpc/pbin_witness_granularity_test.go | 262 +++ rpc/jsonrpc/pbin_witness_phases_test.go | 137 ++ rpc/jsonrpc/pbin_witness_reachable_test.go | 182 ++ rpc/jsonrpc/pbin_witness_size_test.go | 198 ++ rpc/jsonrpc/pbin_witness_stateless.go | 392 ++++ rpc/jsonrpc/pbin_witness_stateless_test.go | 811 +++++++ rpc/jsonrpc/pbin_witness_whale_test.go | 214 ++ .../testdata/hex_witness_baseline.json | 72 + 70 files changed, 12180 insertions(+), 568 deletions(-) create mode 100644 docs/pbin-encoding.md create mode 100644 execution/commitment/commitmentdb/pbin_witness_test.go create mode 100644 execution/commitment/pbin_adversarial_test.go create mode 100644 execution/commitment/pbin_conformance_test.go create mode 100644 execution/commitment/pbin_delegation_test.go create mode 100644 execution/commitment/pbin_pathlimit_test.go create mode 100644 execution/commitment/pbin_reclaim_test.go create mode 100644 execution/commitment/pbin_storage_layout_test.go create mode 100644 execution/commitment/pbin_witness.go create mode 100644 execution/commitment/pbin_witness_codezone_test.go create mode 100644 execution/commitment/pbin_witness_context.go create mode 100644 execution/commitment/pbin_witness_context_test.go create mode 100644 execution/commitment/pbin_witness_decode.go create mode 100644 execution/commitment/pbin_witness_decode_test.go create mode 100644 execution/commitment/pbin_witness_prune.go create mode 100644 execution/commitment/pbin_witness_prune_test.go create mode 100644 execution/commitment/pbin_witness_state.go create mode 100644 execution/commitment/pbin_witness_test.go create mode 100644 execution/commitment/testdata/binary_trie_vectors.json create mode 100644 execution/tests/testutil/block_test_util_test.go create mode 100644 rpc/jsonrpc/pbin_witness_altspec_test.go create mode 100644 rpc/jsonrpc/pbin_witness_bytesplit_test.go create mode 100644 rpc/jsonrpc/pbin_witness_clone_test.go create mode 100644 rpc/jsonrpc/pbin_witness_deploy_test.go create mode 100644 rpc/jsonrpc/pbin_witness_e2e_test.go create mode 100644 rpc/jsonrpc/pbin_witness_granularity_test.go create mode 100644 rpc/jsonrpc/pbin_witness_phases_test.go create mode 100644 rpc/jsonrpc/pbin_witness_reachable_test.go create mode 100644 rpc/jsonrpc/pbin_witness_size_test.go create mode 100644 rpc/jsonrpc/pbin_witness_stateless.go create mode 100644 rpc/jsonrpc/pbin_witness_stateless_test.go create mode 100644 rpc/jsonrpc/pbin_witness_whale_test.go create mode 100644 rpc/jsonrpc/testdata/hex_witness_baseline.json diff --git a/AGENTS.md b/AGENTS.md index ee2035d1434..28d5e075175 100644 --- a/AGENTS.md +++ b/AGENTS.md @@ -120,6 +120,12 @@ Don't sign commits, pr's, issues, comments. `package commitment` holds two engines in one namespace. Every package-level identifier belonging to the EIP-8297 binary trie carries a `pbin` prefix (`PBin` for exported ones) — the hex engine already owns the generic names (`cell`, `fold`, `unfold`, `computeCellHash`), so an unprefixed addition is a collision waiting to happen. Test helpers included. +Selecting the binary trie is process-global, not a per-tester option: set `statecfg.ExperimentalBinCommitment` and `statecfg.BinCommitmentHash`, then `commitment.SetPBinHashSuite`. Calling `SetPBinHashSuite` alone is undone by the settings resolver's keccak default. A test that flips these must restore them in `t.Cleanup` and must not call `t.Parallel` — a concurrent hex test reads the same globals. + +The EIP-8297 embedding is not versioned on disk. `erigondb.toml` records `trie_variant` and `trie_hash` and guards a change of either, but nothing records which embedding wrote the state — so a change to key derivation or leaf layout silently recomputes different roots over an existing bin datadir. Rebuild bin datadirs from genesis whenever the embedding changes. + +Cite by name, never by line number. An EIP reference is `eip:"
"`, not `eip:NNN-NNN`; a reference to erigon source from `docs/` names the identifier and its file, not `file.go:NNN`. Line anchors rot on the next edit in either repo, and a stale one is worse than none — it points a reader at unrelated code with full confidence. + Run `make lint` before every push. The linter is non-deterministic — run it repeatedly until clean. **Important**: Always run `make lint` after making code changes and before committing. Fix any linter errors before proceeding. PRs must pass `make lint` before being opened or updated. diff --git a/common/dbg/experiments.go b/common/dbg/experiments.go index c326597c196..5c69d8a1657 100644 --- a/common/dbg/experiments.go +++ b/common/dbg/experiments.go @@ -73,6 +73,10 @@ var ( // cross-checks execution results. CheckHeaderStateRoot = EnvBool("CHECK_HEADER_STATE_ROOT", true) + warnRootCheckOff = sync.OnceFunc(func() { + log.Warn("HEADER STATE-ROOT CHECK IS DISABLED (CHECK_HEADER_STATE_ROOT=false): nothing cross-checks execution results against headers; a wrong chain will look healthy") + }) + // force skipping of any non-Erigon2 .torrent files DownloaderOnlyBlocks = EnvBool("DOWNLOADER_ONLY_BLOCKS", false) @@ -169,6 +173,16 @@ func init() { } } +// WarnHeaderStateRootCheckDisabled says once per process that nothing +// cross-checks execution against headers. Node startup and the execution path +// both call it, so a runner that never boots a node still says so. +func WarnHeaderStateRootCheckDisabled() { + if CheckHeaderStateRoot { + return + } + warnRootCheckOff() +} + func ReadMemStats(m *runtime.MemStats) { if noMemstat { return diff --git a/db/state/erigondb_settings.go b/db/state/erigondb_settings.go index 28c3dee779d..ac708b10c3d 100644 --- a/db/state/erigondb_settings.go +++ b/db/state/erigondb_settings.go @@ -84,19 +84,24 @@ func reconcileTrieVariant(s *ErigonDBSettings, logger log.Logger) error { return fmt.Errorf("--experimental.bin-commitment.hash=%s: datadir was built with %q; the bin trie needs a fresh datadir to change hash", statecfg.BinCommitmentHash, stored) } - if err := commitment.SetPBinHashSuite(stored); err != nil { - return fmt.Errorf("erigondb.toml: %w", err) + // Resolution runs per RPC request and per aggregator open, while the + // selected suite is read unsynchronized by every engine; only write it + // when it actually has to change. + if commitment.PBinHashSuiteName() != stored { + if err := commitment.SetPBinHashSuite(stored); err != nil { + return fmt.Errorf("erigondb.toml: %w", err) + } } case TrieVariantHex: if s.TrieHash != nil { return errors.New("erigondb.toml: trie_hash is meaningless under trie_variant \"hex\"") } - if statecfg.BinCommitmentHash != "" { - return errors.New("--experimental.bin-commitment.hash needs --experimental.bin-commitment") - } if statecfg.ExperimentalBinCommitment { return errors.New("--experimental.bin-commitment: datadir was created with the hex commitment trie; the bin trie needs a fresh datadir") } + if statecfg.BinCommitmentHash != "" { + return errors.New("--experimental.bin-commitment.hash needs --experimental.bin-commitment") + } default: return fmt.Errorf("erigondb.toml: unknown trie_variant %q", s.TrieVariantName()) } diff --git a/db/state/execctx/options.go b/db/state/execctx/options.go index fe7cc7a9280..8ce400ee81a 100644 --- a/db/state/execctx/options.go +++ b/db/state/execctx/options.go @@ -64,8 +64,8 @@ func WithoutParallelCommitment() SharedDomainOption { } // WithHexCommitmentOnly is WithoutParallelCommitment for callers that can only read -// hex branch records — witness, eth_getProof, eth_simulateV1, receipt regeneration, -// commitment integrity. Under the bin variant NewSharedDomains returns +// hex branch records — eth_getProof, eth_getWitness, eth_simulateV1, receipt +// regeneration, commitment integrity. Under the bin variant NewSharedDomains returns // ErrBinCommitmentUnsupported instead of reading bit-path records as hex ones. func WithHexCommitmentOnly() SharedDomainOption { return func(o *sharedDomainOptions) { diff --git a/docs/pbin-encoding.md b/docs/pbin-encoding.md new file mode 100644 index 00000000000..32cd7c4ae0d --- /dev/null +++ b/docs/pbin-encoding.md @@ -0,0 +1,901 @@ +# How erigon encodes the EIP-8297 partitioned binary tree + +All file references are relative to `execution/commitment/` and name an identifier rather than a +line number, which drifts on every edit. Every identifier belonging to this engine carries a `pbin` +prefix; the hex MPT engine lives in the same package and owns the unprefixed names. + +The engine is `PBinPatriciaHashed` (`pbin_patricia_hashed.go`). It borrows the hex engine's +grid/unfold/fold skeleton and none of its node model: arity 2, no extension node, no storage root, +and a leaf commits its complete tree key (the type's doc comment). + +Everything below was produced by running the engine. Hex is real. + +--- + +## 1. Tree keys + +A tree key is `zone(1) || treePosition || subIndex(1)`, assembled by `pbinTreeKey` +(`pbin_keys.go`). Three zones exist, each admitting exactly one key length +(`pbinZoneKeyLength`, `pbin_keys.go`): + +| zone | name | key length | treePosition | +|------|---------|-----------:|---------------------------------| +| 0x00 | account | 34 | `stem = H(addr32)` | +| 0x01 | code | 34 | `H(codeHash \|\| 0*24 \|\| u64BE(codeIndex))` | +| 0xFF | storage | 66 | `stem \|\| H(addr32 \|\| u256BE(slotIndex))` | + +The two indexes are unrelated quantities, and neither preimage is a bare concatenation of +naturally-sized values — both are exactly 64 bytes, with the index widened to fill the tail: + +- `codeIndex = chunkID / 256`, the chunk's code group, written as 8 big-endian bytes after 24 zero + bytes (§10, `codeChunkKey`, `pbin_keys.go`). No address takes part: the code zone is + content-addressed, so two accounts running the same bytecode share one set of leaves. +- `slotIndex = slot >> 8`, written as a 32-byte big-endian value, which is `0x00 || slot[0:31]` + (§9, `groupDigest`, `pbin_keys.go`). + +The trailing `subIndex` byte is `chunkID % 256` for code and `slot & 0xFF` for storage. + +Zones `0x02..0xFE` have no length and `pbinTreeKey` panics on them. The fixed length per zone *is* +the prefix-free invariant, and it is re-asserted at hash time from the key's own first byte +(`leafCellHash`, `pbin_hash.go`) so a malformed key cannot reach the hasher. + +`addr32` is the 20-byte address left-zero-padded to 32 (`pbinAddr32`, `pbin_keys.go`). +`H` is Keccak-256 by default, blake3 under `--experimental.bin-commitment.hash` +(`SetPBinHashSuite`, `pbin_hash.go`). Key derivation and node hashing both use `H`, and +`setHashSuite` (`pbin_patricia_hashed.go`) swaps both seams at once so neither can be configured +alone. + +Two digests are memoized per `pbinDigestCache` (`pbin_keys.go`): the stem, keyed on +`addr32`, and the storage group hash, keyed on `(addr32, slot[0:31])`. The group entry is bound to +the address as well as the index, so an address change cannot yield a stale hit. + +## 2. `pbinBitpath` + +A path through the tree is up to 528 bits — the longest key, a 66-byte storage leaf +(`pbinMaxPathBits`, `pbin_bitpath.go`). It is held as nine big-endian words plus a bit count +(`pbinBitpath`, `pbin_bitpath.go`): + +``` +bit index 0 63 64 127 ... 512 527 + +------------------+------------------+ ... +----------------+ + | w[0] | w[1] | | w[8], 16 used | + +------------------+------------------+ ... +----------------+ + MSB first MSB first bits 512..527 +``` + +Bit `d` lives in `w[d/64]` at shift `63-(d%64)` (`bit`, `setBitAt`); source byte `i` loads into +`w[i/8] << (56-8*(i%8))` (`pbinPathFromBits`). Word order therefore equals descent order and +divergence is XOR plus `LeadingZeros64` with no reversal — the reason for the layout +(`pbinBitpath`'s doc comment). + +``` +key = 00b10e2d527612073b26eecdfd717e6a320cf44b4afac2b0732d9fcbe2b7fa0cf600 (34 B) +bitLen = 272 +w[0] = 00b10e2d52761207 w[1] = 3b26eecdfd717e6a w[2] = 320cf44b4afac2b0 +w[3] = 732d9fcbe2b7fa0c w[4] = f600000000000000 w[5..8] = 0 +``` + +`bitLen` is the only authority on length. `bit` panics past it, and `appendPackedBits` emits +`ceil(bitLen/8)` bytes and re-masks the last. + +**Masking invariant.** Every path this engine builds holds zero bits at and past `bitLen`. +`maskTail` enforces it, called from `pbinPathFromBits`, `truncate` and the canonicality check in +`pbinDecodeBitPath`; `slice`, `appendBit` and `append` preserve it by construction, writing only +bits below the new `bitLen`. `setBitAt` is the one mutator that *can* dirty the tail — it is +bounded by `pbinMaxPathBits`, not by `bitLen` — and every caller writes inside the path. The type's +own doc comment is weaker, allowing anything past `bitLen`: read it as what a reader may assume, +not as what the constructors produce. The invariant is not what makes the common-prefix scan safe +— that is the `limit` clamp in `pbinCommonPrefixBitsAt`, pinned against a deliberately dirty tail +at `TestPBinCommonPrefixBits_IgnoresBitsBeyondBitLen` (`pbin_bitpath_test.go`). What depends on it +is struct equality: paths and cells are compared with `==`, and `pbinDecodeBitPath` rejects a +non-canonical key by masking a copy and comparing words. + +`pbinCommonPrefixBitsAt(key, from, prefix)` counts agreeing bits between `key` read +from bit `from` and `prefix` read from bit 0. The asymmetry exists because the descent compares a +whole tree key against a cell prefix that starts partway down: + +``` + from=4 +key w[wi] : b b b b|X X X X X X X X ... << 4 + w[wi+1] : h h h h| ... >> 60, spliced in +prefix w[0] : X X X X X X X X X X X X ... word-aligned + ^ XOR, LeadingZeros64 = agreeing bits in this word +``` + +## 3. Three prefix encodings + +The same bit string is spelled three different ways depending on where it is written. + +| where | count | layout | code | +|---|---|---|---| +| node preimage | `u16` big-endian, leading | `u16(bitLen) \|\| packed` | `pbinAppendBitPrefix`, `pbin_hash.go` | +| cell in a branch record | uvarint, leading | `uvarint(bitLen) \|\| packed` | `pbinAppendCell`, `pbin_branch.go` | +| domain key | one byte `bitLen mod 8`, **trailing** | `packed \|\| byte(bitLen%8)` | `pbinAppendBitPath`, `pbin_bitpath.go` | + +``` +encode_bit_prefix domain key + 0 bits 0000 0 bits 00 + 1 bit '1' 000180 1 bit 0x80 8001 + 3 bits '101' 0003a0 3 bits 0xE0 e003 + 7 bits all-1 0007fe 7 bits 0xB0 b007 + 8 bits 0xAA 0008aa 8 bits 0xB1 b100 <- mod 8 == 0 + 9 bits 0009aa80 9 bits 0xB180 b18001 +528 bits all-1 0210 || ff*66 +``` + +The preimage count is what keeps a 7-bit prefix from colliding with an 8-bit one that agrees with it +on the pad bit (`pbinAppendBitPrefix`'s doc comment). + +The domain key puts its count *last* so a subtree stays contiguous in the keyspace: every descendant +of a `b`-bit path repeats its first `floor(b/8)` whole packed bytes and the leading `b mod 8` bits +of the next, so the whole subtree lands in one byte-range. A leading length field would sort by +depth first and scatter that range (`pbinAppendBitPath`'s doc comment). Contiguity is all the +layout buys — the order inside the range is **not** ancestors-before-descendants, and the comment +says so. +Counterexample, measured: + +``` +7 bits 1111111 -> fe07 +8 bits 11111110 -> fe00 fe00 < fe07, yet the 7-bit path is a prefix of the 8-bit one +``` + +Nothing range-scans the domain today: every access is a point lookup by exact key +(`unfoldBranchNode`, `foldBranch`, `foldDelete`, `materializeBranch`). Contiguity is a property a +future scan could rely on, not one anything currently depends on. + +`pbinDecodeBitPath` is total and canonical — one path, one key. It rejects an empty buffer, a tail +byte above 7, a non-zero tail with no payload, over 528 bits and set pad bits. Bijectivity is +fuzz-pinned by `FuzzPBinBitPathCodec` (`pbin_bitpath_test.go`). + +## 4. Node preimages + +Two shapes, distinguished by a leading tag byte (`pbinLeafTag` / `pbinBranchTag`, `pbin_hash.go`). + +``` +leaf 0x00 || tree key (34 or 66) || value (32) leafCellHash +branch 0x01 || u16(bitLen) || packed prefix || left(32) || right(32) + branchHash +``` + +A leaf carries its **complete** tree key, not a suffix — `leafCellHash` concatenates the descent +path with the cell's own prefix and requires the result to be whole bytes (`pbin_hash.go`). +A branch's prefix here is **relative**: the bits between the parent's split and this node's own, +cut at fold time by `pph.currentKey.slice(upDepth, depth-1)` (`foldBranch`). The +domain key for that same node holds the *absolute* path. They coincide only at the top. + +An absent child hashes as 32 zero bytes and is never omitted (`pbinEmptyTreeHash`, `branchHash`) — +`pbinEmptyTreeHash`, deliberately not `empty.RootHash`, which would build a different tree. So the +empty tree's root is 32 zero bytes, and a one-key tree's root is the leaf hash itself with no branch +wrapping it (`RootHash`). + +Real leaf preimage and its hash, for the account of §5.1 (code_size 6, balance 0x3e8): + +``` +00 0012b9c2d7398802bddf3d70e0e8cf9074f4819101be174b883975a79061d53e7a 00 + 0000000000000006 0000000000000003 000000000000000000000000000003e8 +-> 5dbe9906fc51df4ac846a8fe44ed92a3ec310d2edaeeea605289a290f6b38eba +``` + +Real branch preimage, 5-bit prefix, both children being the leaves above: + +``` +01 0005 00 + 5dbe9906fc51df4ac846a8fe44ed92a3ec310d2edaeeea605289a290f6b38eba + 970021c05f854ea9f1b9dd97d180ae62d0d2b9bb4acc23869cc5879919434ef8 +-> de50844a66c2a773d715492d679fee88416467c0c5a7802a6b033199b357b0a4 +``` + +`de5084…` is the byte string stored as cell 0's hash in the 265-bit record dumped in §5. + +## 5. The branch record + +### 5.1 The example tree + +One account (address `0102…14`, nonce 3, balance 1000, code `60aabb000102`), storage slot 5 holding +5, storage slot 300 holding 0x2c. Four branch records plus the root record. +`stem = 12b9c2d7…61d53e7a`, `codeHash = 1d6423ed…7696574d`. + +Those values are the corpus, not something the records carry: a record names a leaf's identity only +(§6), so every hash below and in §12 needs them supplied from outside. The tree's five leaves in +full: BASIC_DATA packing nonce 3 / balance 1000 / code_size 6; CODE_HASH = +`keccak256(60aabb000102) = 1d6423ed…7696574d`; code chunk 0 in the code zone, that code padded to +31 bytes; slot 5 = `0000…0005`; slot 300 = `0000…002c` (44 — the value, not the slot number). + +The account holds no DELEGATION leaf: its code is contract code, so it takes the CODE_HASH branch +of the exclusive pair (§8). + +Each record below is named by its domain key, with the parent cell it hangs off: + +``` + key 08 root cell: branch, 0-bit prefix, hash = state root + its node is the record at key 00 + + key 00 [ 0 bits] splits on bit 0 (the zone byte's top bit) + |-- bit0 branch, 6-bit prefix ---------------------> node at 7 bits = key 0007 + `-- bit1 leaf, 527-bit prefix, storageAddr --------> slot 300 (528-bit key, zone 0xFF) + + key 0007 [ 7 bits] splits on bit 7, the zone byte's last: + account zone 0x00 against code zone 0x01 + child of key 00, cell bit 0 + |-- bit0 branch, 257-bit prefix ---------------------> node at 265 bits = key 0012b9…7a0001 + `-- bit1 leaf, 264-bit prefix, leafValue ----------> code chunk 0 (zone 0x01, 272-bit key) + + key 0012b9…7a0001 [265 bits] = 0x00 || stem || sub-index bit 264, which is 0 for every + allocated sub-index; splits on sub-index bit 265 + child of key 0007, cell bit 0 + |-- bit0 branch, 5-bit prefix -----------------------> node at 271 bits = key 0012b9…7a0007 + `-- bit1 leaf, 6-bit prefix 000101, storageAddr ---> slot 5 (sub 0x45 = 64+5) + + key 0012b9…7a0007 [271 bits] splits on the sub-index's last bit + child of key 0012b9…7a0001, cell bit 0 + |-- bit0 leaf, 0-bit prefix, accountAddr ------------> BASIC_DATA (sub 0x00) + `-- bit1 leaf, 0-bit prefix, accountAddr ------------> CODE_HASH (sub 0x01) +``` + +Every chain descends through cell bit 0; the bit-1 cells are all leaves. + +The chunk leaf hanging off the zone byte rather than off the account's stem is what content +addressing looks like in the tree: the account's three header keys and its code share nothing below +bit 7. + +Depth arithmetic closes at every step: `record bits + 1 branch bit + cell prefix bits = child's +absolute depth`. `0+1+6 = 7`, `7+1+257 = 265`, `265+1+5 = 271`, `271+1+0 = 272` (the 34-byte account +key), `7+1+264 = 272` (the 34-byte code key), `0+1+527 = 528` (the 66-byte storage key). + +### 5.2 Layout + +``` ++=====================+ written by encode, +| touchMap u16 BE | read by pbinDecodeBranch +| afterMap u16 BE | ++=====================+ +| cell body for bit 0 | present iff afterMap & 1 +| cell body for bit 1 | present iff afterMap & 2 ++=====================+ +``` + +Cells are emitted in ascending bit order (`bitset & -bitset` / `TrailingZeros16`, +`encode`, `pbin_branch.go`); the decoder mirrors it exactly (`pbinDecodeBranch`). + +``` +cell body pbinAppendCell / pbinDecodeCell + fields 1 byte bitmask, below + bitLen uvarint prefix length in BITS, 0..528 + prefix ceil(bitLen/8) bytes, MSB-first, pad bits zero + [accAddr] uvarint(20)=0x14 || 20 bytes + [stoAddr] uvarint(52)=0x34 || 52 bytes + [value] uvarint(32)=0x20 || 32 bytes + [hash] uvarint(32)=0x20 || 32 bytes +``` + +`fields` (`pbinCellFields`, `pbin_branch.go`): bit0 LEAF, bit1 BRANCH, bit2 ACCOUNT_ADDR, +bit3 STORAGE_ADDR, bit4 HASH, bit5 LEAF_VALUE. The optional blocks appear in one fixed order in +both encoder and decoder — accAddr, stoAddr, LEAF_VALUE, HASH (`pbinAppendCell` / +`pbinDecodeCell`) — and that is +**not** the bit order: LEAF_VALUE is bit 5 and HASH is bit 4, so LEAF_VALUE is written first. The +fields byte says which blocks are present, not what order to read them in; a decoder that walks it +LSB-to-MSB takes HASH before LEAF_VALUE and desynchronises the cursor on any cell carrying both +(the §5.5 format-ceiling row). The length prefixes are uvarints but `pbinDecodeFixedVal` +demands the one exact width per field, making `0x14` / `0x34` / `0x20` the only legal +tag bytes. + +The cell prefix is relative to the record's own key plus the branch bit: the record's key is +`pbinAppendBitPath(currentKey)`, the child sits at `keyBits+1`, and `prefix` carries the remainder +down to the child node. + +### 5.3 A real record, byte by byte + +The 265-bit record from §5.1 — a branch child and a header-storage leaf. + +``` +key 0012b9c2d7398802bddf3d70e0e8cf9074f4819101be174b883975a79061d53e7a0001 + +00000000 00 03 00 03 12 05 00 20 de 50 84 4a 66 c2 a7 73 |....... .P.Jf..s| +00000010 d7 15 49 2d 67 9f ee 88 41 64 67 c0 c5 a7 80 2a |..I-g...Adg....*| +00000020 6b 03 31 99 b3 57 b0 a4 09 06 14 34 01 02 03 04 |k.1..W.....4....| +00000030 05 06 07 08 09 0a 0b 0c 0d 0e 0f 10 11 12 13 14 |................| +00000040 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 |................| +00000050 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 05 |................| + +[00..01] 0003 touchMap = 0b11 +[02..03] 0003 afterMap = 0b11 +cell bit 0 +[04] 12 fields = 00010010 BRANCH | HASH +[05] 05 bitLen = uvarint 5 +[06] 00 prefix = 00000 + 3 zero pad bits +[07..27] 20 || hash = de50844a66c2a773d715492d679fee88416467c0c5a7802a6b033199b357b0a4 +cell bit 1 +[28] 09 fields = 00001001 LEAF | STORAGE_ADDR +[29] 06 bitLen = uvarint 6 +[2a] 14 prefix = 000101 + 2 zero pad bits +[2b..5f] 34 || stoAddr = 0102030405060708090a0b0c0d0e0f1011121314 + 0000…0005 (addr || slot, 52 bytes) +``` + +Sub-index reconstruction for cell 1: the record sits at 265 bits, so the sub-index's top bit is +already fixed to `0` by the prefix above it and this record's branch bit supplies the next, `1`. +The cell prefix then supplies `000101`. Full sub-index `0b01000101 = 0x45 = 64 + 5` — storage slot 5 +in the account header (§9). + +The other three records of the same tree: + +``` +key 00 [0 bits] 162 bytes + 0003 0003 + 12 06 00 20 c9aca54ec7a6c2fe06fc1cee22bd609b559f1c16217ce2ea48793349b8d61be5 + 09 8f04 fe257385ae7310057bbe7ae1c1d19f20e9e90322037c2e971072eb4f20c3aa7cf4 + 3211d8496e2c633f71a67a015a0551623e46676cc65d3acc04301137a5fc5a8458 + 34 0102030405060708090a0b0c0d0e0f1011121314 + 000000000000000000000000000000000000000000000000000000000000012c + +key 0007 [7 bits] 142 bytes + 0003 0003 + 12 8102 12b9c2d7398802bddf3d70e0e8cf9074f4819101be174b883975a79061d53e7a00 + 20 ac8c75fc4b6f6e25d0831229dd10d3ad353c56dc573141f6f7e62707a5076b5d + 21 8802 073be86901ad75392dc6c8cd03071cf8e0c17da59c33a1911c7b85c09f969b5a00 + 20 0060aabb00010200000000000000000000000000000000000000000000000000 + +key 0012b9…7a0007 [271 bits] 50 bytes + 0003 0003 + 05 00 14 0102030405060708090a0b0c0d0e0f1011121314 + 05 00 14 0102030405060708090a0b0c0d0e0f1011121314 +``` + +The 7-bit record shows the two zones side by side and needs no shift to read: seven bits are +consumed above it and one more by its own branch, so both cell prefixes start on a byte boundary — +cell 0 carries `stem || 0x00` (the account key from byte 1 on), cell 1 the chunk key's own 33 bytes. +The top record is where the shift shows: the storage key starts `ff 12 b9…` and its cell prefix +(bits 1..527) starts `fe 25 73…`, since shifting left by one turns `ff 12 b9` into `fe 25 73` +(`0xff<<1 | 0x12>>7 = 0xfe`). + +The 271-bit record is the account pair: two leaf cells, zero-bit prefixes, both naming the *same* +20-byte plain key. Which leaf each is is decided by the last bit of the reconstructed tree key and +resolved at hash time by `pbinLeafValue` (`pbin_hash.go`), not by anything in the record. + +### 5.4 touchMap and afterMap + +Both are `uint16` at offsets 0 and 2 (`encode` and `pbinDecodeBranch`, `pbin_branch.go`) purely so +the `OnesCount16` / `TrailingZeros16` arithmetic ports from the hex engine unchanged (`pbinGrid`'s +doc comment, `pbin_cell.go`). Only bits 0 and 1 may be set; `pbinCheckCellMaps` rejects anything +outside `pbinCellBits = 0b11` on both encode and decode. + +`afterMap` is structural — it says which cell bodies follow. `touchMap` is write-time bookkeeping +only. The reader throws it away (`_, afterMap, err := pbinDecodeBranch`, +in `unfoldBranchNode`; the only other call site is `materializeBranch`), and +nothing downstream parses the record either: `TrieContext.PutBranch` hands the bytes straight to +`DomainPut` (`commitmentdb/commitment_context.go`). There is no `BranchData` merge. + +On disk, `afterMap` of a branch record is always `0b11`: `foldBranch` refuses a row that does not +keep exactly two cells (`foldBranch`). A row collapsing to one survivor writes +no record at all — the node moves up and the consumed bits are prepended to the survivor's prefix +(`foldPropagate`); a row keeping nothing writes a zero-length value, which is the deletion +encoding (`foldDelete`). `touchMap` does vary: bits are set at update time (`updateCell`) and +carried upward by `propagateTouch`. + +Both non-branch outcomes are reachable: + +- **One survivor** is routine, and has nothing to do with removal. An unfold that descends into a + cell seeds the new row with that one cell (`unfold`), so a row that no later update splits folds + straight back through `foldPropagate` — the exact inverse of the unfold that opened it. +- **No survivor** needs a parent cell that was touched and is now absent, which `unfoldBranchNode` + loads as `after = 0` through its `deleted` flag. A write of 32 zero bytes is a + deletion (§11), so zeroing a subtree's last leaf reaches it; pinned at + `TestPBinFoldDeleteRunsOnProcess` (`pbin_zerovalue_test.go`). + +A reader still needs an answer for a zero-length value, and it differs by key: at a bit-path key +`unfoldBranchNode` rejects it as a missing branch, so it is not a shape a decoder has to parse; at +the root key `0x08` it is legal and means the empty tree (`loadRoot`). + +That every record carries both children is what removes the merge path +(`pbinBranchEncoder`'s doc comment, `pbin_branch.go`): at arity 2 the untouched sibling is the +whole other half of the subtree, so a record read back replaces its predecessor outright. + +### 5.5 Size + +Per cell: `1 (fields) + 1..2 (bitLen uvarint) + 0..66 (packed prefix) + one value block`. Value +blocks are 21 (account), 53 (storage), 33 (verbatim value), 33 (hash). + +| shape | bytes | reachable | +|---|---:|---| +| `afterMap = 0` | 4 | decodes; `foldBranch` never writes it | +| one bare BRANCH cell `000100010200` | 6 | same | +| two bare BRANCH cells `0003000302000200` | 8 | same | +| two hashed branch cells | 74 | yes | +| **writer floor** — two 0-prefix account leaves | **50** | yes, once in §5.1 (the 271-bit record) | +| **writer ceiling** — two 527-bit-prefix storage leaves | **248** | only at a depth-0 record | +| **format ceiling** — the same plus a HASH block on each | **314** | decodes; writer never emits it | + +All seven rows encode-and-decode round-trip. Measured record sizes for the §5.1 corpus: 162, 142, +96, 50, plus a 35-byte root record. The root record is framed differently and sized in §7. + +Size is driven, in order of weight, by: the two prefix bit lengths (up to 66 bytes each — all the +variance lives here, and it is inverse to depth); which value each child names (53 > 33 > 21); and +the 1-vs-2-byte `bitLen` uvarint at the 128-bit boundary. + +### 5.6 Decoding + +`pbinDecodeBranch(data, cells *[2]pbinCell)` (`pbin_branch.go`) resets both cells +unconditionally, requires ≥4 bytes, reads the maps, re-checks them, then walks +`afterMap` in ascending bit order filling `cells[TrailingZeros16(bit)]`. A cell whose bit is clear +stays zeroed — that is how an absent child is spelled. Any leftover byte is an error. + +Each body restores kind from the LEAF/BRANCH bits; the prefix from the explicit bit count, never +from the byte length, with pad bits asserted zero (`pbinDecodePrefix`); `accountAddrLen` / +`storageAddrLen` / `hashLen` as side effects of their fields being present; and a LEAF_VALUE as +`Update{Flags: StorageUpdate, StorageLen: 32}`. + +Rejections, each observed firing: + +``` +pbinDecodeCell unknown field bits; neither or both node kinds; a leaf naming + 0 or 2+ value sources; a branch carrying a leaf value +pbinDecodePrefix a prefix over 528 bits; non-zero pad bits +pbinDecodeFixedVal a wrong length tag +pbinDecodeBranch trailing bytes + + leaf with both addrs -> malformed branch record: leaf cell fields 00001101 name no single value source + kind = leaf|branch -> malformed branch record: cell fields 00000111 name no single node kind + dirty pad bits -> malformed branch record: non-zero pad bits after a 3-bit prefix + trailing byte -> malformed branch record: 1 trailing bytes +``` + +One asymmetry against the "one canonical form" claim in `pbinDecodeBranch`'s doc comment: a BRANCH +cell carrying ACCOUNT_ADDR or STORAGE_ADDR decodes cleanly (only LEAF_VALUE is refused for +branches). The writer cannot produce it — `foldBranch` resets the upCell before setting kind — so +it is an unreachable spelling the decoder still accepts, +not a live bug. + +Caller side: `unfoldBranchNode` keeps `afterMap` and discards the record's `touchMap`, setting +`touch=0, after=afterMap` normally, or `touch=afterMap, after=0` when the parent cell was touched +and is now gone, which is how a whole subtree is dropped (`unfoldBranchNode`). + +## 6. Leaf cells in a record + +Yes — a leaf child is stored as a full cell body, not as a hash. What it carries is its *identity*, +never its state value, from exactly one of three sources (the decoder enforces exactly one, +`pbinDecodeCell`, `pbin_branch.go`). This section describes records **written by the fold**; the +witness context spells the same three fields differently, below. + +- **ACCOUNT_ADDR** — the 20-byte plain key, set from an update with `len(plainKey)==20` + (`updateCell`, `pbin_patricia_hashed.go`). +- **STORAGE_ADDR** — `addr||slot`, `len(plainKey)==52`. +- **LEAF_VALUE** — 32 raw bytes, and only when the leaf has no plain key at all: the encoder sets it + iff no address field is present (`pbinAppendCell`). That is the code chunk, the + EIP-7702 delegation indicator and any reserved sub-index — every leaf whose value no state domain + holds as a field (`pbinFieldLeafValue`, `pbin_branch.go`; `pbinRecordLeafValue`, `pbin_code.go`). + +A leaf's own hash is **not** in the record. `hashRowCell` writes a computed hash back only for +branch cells (`pbin_patricia_hashed.go`), and no other site sets `hashLen` on a leaf, so the +encoder's HASH field is never emitted for one in practice. The consequence is that rehashing a +decoded record's leaf child requires state-domain reads — `loadCellState` +(`pbin_patricia_hashed.go`) fetches the account or slot behind the plain key. Only branch +children are hash-only. + +Balance, nonce, code hash and storage value are absent for address-bearing leaves: the plain key is +the pointer back into the state domains. + +**Witness-produced records read differently.** A witness has no state domains behind it, so +`fillLeafCell` (`pbin_witness_context.go`) picks the field by re-encoding, not by zone: any +leaf whose 32 bytes round-trip through `pbinLeafValue` verbatim — storage slots, header slots, code +chunks — is written as LEAF_VALUE, and the rest — BASIC_DATA and CODE_HASH, which are +packed from account fields — go into ACCOUNT_ADDR as a synthetic 20-byte *handle*, the first 20 +bytes of the node hash, which the context resolves back to the account state. So on a +witness record ACCOUNT_ADDR is not an address and LEAF_VALUE is not evidence of a code chunk. +Consumers must know which producer wrote the record. + +**A witness pass is told what the parent state cannot say.** It walks the parent state, where a +contract the block deploys has no code and an account the block removed is indistinguishable from +one it created. Both decide which keys the pass has to walk, so the caller supplies them in a +`PBinWitnessBlock` (`pbin_witness.go`) keyed by account plain key: `chunkSource` +(`pbin_update_stream.go`) derives chunk keys from the supplied code — key derivation only, values +stay pre-state — and `removesAccount` reads the supplied removal set instead of the update's delete +flag. Both overrides apply on a witness pass only; a fold ignores a block left set. `SetWitnessBlock` +must be called before the capture, `Witnesses` clears it on return, and +`SharedDomainsCommitmentContext.SetWitnessBlock` (`commitment_context.go`) is a silent no-op on a +hex trie. A bin capture that skips it walks too few keys and prunes away nodes the verifier needs. + +**A witness carries the sibling of every branch its keys descend, not just the proof paths.** A +branch commits to both children, so a hash is enough to *verify* a path — but not to *change* one. +Removing a key collapses the branch above it and moves the surviving sibling up under a longer +prefix, and `H(0x01 || encode_bit_prefix(prefix) || left || right)` binds the prefix the sibling had, +so re-hashing it needs its own children. The capture therefore reads back any branch cell that +arrived as a bare hash (`captureBranchPreimage`, `pbin_patricia_hashed.go`) and the pruner keeps it +(`keepSibling`, `pbin_witness_prune.go`). This is what replaces hex's collapse-sibling detection +phase; the cost is roughly one extra node per level of each proved path. + +## 7. The root record + +`pbinRootKey = {0x08}` (`pbin_patricia_hashed.go`) holds a **bare cell body with no 4-byte +header**: `storeRoot` calls `pbinAppendCell` directly and `loadRoot` calls `pbinDecodeCell` at +position 0, rejecting trailing bytes. A zero-length value at that key is the deletion encoding for +an emptied tree (`storeRoot`). + +``` +key 08, 35 bytes — the §5.1 tree: + 12 00 20 658b62aba5ac2933e86f1100cce5084bdb35b32d797b05d247abf27a2018c064 + ^ BRANCH|HASH + ^ bitLen 0 + ^ len 32 || the state root +``` + +That is the common shape, not the only one. `storeRoot` serialises whatever the root cell is, +and a one-key tree's root is the leaf itself with no branch wrapping it (§4), so the +record can equally be a LEAF cell — with a full-length prefix, since no descent sits above it to +consume any of the key. Measured, the §5.1 address holding slot 300 and nothing else: + +``` +key 08, 122 bytes: + 09 9004 ff12b9…d2fe2d42 2c 34 0102…1314 0000…012c + ^ LEAF|STORAGE_ADDR + ^ uvarint 528 bits + ^ the whole 66-byte tree key, packed + ^ len 52 || addr || slot +``` + +Sizes follow the §5.5 per-cell arithmetic with no 4-byte header: `1 (fields) + 1..2 (bitLen uvarint) ++ 0..66 (packed prefix) + one value block of 21 / 33 / 53`. That is 35 bytes for the branch-and-hash +spelling above, 58 / 70 / 90 for a 272-bit leaf root naming an address, a verbatim value or an +`addr||slot`, and 122 at most — the 528-bit storage leaf shown. A decoder that assumes BRANCH|HASH +fails on every one-key tree. + +The root cell needs a key of its own, and not because nothing names it — the empty path does. The +problem is that the empty path already encodes to the 1-byte key `00`, which is the record of the +top branch node (see §5.1, where `00` and `08` are two different records of the same tree). Every +other node is found by the path that reaches it, so only the root is left needing a key nothing else +claims (`pbinRootKey`'s doc comment). The zero-length key is no alternative either: domain +iteration reads it as end-of-stream and it sorts first, truncating the table, so the datadir would +read back as fresh — pinned against the real `TblCommitmentVals` by +`TestPBinRootRecordRealTableIteration` (`pbin_rootkey_test.go`). + +`0x08` works because the trailing byte of every path key is `bitLen mod 8`, so its range is exactly +`0..7` and `pbinDecodeBitPath` rejects anything above (`pbinDecodeBitPath`). `0x08` is the +smallest byte that can never be a trailing bit count, so a 1-byte key of `0x08` cannot be any +encoded path — checked exhaustively over every `bitLen` 0..528 by +`TestPBinRootKeySentinelNotABitPath`. The same bound keeps `KeyCommitmentState` (`"state"`, tail +`0x65`) out of the path image (`TestPBinBitPathNeverEncodesToStateKey`, `pbin_bitpath_test.go`): + +``` +pbinDecodeBitPath(08) -> pbin: invalid trailing bit count 8 in bit-path key +pbinDecodeBitPath(7374617465) -> pbin: invalid trailing bit count 101 in bit-path key +``` + +The witness-side `PatriciaContext` uses the same two record framings — a bare cell for the root +(`rootRecord`, `pbin_witness_context.go`), a full header plus two cells for a branch, with +`touchMap` set equal to `afterMap` because a read discards it anyway (`branchRecord`). The framing +is shared; what goes into a leaf cell is not — see the witness paragraph in §6. + +## 8. The account header stem + +Zone `0x00`, 34 bytes, `treePosition = stem = H(addr32)`. The trailing byte is the sub-index, and it +partitions a 256-wide subtree under one stem: + +``` +byte: 0 1 ............................ 32 33 + +----+------------------------------------+------+ + | 00 | stem = H(addr32) | sub | + +----+------------------------------------+------+ + +sub 0 BASIC_DATA packed from account state + 1 CODE_HASH 32 raw bytes + 2 DELEGATION the 23-byte indicator, right-padded with nine zeros + 3 .. 63 reserved not packed; leaf carries 32 verbatim bytes + (pbinLeafValue -> pbinRecordLeafValue) + 64 .. 127 storage slots 0..63, value left-padded + 128 .. 255 unallocated no key this embedding derives lands here +``` + +Constants in `pbin_keys.go`; the dispatch that turns a sub-index into a leaf value is +`pbinLeafValue` (`pbin_hash.go`). + +Sub-indices 128..255 held the first 128 code chunks before every chunk moved into the code zone +(§10). They are now reserved like 3..63, and the dispatch treats both ranges the same: a leaf there +carries its 32 bytes verbatim rather than being packed from state, which is the right answer for a +sub-index whose meaning is not yet defined. + +BASIC_DATA packing (`pbinEncodeBasicData` and its offset constants, `pbin_values.go`), +big-endian throughout: + +``` +off: 0 1 2 3 4 8 16 32 + +----+-----------+-----------+----------------+-----------------------+ + |ver | reserved | code_size | nonce | balance (128 bit) | + | 0 | 0 0 0 | u32 | u64 | 16 bytes | + +----+-----------+-----------+----------------+-----------------------+ +``` + +Bytes 0..3 are never written; the zero value of the array supplies them. A balance over 128 bits or +a code size over 2^32-1 is an error, not a truncation — a silent truncation would commit a wrong +root (`pbinEncodeBasicData`). + +The CODE_HASH leaf is the raw 32-byte hash, with the zero hash mapped to `keccak256("")` for a +codeless account (`pbinCodeHashValue`). The DELEGATION leaf holds an EIP-7702 indicator — the 23 +bytes `0xef0100 || target` — right-padded with nine zeros (`pbinEncodeDelegation`, +`pbin_values.go`). That is *not* the chunk encoding of §10: an indicator never executes, so +byte 0 carries code rather than a PUSHDATA count. + +**An existing account holds exactly one of the two**, decided by its code bytes alone +(`pbinIsDelegation`, `pbin_values.go`) and never by its hash — a contract whose *hash* opens +`0xef0100` is still contract code. So a write emits one of the pair and deletes the other +unconditionally, since the stream is told nothing about what the account held a moment ago +(`emitCodeLeaves`, `pbin_update_stream.go`). A delegated account holds no code-zone chunks +at all: its leaf *is* its code, a read takes the leading `code_size` bytes and `EXTCODEHASH` hashes +them. Clearing a delegation restores a CODE_HASH leaf of `keccak256("")` with `code_size` zeroed. + +Neither sibling has a key derivation of its own: `treeKey` (`pbin_keys.go`) only ever +derives BASIC_DATA for an address, and the stream produces the sibling by overwriting the last key +byte inside the same visit (`emitSibling`, `pbin_update_stream.go`). + +Because the delegation leaf also marks an account present, a reader asking whether an account +exists must accept **either** sibling. BASIC_DATA is not that marker: an account with zero nonce, +zero balance and no code stores none (`PBinWitnessState.Account`, `pbin_witness_state.go`). + +``` +addr = 0102030405060708090a0b0c0d0e0f1011121314 +addr32 = 0000000000000000000000000102030405060708090a0b0c0d0e0f1011121314 +stem = 12b9c2d7398802bddf3d70e0e8cf9074f4819101be174b883975a79061d53e7a + +BASIC_DATA key 00 12b9…3e7a 00 +CODE_HASH key 00 12b9…3e7a 01 +DELEGATION key 00 12b9…3e7a 02 + +BASIC_DATA value, nonce=3 balance=1e18 code_size=100: + 00000000 00000064 0000000000000003 00000000000000000de0b6b3a7640000 + ^ver+rsv ^size ^nonce ^balance +CODE_HASH value — a *separate* example, for a codeless account (code_size 0), where the zero +hash maps to keccak256(""): + c5d2460186f7233c927e7db2dcc703c0e500b653ca82273b7bfad8045d85a470 +DELEGATION value — a third account, delegating to 00…aa, so code_size is 23: + ef0100 00000000000000000000000000000000000000aa 000000000000000000 + ^marker ^target (20 B) ^nine zero bytes +``` + +The three value lines are three different accounts. Pairing the first two would describe an account +running 100 code bytes whose code hashes empty, which no state can produce and the witness rejects +outright — `codeFromLeaves` re-checks the reassembled code against CODE_HASH +(`pbin_witness_state.go`). For one consistent account, see §5.1: code_size 6, CODE_HASH +`1d6423ed…7696574d`. + +## 9. The storage sub-trie + +`pbinSlotInHeader` (`pbin_keys.go`) decides: slot bytes `[0:31]` all zero **and** +`slot[31] < HEADER_STORAGE_SLOTS = 64`. So slots 0..63 only. The spec's invariant is +`HEADER_STORAGE_OFFSET + HEADER_STORAGE_SLOTS <= STEM_SUBTREE_WIDTH`, which pins the header slots +to sub-indices 64..127. + +Header slots take an account-zone key at sub-index `64 + slot` (`storageKey`, `pbin_keys.go`) — same +34-byte shape, same stem, no extra hash. Everything else goes to zone `0xFF`, 66 bytes +(`storageKey`): + +``` +byte: 0 1 .................. 32 33 ....................... 64 65 + +----+-----------------------------+----------------------------+------+ + | FF | stem = H(addr32) | group = H(addr32||treeIdx) | sub | + +----+-----------------------------+----------------------------+------+ + +treeIdx = slot >> 8, as a 32-byte big-endian value = 0x00 || slot[0:31] +sub = slot & 0xFF = slot[31] + +a group = the 256 consecutive slots sharing one treeIdx: + + slot 300 ff | 12b9…3e7a | 1908ec24…d2fe2d42 | 2c \ + slot 301 ff | 12b9…3e7a | 1908ec24…d2fe2d42 | 2d | identical 65-byte prefix + … | -> one dense subtree + slot 319 ff | 12b9…3e7a | 1908ec24…d2fe2d42 | 3f / +``` + +The group preimage is built by `groupDigest` (`pbin_keys.go`) as +`addr32 || 0x00 || slot[0:31]`, 64 bytes. Co-location of a group in one subtree is the point of the +layout, and the digest is memoized per group. + +The key carries **both** digests, and the stem digest is the same one the account leaves use, so a +storage key costs one extra hash over an account key, not two. + +Sharpest discontinuity in the embedding — slot 63 is a 34-byte key inside the account's own header, +slot 64 is a 66-byte key in a different zone (pinned by `TestPBinStorageLayoutCost`, +`pbin_storage_layout_test.go`): + +``` +slot 5 (34) 00 12b9…3e7a 45 +slot 63 (34) 00 12b9…3e7a 7f +slot 64 (66) ff 12b9…3e7a 1fef389e506c6134e0d9befd0702f549c08b2aeeba1bdf45776999fc988076f4 40 +slot 300 (66) ff 12b9…3e7a 1908ec24b716319fb8d33d00ad02a8b11f2333b6632e9d660218089bd2fe2d42 2c + +group preimage for slot 300: + 0000…0102030405060708090a0b0c0d0e0f1011121314 00 00000000000000000000000000000000000000000000000000000000000001 + -> 1908ec24b716319fb8d33d00ad02a8b11f2333b6632e9d660218089bd2fe2d42 +``` + +## 10. The code sub-trie + +A chunk value is 32 bytes: byte 0 is metadata, bytes 1..31 are code +(`pbinChunkDataLen = 31`, `pbin_code.go`). + +``` ++----+---------------------------------+ +| n | 31 bytes of bytecode | ++----+---------------------------------+ + ^ leading bytes of this chunk that are PUSHDATA, clamped to 31 +``` + +The code is zero-padded to a multiple of 31 **before** the PUSHDATA scan +(`pbinChunkifyCode`, `pbin_code.go`). That ordering buys two things: the last chunk is always a +full 32 bytes with a zero tail, and a PUSH whose data runs off the end of the real code counts +against the padded tail instead of being dropped. `pushdataAt[i]` is how many bytes from `i` on are +still PUSHDATA; the table is allocated a whole chunk past the padded code so a PUSH32 on the last +byte has room, and `chunk[0] = min(pushdataAt[pos], 31)`. A PUSH is any opcode in `[0x60, 0x7f]` +(`pbinPush1` .. `pbinPush32`). The scan runs over the whole code, so residual PUSHDATA carries +across chunk boundaries — which is exactly what the byte reports. + +``` +code (40 bytes), PUSH2 at offset 0 and PUSH32 at offset 30 so its data crosses the boundary: + 61aabb 000000000000000000000000000000000000000000000000000000 7f f0f1f2f3f4f5f6f7f8 + ^ 27 zero bytes, offsets 3..29 + +chunk 0 = 00 61aabb0000000000000000000000000000000000000000000000000000007f + ^^ offset 0 is an opcode +chunk 1 = 1f f0f1f2f3f4f5f6f7f800000000000000000000000000000000000000000000 + ^^ 31: every byte of this chunk is PUSH32 data, clamped from 32; + the tail is padding added before the scan + +short code 000102 -> one chunk: 00 000102 0000…00 +empty code -> zero chunks (pbinChunkifyCode) +``` + +**Every** chunk lives in the code zone; the account header holds none. One deriver takes a code hash +and a chunk id (`codeChunkKey`, `pbin_keys.go`): + +``` +treeIndex = chunkID / 256 the chunk's code group +preimage (64 B) = codeHash(32) || 24 zero bytes || u64BE(treeIndex) +key (34 B) = 0x01 || H(preimage) || byte(chunkID % 256) +``` + +An aligned run of 256 chunks sharing one `treeIndex` is a code group: its chunks share a stem and +differ only in the sub-index byte, so a contract of at most 256 chunks (7936 bytes) occupies one +dense subtree and the group edge is the only boundary in the layout. + +The derivation names no address — only the code hash. Two accounts running the same bytecode derive +identical keys and share one set of leaves, whatever the code's size +(`pbinTreeKeyCodeChunk`, `pbin_keys.go`). The dedup is realised at emit time: chunks are +buffered, sorted by key, and duplicate keys collapse to one emission, with an error if two carry +different values (`flushCodeChunks`, `pbin_update_stream.go`). The chunk digest is +deliberately not memoized: the digest cache's entries are bound to an address these keys do not +have (`codeChunkKey`'s doc comment). + +``` +codeHash = 7b1e263ffcf71ebd01a2edd752b53eb24ed6abf042e8678a4a1db8d05d5d31b0 + chunk 0 (group 0) 01 1b05bf4b082e83c2b306efdbfdd460ba5193adeebcec3ca8453a5cff437d3f4d 00 + chunk 255 (group 0) 01 1b05bf4b082e83c2b306efdbfdd460ba5193adeebcec3ca8453a5cff437d3f4d ff + chunk 256 (group 1) 01 2aeb430d323776088db507c7efbad5c4797d0f748b8b8a0112153cb665a413f5 00 + chunk 512 (group 2) 01 aa179620390ea03ed4cd924bbb94938f8162bf6741205ed18fbd5a34e449b9c0 00 + ^ each group is a fresh stem; no address in any of them +``` + +A chunk of 32 zero bytes is stored as no leaf at all, like any other zero value (§11). That takes +31 zero code bytes **and** a zero PUSHDATA count in byte 0 — zero bytes continuing PUSHDATA from an +earlier chunk do not qualify, since byte 0 then records the continuation. Chunk presence therefore +does not delimit the code: `code_size` does, and an absent chunk reads back as the zeros it stands +for (`codeFromLeaves`, `pbin_witness_state.go`). + +Chunk leaves carry no plain key: no state domain holds a code chunk, since chunking is a property of +the tree rather than of the account. They are emitted with a nil plain key +(`flushCodeChunks`), validated in `updateCell` (`pbin_patricia_hashed.go`), and stored under +`pbinFieldLeafValue` (§6). Emission ordering keeps the trie walk monotone: chunks are queued as +accounts are visited (`queueChunks`, `pbin_update_stream.go`) and flushed once a key past +the code zone appears (`flushCodeChunksBefore`). + +A delegated account queues nothing: its indicator lives in the header and it owns no chunk leaves +at all (§8). + +Read-back, for a stateless verifier: concatenate `value[1:]` of chunks `0..ceil(size/31)-1`, +truncate to `code_size`, verify against the CODE_HASH leaf (`pbin_witness_state.go`). + +## 11. There is no storage root + +Nothing computes one. The engine doc comment says so (`PBinPatriciaHashed`) and the +witness account type says so (`PBinAccount`) — those two comments are all +`grep -i "storage root" pbin_*.go` finds. The absence of code is a different grep, over the +identifier: `grep -n "storageRoot\|StorageRoot" pbin_*.go` returns nothing at all — no producer, no +consumer. `PBinAccount` is exactly Nonce, Balance, CodeSize, CodeHash +(`pbin_witness_state.go`). BASIC_DATA has no room for one either: +`1 + 3 + 4 + 8 + 16 = 32`, fully accounted (`pbin_values.go`). + +What replaces it is a single flat global trie. Account fields, storage slots and code chunks are all +ordinary leaves of *one* binary trie, each addressed by its own 34- or 66-byte tree key. There is no +nesting, so there is no second trie to have a root. `pbinLeafValue` (`pbin_hash.go`) +enumerates every value a leaf may hold — BASIC_DATA, CODE_HASH, a padded storage word, a verbatim +32-byte record value — and none of them is a subtree hash. + +An account and its storage are related only by sharing a key **prefix**: bytes 1..32 of the +account-zone key and bytes 1..32 of the storage-zone key are the same `H(addr32)` +(`accountHeaderStem` vs `storageKey`, `pbin_keys.go`). Prefix, not containment. The account's code +shares not even that: it is keyed by code hash and sits in a third zone. + +``` +account BASIC_DATA : 00 |12b9c2d7…61d53e7a| 00 +account CODE_HASH : 00 |12b9c2d7…61d53e7a| 01 +storage slot 5 : 00 |12b9c2d7…61d53e7a| 45 +storage slot 300 : ff |12b9c2d7…61d53e7a| 1908ec24…d2fe2d42 2c + ^^^^^^^^^^^^^^^^^^ same stem, different zone +code chunk 0 : 01 |073be869…9f969b5a| 00 + ^^^^^^^^^^^^^^^^^^ H(codeHash || 0), no stem at all +``` + +Compare the hex engine in the same package, where the MPT structure is explicit: +`accountForHashing(buffer, storageRootHash)` writes the 32-byte root into the account RLP +(`hex_patricia_hashed.go`), called from `computeCellHash` and +`witnessComputeCellHashWithStorage`; both derive `storageRootHash` down the fold, the witness one +threading a `storageRootHashIsSet` flag with it, and both give a storage-less account +`empty.RootHash`. The pbin fold has no equivalent variable. + +The near-miss worth naming so it is not mistaken for one: the subtree under the 264-bit prefix +`0xFF || stem` does hold exactly one account's non-header slots, and the cell at that point has a +hash. But nothing references it — no leaf value, no record field, no API — and it excludes slots +0..63, which live in the account zone. + +Behavioural consequences: + +- Proving a slot is a root-to-leaf walk of the global trie, per slot: + `Storage` is `tree.leaf(storageKey(addr, slot))` (`PBinWitnessState.Storage`, + `pbin_witness_state.go`). There is no per-account root to prove first and descend from. +- Deleting an account is deleting two key-space regions, not dropping one node. `removeAccount` + (`pbin_update_stream.go`) emits a drop at the account's header stem and another at its + storage prefix once the walk reaches that zone, because nothing enumerates the slots an account + holds. Its code-zone leaves are content-addressed and stay: another account may run the same + bytecode. This is where the engine parts from `binarize(post_state)` — the reference suite drops + the chunks when the removed account was the sole holder, and the engine keeps them. EIP-6780 + bounds the gap to states the chain cannot reach: on-chain, an account is only deleted with its + code in the transaction that created it, and such an account's chunks were never inserted (a + create-and-destroy merges to a bare deletion). Under the MPT, self-destruct drops one storage + root; here there is no such node. +- Zero and absent are the same state, so a write of 32 zero bytes removes the leaf rather than + storing zeros (`state_write`, eip:"Zero values and deletion"), and the fold collapses whatever + subtree that empties (§5.4). +- A subtree drop resets one cell, and nothing unfolds what was beneath it, so no fold reaches the + branch records stored there. `dropSubtreeRecords` (`pbin_patricia_hashed.go`) walks them from the + dropped cell's record down and deletes each one; without it the commitment domain would keep a + row per internal node of every removed account, which pruning does not collect because it goes by + step rather than by reachability. The rows a *collapsing* fold leaves behind are deleted + separately (`deleteRowRecord`). A decoded witness carries no node the proof paths did not need, + so a drop against one sweeps nothing (`pbinDerivedContext`). +- The one persisted root record, under key `0x08`, is the root cell of the whole trie — one per + trie, never per account. + +## 12. Reconstruction check + +Rebuilding the §5.1 tree by hand from the stored records **plus the leaf values read from the state +domains**, using only §4's two preimage shapes, reproduces the engine's `Process()` root. The +records alone are not enough input: only the code chunk carries its 32 bytes in the record +(LEAF_VALUE, §6), while an address-bearing leaf names a plain key and nothing else, so BASIC_DATA's +fields, CODE_HASH and the two storage values come from outside. Both storage values are elided +below: `0000…0005` is slot 5 holding 5, and `0000…002c` is slot 300 holding 0x2c (44) — not the slot +number 300 = 0x12c, which would give `L_slot300 = 120daed1…` and `root = 78497b28…` instead. + +``` +L_basic = H(0x00 || 0012b9…7a00 || 0000000000000006 0000000000000003 …03e8) + = 5dbe9906fc51df4ac846a8fe44ed92a3ec310d2edaeeea605289a290f6b38eba +L_code = H(0x00 || 0012b9…7a01 || 1d6423ed…7696574d) + = 970021c05f854ea9f1b9dd97d180ae62d0d2b9bb4acc23869cc5879919434ef8 +N271 = H(0x01 || 0005 || 00 || L_basic || L_code) + = de50844a66c2a773d715492d679fee88416467c0c5a7802a6b033199b357b0a4 [265-bit rec, cell 0] +L_slot5 = H(0x00 || 0012b9…7a45 || 0000…0005) + = a3fe2808a326a445d72d6488cf48f5a73fa6e9eb552e7e4b224785b8a0208305 +N265 = H(0x01 || 0101 || 12b9c2d7…61d53e7a 00 || N271 || L_slot5) + = ac8c75fc4b6f6e25d0831229dd10d3ad353c56dc573141f6f7e62707a5076b5d [7-bit rec, cell 0] +L_chunk0 = H(0x00 || 01073be8…9f969b5a 00 || 0060aabb000102 0000…00) + = c2b8ca4b597abfe8f13fa11cebfcf945d417addef7841108b1064abe064c50e0 +N7 = H(0x01 || 0006 || 00 || N265 || L_chunk0) + = c9aca54ec7a6c2fe06fc1cee22bd609b559f1c16217ce2ea48793349b8d61be5 [0-bit rec, cell 0] +L_slot300= H(0x00 || ff12b9…d2fe2d42 2c || 0000…002c) + = 8cfca105b43b269e0b12a1fcd0649a8b193381be942582566dc573ab8749fa49 +root = H(0x01 || 0000 || N7 || L_slot300) + = 658b62aba5ac2933e86f1100cce5084bdb35b32d797b05d247abf27a2018c064 [key 08] +``` + +`0x0101` is `u16(257)` — the branch preimage's fixed-width count, where the same 257-bit prefix is +spelled `8102` as a uvarint inside the record. Every intermediate hash equals the bytes stored in +the corresponding record. + +`L_chunk0` is where content addressing shows in the arithmetic: the chunk's key names the code hash, +not the account, so an identical contract at any other address produces this same leaf and the same +`N7` input. diff --git a/execution/commitment/commitment.go b/execution/commitment/commitment.go index c0a3fe73f91..f7141e5c9e2 100644 --- a/execution/commitment/commitment.go +++ b/execution/commitment/commitment.go @@ -1727,6 +1727,7 @@ func (t *Updates) TouchPlainKeyDirect(key string, update *Update) { } if update.Flags&CodeUpdate != 0 { existing.update.CodeHash = update.CodeHash + existing.update.CodeSize = update.CodeSize existing.update.Flags |= CodeUpdate } if update.Flags&StorageUpdate != 0 { @@ -2351,6 +2352,9 @@ func (u *Update) Decode(buf []byte, pos int) (int, error) { return 0, errors.New("decode Update: storage pos overflow") } pos += n + if l > uint64(len(u.Storage)) { + return 0, errors.New("decode Update: storage len out of range") + } if len(buf) < pos+int(l) { return 0, errors.New("decode Update: buffer too small for storage") } diff --git a/execution/commitment/commitment_test.go b/execution/commitment/commitment_test.go index 21818de34ef..00e2bdb814f 100644 --- a/execution/commitment/commitment_test.go +++ b/execution/commitment/commitment_test.go @@ -30,6 +30,7 @@ import ( "github.com/stretchr/testify/require" "github.com/erigontech/erigon/common" + "github.com/erigontech/erigon/common/length" "github.com/erigontech/erigon/db/kv" ) @@ -284,6 +285,23 @@ func TestHashSort_WaitBufferFreeErrorKeepsArenaInvariant(t *testing.T) { }) } +// TestUpdateDecodeRefusesOversizedStorage: the storage length is a varint on the +// wire but an int8 in the struct, so a value past the field's own width has to +// be refused at the bound check rather than wrap negative. +func TestUpdateDecodeRefusesOversizedStorage(t *testing.T) { + t.Parallel() + + for _, storageLen := range []uint64{uint64(length.Hash) + 1, 200} { + buf := []byte{byte(StorageUpdate)} + buf = binary.AppendUvarint(buf, storageLen) + buf = append(buf, bytes.Repeat([]byte{0xAA}, int(storageLen))...) + + var u Update + _, err := u.Decode(buf, 0) + require.Error(t, err, "storage len %d", storageLen) + } +} + // TestUpdates_ArenaAlloc verifies that sequential allocations within a ring buffer return // non-overlapping sub-slices, and that an over-capacity request falls back to an independent // allocation that leaves prior sub-slices intact. diff --git a/execution/commitment/commitmentdb/commitment_context.go b/execution/commitment/commitmentdb/commitment_context.go index 66d89961294..12b9fb22155 100644 --- a/execution/commitment/commitmentdb/commitment_context.go +++ b/execution/commitment/commitmentdb/commitment_context.go @@ -342,25 +342,60 @@ func (sdc *SharedDomainsCommitmentContext) TouchHashedKey(hashedKey []byte) { sdc.updates.TouchHashedKey(hashedKey) } +// witnessTrie is the capture seam: each engine walks its own tree and returns the +// nodes it hashed. Both variants implement it, so the capture names no concrete trie. +type witnessTrie interface { + Witnesses(ctx context.Context, updates *commitment.Updates, produceExclusionProofs bool, logPrefix string) (nodes [][]byte, provedKeys [][]byte, rootHash []byte, err error) +} + +var ( + _ witnessTrie = (*commitment.HexPatriciaHashed)(nil) + _ witnessTrie = (*commitment.PBinPatriciaHashed)(nil) +) + +// witnessBlockTrie is the seam for a trie whose key set depends on what the +// block did. Only the binary trie commits code, so only it implements this. +type witnessBlockTrie interface { + SetWitnessBlock(b commitment.PBinWitnessBlock) +} + +// SetWitnessBlock hands the next capture what the parent state it walks cannot +// say about the block. See commitment.PBinWitnessBlock. +func (sdc *SharedDomainsCommitmentContext) SetWitnessBlock(b commitment.PBinWitnessBlock) { + if trie, ok := sdc.Trie().(witnessBlockTrie); ok { + trie.SetWitnessBlock(b) + } +} + // witnessCapture runs the on-the-fly fold and returns the captured superset node // set (root first), the fold's hashed keys, and the root hash. func (sdc *SharedDomainsCommitmentContext) witnessCapture(ctx context.Context, produceExclusionProofs bool, logPrefix string) (nodes [][]byte, provedKeys [][]byte, rootHash []byte, err error) { - hexPatriciaHashed, ok := sdc.Trie().(*commitment.HexPatriciaHashed) + defer sdc.SetWitnessBlock(commitment.PBinWitnessBlock{}) // Witnesses clears it too, but only once it runs + + capturer, ok := sdc.Trie().(witnessTrie) if !ok { - return nil, nil, nil, errors.New("shared domains commitment context doesn't have HexPatriciaHashed") + return nil, nil, nil, fmt.Errorf("commitment trie %T captures no witness", sdc.Trie()) } - return hexPatriciaHashed.Witnesses(ctx, sdc.updates, produceExclusionProofs, logPrefix) + return capturer.Witnesses(ctx, sdc.updates, produceExclusionProofs, logPrefix) } // WitnessNodes builds the lean execution-witness node set: it prunes the captured -// superset to the proof paths of the fold's keys, returning the RLP node bytes -// (root first) and the root hash. This is the strict-verifier (reth) form. +// superset to the proof paths of the fold's keys, returning the node bytes (root +// first) and the root hash. This is the strict-verifier (reth) form. +// +// Each variant prunes with its own walker: the hex one is MPT-shaped and cannot +// read a bin preimage. func (sdc *SharedDomainsCommitmentContext) WitnessNodes(ctx context.Context, produceExclusionProofs bool, logPrefix string) (nodes [][]byte, rootHash []byte, err error) { full, provedKeys, rootHash, err := sdc.witnessCapture(ctx, produceExclusionProofs, logPrefix) if err != nil { return nil, nil, err } - lean, err := trie.WitnessNodesForKeysFromNodes(full, provedKeys) + var lean [][]byte + if sdc.variant == commitment.VariantBinPatriciaTrie { + lean, err = commitment.PBinWitnessNodesForKeys(full, rootHash, provedKeys) + } else { + lean, err = trie.WitnessNodesForKeysFromNodes(full, provedKeys) + } if err != nil { return nil, nil, fmt.Errorf("prune witness nodes: %w", err) } diff --git a/execution/commitment/commitmentdb/pbin_witness_test.go b/execution/commitment/commitmentdb/pbin_witness_test.go new file mode 100644 index 00000000000..3283e3c09e0 --- /dev/null +++ b/execution/commitment/commitmentdb/pbin_witness_test.go @@ -0,0 +1,255 @@ +// Copyright 2026 The Erigon Authors +// This file is part of Erigon. +// +// Erigon is free software: you can redistribute it and/or modify +// it under the terms of the GNU Lesser General Public License as published by +// the Free Software Foundation, either version 3 of the License, or +// (at your option) any later version. +// +// Erigon is distributed in the hope that it will be useful, +// but WITHOUT ANY WARRANTY; without even the implied warranty of +// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +// GNU Lesser General Public License for more details. +// +// You should have received a copy of the GNU Lesser General Public License +// along with Erigon. If not, see . + +package commitmentdb + +import ( + "bytes" + "context" + "testing" + + "github.com/stretchr/testify/require" + + "github.com/erigontech/erigon/common/empty" + "github.com/erigontech/erigon/common/length" + "github.com/erigontech/erigon/db/kv" + "github.com/erigontech/erigon/execution/commitment" + "github.com/erigontech/erigon/execution/commitment/trie" +) + +// pbinWitnessState is the in-memory PatriciaContext both engines are driven +// over: branch records they write, plus the accounts and slots they read back. +type pbinWitnessState struct { + branches map[string][]byte + accounts map[string]*commitment.Update + storage map[string]*commitment.Update +} + +func newPBinWitnessState() *pbinWitnessState { + return &pbinWitnessState{ + branches: make(map[string][]byte), + accounts: make(map[string]*commitment.Update), + storage: make(map[string]*commitment.Update), + } +} + +func (s *pbinWitnessState) Branch(prefix []byte) ([]byte, kv.Step, error) { + return s.branches[string(prefix)], 0, nil +} + +func (s *pbinWitnessState) PutBranch(prefix, data, prevData []byte) error { + s.branches[string(prefix)] = bytes.Clone(data) + return nil +} + +func (s *pbinWitnessState) Account(plainKey []byte) (*commitment.Update, error) { + if u, ok := s.accounts[string(plainKey)]; ok { + return u, nil + } + return new(commitment.Update), nil +} + +func (s *pbinWitnessState) Storage(plainKey []byte) (*commitment.Update, error) { + if u, ok := s.storage[string(plainKey)]; ok { + return u, nil + } + return new(commitment.Update), nil +} + +// Code satisfies the seam the binary update stream type-asserts for; the corpus +// carries no code, so it is never asked for any. +func (s *pbinWitnessState) Code(plainKey []byte) ([]byte, error) { return nil, nil } + +func (s *pbinWitnessState) addAccount(addr []byte, nonce, balance uint64) []byte { + u := &commitment.Update{Flags: commitment.BalanceUpdate | commitment.NonceUpdate, Nonce: nonce} + u.Balance.SetUint64(balance) + u.CodeHash = empty.CodeHash + s.accounts[string(addr)] = u + return addr +} + +func (s *pbinWitnessState) addStorage(addr, slot []byte, val byte) []byte { + key := append(bytes.Clone(addr), slot...) + u := &commitment.Update{Flags: commitment.StorageUpdate, StorageLen: 1} + u.Storage[length.Hash-1] = val + s.storage[string(key)] = u + return key +} + +type pbinWitnessTouch struct { + domain kv.Domain + key []byte +} + +// pbinWitnessDBCorpus spans two accounts and their slots, so a witness over it +// captures branch nodes rather than a bare root. +func pbinWitnessDBCorpus(state *pbinWitnessState) []pbinWitnessTouch { + var touches []pbinWitnessTouch + for i := byte(1); i <= 4; i++ { + addr := bytes.Repeat([]byte{i}, length.Addr) + touches = append(touches, pbinWitnessTouch{kv.AccountsDomain, state.addAccount(addr, uint64(i), uint64(i)*1000)}) + for _, slot := range []byte{0, 7, 64} { + key := bytes.Repeat([]byte{slot}, length.Hash) + touches = append(touches, pbinWitnessTouch{kv.StorageDomain, state.addStorage(addr, key, i)}) + } + } + return touches +} + +// pbinWitnessTrieCtx wires a fresh engine of the given variant over state. The +// witness pass runs on its own engine so it starts from the stored records +// rather than from a folded one left behind by the build. +func pbinWitnessTrieCtx(t *testing.T, variant commitment.TrieVariant, state *pbinWitnessState) *SharedDomainsCommitmentContext { + t.Helper() + sdc := pbinStateTestCtx(t, variant) + sdc.patriciaTrie.ResetContext(state) + return sdc +} + +func pbinWitnessTouchAll(sdc *SharedDomainsCommitmentContext, touches []pbinWitnessTouch) { + for _, touch := range touches { + sdc.TouchKey(touch.domain, string(touch.key), nil) + } +} + +func pbinWitnessCommit(t *testing.T, variant commitment.TrieVariant, state *pbinWitnessState, touches []pbinWitnessTouch) []byte { + t.Helper() + sdc := pbinWitnessTrieCtx(t, variant, state) + pbinWitnessTouchAll(sdc, touches) + root, err := sdc.patriciaTrie.Process(t.Context(), sdc.updates, "test", nil, commitment.WarmupConfig{}) + require.NoError(t, err) + return bytes.Clone(root) +} + +// pbinWitnessCapture builds the corpus under variant and then captures a witness +// over the same touches, returning the committed root alongside the capture. +func pbinWitnessCapture(t *testing.T, variant commitment.TrieVariant) (nodes, provedKeys [][]byte, root, committedRoot []byte, err error) { + t.Helper() + state := newPBinWitnessState() + touches := pbinWitnessDBCorpus(state) + committedRoot = pbinWitnessCommit(t, variant, state, touches) + + sdc := pbinWitnessTrieCtx(t, variant, state) + pbinWitnessTouchAll(sdc, touches) + nodes, provedKeys, root, err = sdc.witnessCapture(t.Context(), false, "test") + return nodes, provedKeys, root, committedRoot, err +} + +// TestPBinWitnessCaptureServesBothVariants: the capture used to type-assert the +// hex engine, so the bin variant failed before it ever walked a tree. +func TestPBinWitnessCaptureServesBothVariants(t *testing.T) { + t.Parallel() + + for _, variant := range []commitment.TrieVariant{commitment.VariantHexPatriciaTrie, commitment.VariantBinPatriciaTrie} { + t.Run(string(variant), func(t *testing.T) { + t.Parallel() + + nodes, provedKeys, root, committedRoot, err := pbinWitnessCapture(t, variant) + require.NoError(t, err) + require.Equal(t, committedRoot, root, "the capture must return the pre-state root") + require.Len(t, root, length.Hash) + require.Greater(t, len(nodes), 1, "a corpus this wide must capture more than the root node") + require.NotEmpty(t, provedKeys) + }) + } +} + +// TestPBinWitnessCaptureHexUnchanged pins the hex capture against the engine +// called directly, so the interface dispatch cannot alter what hex returns. +func TestPBinWitnessCaptureHexUnchanged(t *testing.T) { + t.Parallel() + + state := newPBinWitnessState() + touches := pbinWitnessDBCorpus(state) + committedRoot := pbinWitnessCommit(t, commitment.VariantHexPatriciaTrie, state, touches) + + viaCapture := pbinWitnessTrieCtx(t, commitment.VariantHexPatriciaTrie, state) + pbinWitnessTouchAll(viaCapture, touches) + nodes, provedKeys, root, err := viaCapture.witnessCapture(t.Context(), true, "test") + require.NoError(t, err) + + direct := pbinWitnessTrieCtx(t, commitment.VariantHexPatriciaTrie, state) + pbinWitnessTouchAll(direct, touches) + hph, ok := direct.Trie().(*commitment.HexPatriciaHashed) + require.True(t, ok) + wantNodes, wantKeys, wantRoot, err := hph.Witnesses(t.Context(), direct.updates, true, "test") + require.NoError(t, err) + + require.Equal(t, committedRoot, wantRoot) + require.Equal(t, wantRoot, root) + require.Equal(t, wantKeys, provedKeys) + require.Equal(t, wantNodes[0], nodes[0], "root node must stay first") + require.ElementsMatch(t, wantNodes, nodes) +} + +// TestPBinWitnessNodesPrunesPerVariant: the lean set is cut by the walker that +// can read the capture — the MPT one cannot follow a binary preimage. +func TestPBinWitnessNodesPrunesPerVariant(t *testing.T) { + t.Parallel() + + for _, variant := range []commitment.TrieVariant{commitment.VariantHexPatriciaTrie, commitment.VariantBinPatriciaTrie} { + t.Run(string(variant), func(t *testing.T) { + t.Parallel() + + state := newPBinWitnessState() + touches := pbinWitnessDBCorpus(state) + committedRoot := pbinWitnessCommit(t, variant, state, touches) + + capture := pbinWitnessTrieCtx(t, variant, state) + pbinWitnessTouchAll(capture, touches) + full, provedKeys, root, err := capture.witnessCapture(t.Context(), false, "test") + require.NoError(t, err) + + sdc := pbinWitnessTrieCtx(t, variant, state) + pbinWitnessTouchAll(sdc, touches) + lean, rootHash, err := sdc.WitnessNodes(t.Context(), false, "test") + require.NoError(t, err) + require.Equal(t, committedRoot, rootHash) + require.NotEmpty(t, lean) + + want := trie.WitnessNodesForKeysFromNodes + if variant == commitment.VariantBinPatriciaTrie { + want = func(nodes, keys [][]byte) ([][]byte, error) { + return commitment.PBinWitnessNodesForKeys(nodes, root, keys) + } + } + wantNodes, err := want(full, provedKeys) + require.NoError(t, err) + require.Equal(t, wantNodes[0], lean[0], "root node must stay first") + require.ElementsMatch(t, wantNodes, lean) + }) + } +} + +// pbinWitnessCaptureLessTrie is a Trie that captures no witness, standing in for +// the parallel variants the capture cannot serve. +type pbinWitnessCaptureLessTrie struct{ commitment.Trie } + +func (pbinWitnessCaptureLessTrie) Release() {} + +// TestPBinWitnessCaptureRejectsUnknownTrie: falling through to a nil capturer +// would panic instead of naming the trie that cannot serve the request. +func TestPBinWitnessCaptureRejectsUnknownTrie(t *testing.T) { + t.Parallel() + + sdc := pbinStateTestCtx(t, commitment.VariantHexPatriciaTrie) + sdc.patriciaTrie = pbinWitnessCaptureLessTrie{} + + _, _, _, err := sdc.witnessCapture(context.Background(), false, "test") + require.Error(t, err) + require.Contains(t, err.Error(), "pbinWitnessCaptureLessTrie") + require.Contains(t, err.Error(), "captures no witness") +} diff --git a/execution/commitment/parallel_patricia_hashed.go b/execution/commitment/parallel_patricia_hashed.go index 8a5b02e29ae..7891f48d323 100644 --- a/execution/commitment/parallel_patricia_hashed.go +++ b/execution/commitment/parallel_patricia_hashed.go @@ -104,7 +104,10 @@ func (p *ParallelPatriciaHashed) EncodeCurrentState(buf []byte) ([]byte, error) return p.template.EncodeCurrentState(buf) } +// A restore moves the root, so a root published by an earlier Process no longer +// describes the trie and RootHash has to fall back to the template. func (p *ParallelPatriciaHashed) SetState(buf []byte) error { + p.rootHash.Store(nil) return p.template.SetState(buf) } diff --git a/execution/commitment/parallel_patricia_hashed_test.go b/execution/commitment/parallel_patricia_hashed_test.go index a260cada6b3..33e1da4f57f 100644 --- a/execution/commitment/parallel_patricia_hashed_test.go +++ b/execution/commitment/parallel_patricia_hashed_test.go @@ -126,6 +126,23 @@ func TestParallelPatriciaHashedSkeletonReset(t *testing.T) { require.NotNil(t, p.template, "Reset preserves the template") } +// A restore moves the root, so RootHash must report the restored template +// rather than a root an earlier Process published. +func TestParallelPatriciaHashedSetStateDropsPublishedRoot(t *testing.T) { + p := NewParallelPatriciaHashed(nil, length.Addr, DefaultTrieConfig()) + stashed := []byte{0xde, 0xad} + p.rootHash.Store(&stashed) + + require.NoError(t, p.SetState(nil)) + assert.Nil(t, p.rootHash.Load(), "SetState clears the published rootHash") + + restored, err := p.RootHash() + require.NoError(t, err) + templateRoot, err := p.template.RootHash() + require.NoError(t, err) + assert.Equal(t, templateRoot, restored) +} + // Every checkout must be config-correct whether it hit the shared pool or // constructed fresh — that fungibility is what lets workers cross instances. func TestWorkerCheckoutAppliesConfig(t *testing.T) { diff --git a/execution/commitment/pbin_adversarial_test.go b/execution/commitment/pbin_adversarial_test.go new file mode 100644 index 00000000000..b06095861c7 --- /dev/null +++ b/execution/commitment/pbin_adversarial_test.go @@ -0,0 +1,129 @@ +// Copyright 2026 The Erigon Authors +// This file is part of Erigon. +// +// Erigon is free software: you can redistribute it and/or modify +// it under the terms of the GNU Lesser General Public License as published by +// the Free Software Foundation, either version 3 of the License, or +// (at your option) any later version. +// +// Erigon is distributed in the hope that it will be useful, +// but WITHOUT ANY WARRANTY; without even the implied warranty of +// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +// GNU Lesser General Public License for more details. +// +// You should have received a copy of the GNU Lesser General Public License +// along with Erigon. If not, see . + +package commitment + +import ( + "bytes" + "testing" + + keccak "github.com/erigontech/fastkeccak" + "github.com/stretchr/testify/require" + + "github.com/erigontech/erigon/common/empty" +) + +// Intra-batch sequences and the group-boundary shape the vendored corpus does +// not reach. No vector pins them, so the canonical-rebuild oracle is the +// reference throughout. A key touched twice in one corpus is one batch touching +// it twice: state and oracle both keep the last write. + +func pbinTestIndicator(fill byte) []byte { + return append([]byte{0xEF, 0x01, 0x00}, bytes.Repeat([]byte{fill}, 20)...) +} + +func TestPBinDelegationSetAndClearedInOneBatch(t *testing.T) { + t.Parallel() + + addr := pbinOracleAddr(101) + indicator := pbinTestIndicator(0x33) + corpus := new(pbinTestCorpus). + accountWithCodeBytes(addr, 1, 10, indicator). + accountWithCodeBytes(addr, 2, 10, nil) + _, root := corpus.process(t) + + cleared := new(pbinTestCorpus).account(addr, 2, 10, empty.CodeHash) + require.Equal(t, cleared.oracleRoot(t), root, + "a delegation set and cleared inside one batch ends at the empty-code CODE_HASH leaf") + require.Equal(t, corpus.oracleRoot(t), root) + + delegation := pbinEncodeDelegation(indicator) + leftBehind := append(cleared.entries(t), + pbinOracleEntry{key: pbinTreeKeyAccount(addr, pbinDelegationLeafKey), value: delegation[:]}) + wrong := pbinOracleRoot(leftBehind) + require.NotEqual(t, wrong[:], root, "the mid-batch indicator must not survive the clear") +} + +func TestPBinDelegationRepointedInOneBatch(t *testing.T) { + t.Parallel() + + addr := pbinOracleAddr(102) + prior, mid, final := pbinTestIndicator(0x44), pbinTestIndicator(0x55), pbinTestIndicator(0x66) + stored := new(pbinTestCorpus).accountWithCodeBytes(addr, 1, 10, prior) + repoint := new(pbinTestCorpus). + accountWithCodeBytes(addr, 2, 10, mid). + accountWithCodeBytes(addr, 3, 10, final) + _, _, root := pbinTestBatches(t, stored, repoint) + + want := new(pbinTestCorpus).accountWithCodeBytes(addr, 3, 10, final) + require.Equal(t, want.oracleRoot(t), root, + "two authorizations in one batch leave one delegation leaf holding the last target") + + stale := new(pbinTestCorpus).accountWithCodeBytes(addr, 3, 10, mid) + require.NotEqual(t, stale.oracleRoot(t), root, "the earlier target must not survive the repoint") + + asCode := new(pbinTestCorpus).accountWithCode(addr, 3, 10, keccak.Sum256(final), uint64(len(final))) + require.NotEqual(t, asCode.oracleRoot(t), root, "no code-hash leaf may appear for a delegated account") +} + +func TestPBinZeroChunkAloneInItsGroup(t *testing.T) { + t.Parallel() + + addr := pbinOracleAddr(103) + code := append(pbinTestCode(pbinStemSubtreeWidth*pbinChunkDataLen), make([]byte, pbinChunkDataLen)...) + chunks := pbinChunkifyCode(code) + require.Len(t, chunks, pbinStemSubtreeWidth+1) + require.Equal(t, [pbinValueLength]byte{}, chunks[pbinStemSubtreeWidth], + "the sole chunk of group 1 must be all-zero, PUSHDATA count included") + + corpus := new(pbinTestCorpus).accountWithCodeBytes(addr, 1, 5, code) + _, root := corpus.process(t) + require.Equal(t, corpus.oracleRoot(t), root, + "a zero chunk alone in its tree_index group leaves the group with no leaf at all") + + withLeaf := append(corpus.entries(t), pbinOracleEntry{ + key: pbinTreeKeyCodeChunk(keccak.Sum256(code), pbinStemSubtreeWidth), + value: make([]byte, pbinValueLength), + }) + wrong := pbinOracleRoot(withLeaf) + require.NotEqual(t, wrong[:], root, "materializing the zero chunk as a leaf must change the root") +} + +func TestPBinSharedCodeOutlivesOneHolder(t *testing.T) { + t.Parallel() + + holder, doomed := pbinOracleAddr(104), pbinOracleAddr(105) + code := pbinTestCode(31 * 3) + both := new(pbinTestCorpus). + accountWithCodeBytes(holder, 1, 5, code). + accountWithCodeBytes(doomed, 2, 7, code) + + pph, ms := pbinTestEngine(t) + both.applyTo(t, ms) + pbinTestProcess(t, pph, both.plainKeys, both.updates) + + removal := [][]byte{doomed} + require.NoError(t, ms.applyPlainUpdates(removal, []Update{{Flags: DeleteUpdate}})) + pph.Reset() + root := pbinTestProcess(t, pph, removal, []Update{{Flags: DeleteUpdate}}) + + survivor := new(pbinTestCorpus).accountWithCodeBytes(holder, 1, 5, code) + require.Equal(t, survivor.oracleRoot(t), root, + "deleting one holder leaves the shared chunk set with the other") + + noChunks := new(pbinTestCorpus).accountWithCode(holder, 1, 5, keccak.Sum256(code), uint64(len(code))) + require.NotEqual(t, noChunks.oracleRoot(t), root, "the survivor's chunks must not go with the removed holder") +} diff --git a/execution/commitment/pbin_code.go b/execution/commitment/pbin_code.go index 230e9db5d5b..ae49457a8f9 100644 --- a/execution/commitment/pbin_code.go +++ b/execution/commitment/pbin_code.go @@ -18,22 +18,18 @@ package commitment import "fmt" -// EIP-8297's code embedding (eip:349-397). +// EIP-8297's code embedding (eip:"Code"). const ( // pbinChunkDataLen is how much code one chunk holds; byte 0 of the 32-byte // value carries the PUSHDATA count instead. pbinChunkDataLen = pbinValueLength - 1 - // pbinHeaderCodeChunks are the chunks the account header holds, at sub-indices - // CODE_OFFSET..255. Higher chunks live in the code zone. - pbinHeaderCodeChunks = pbinStemSubtreeWidth - pbinCodeOffset - pbinPushOffset = 95 pbinPush1 = pbinPushOffset + 1 pbinPush32 = pbinPushOffset + 32 ) -// pbinChunkifyCode splits code into the tree's chunk values (eip:374-397). The +// pbinChunkifyCode splits code into the tree's chunk values (eip:"Code"). The // PUSHDATA scan runs over the whole code, so residual PUSHDATA carries across // chunk boundaries. Padding to a multiple of 31 happens before the scan, which // is what makes a PUSH whose data runs off the end count against the padded tail. diff --git a/execution/commitment/pbin_code_test.go b/execution/commitment/pbin_code_test.go index 9f08616a0c9..454f70a70e7 100644 --- a/execution/commitment/pbin_code_test.go +++ b/execution/commitment/pbin_code_test.go @@ -22,13 +22,14 @@ import ( "fmt" "testing" + keccak "github.com/erigontech/fastkeccak" "github.com/stretchr/testify/require" "github.com/erigontech/erigon/common/empty" ) // TestPBinChunkifyCodeVectors checks chunking against the reference's own -// chunkings of chunk_code (eip:374-397). +// chunkings of chunk_code (eip:"Code"). func TestPBinChunkifyCodeVectors(t *testing.T) { t.Parallel() v := pbinLoadSpecVectors(t) @@ -85,9 +86,9 @@ func TestPBinChunkifyCodeEmpty(t *testing.T) { require.Empty(t, pbinChunkifyCode([]byte{})) } -// TestPBinChunkifyCodeChunkCount pins the sizing the header/overflow split rests -// on: chunks are ceil(len/31), and MaxCodeSize needs more of them than the 128 -// the account header holds. +// TestPBinChunkifyCodeChunkCount pins the sizing the code grouping rests on: +// chunks are ceil(len/31), and MaxCodeSize needs more of them than the 256 one +// code group holds. func TestPBinChunkifyCodeChunkCount(t *testing.T) { t.Parallel() @@ -95,8 +96,8 @@ func TestPBinChunkifyCodeChunkCount(t *testing.T) { {size: 1, chunks: 1}, {size: 31, chunks: 1}, {size: 32, chunks: 2}, - {size: pbinHeaderCodeChunks * pbinChunkDataLen, chunks: pbinHeaderCodeChunks}, - {size: pbinHeaderCodeChunks*pbinChunkDataLen + 1, chunks: pbinHeaderCodeChunks + 1}, + {size: pbinStemSubtreeWidth * pbinChunkDataLen, chunks: pbinStemSubtreeWidth}, + {size: pbinStemSubtreeWidth*pbinChunkDataLen + 1, chunks: pbinStemSubtreeWidth + 1}, {size: 24576, chunks: 793}, } { require.Len(t, pbinChunkifyCode(make([]byte, tc.size)), tc.chunks, "code of %d bytes", tc.size) @@ -114,9 +115,9 @@ func pbinTestCode(n int) []byte { return code } -// TestPBinEngineEmitsHeaderCodeChunks covers the first half of code in the tree: -// chunks reaching the reference leaf set at header sub-indices CODE_OFFSET and up. -func TestPBinEngineEmitsHeaderCodeChunks(t *testing.T) { +// TestPBinEngineEmitsCodeChunks covers code in the tree: chunks reaching the +// reference leaf set in the content-addressed code zone. +func TestPBinEngineEmitsCodeChunks(t *testing.T) { t.Parallel() addr := pbinOracleAddr(11) @@ -130,10 +131,9 @@ func TestPBinEngineEmitsHeaderCodeChunks(t *testing.T) { require.Equal(t, corpus.oracleRoot(t), root) } -// TestPBinCodeChunksFollowHeaderSlots pins the emit order inside a stem: chunks -// sit at the top sub-indices, so emitting them at the account's own visit -// descends past header storage slots the stream has not delivered yet, and the -// fold that comes back for them rewrites a record it already wrote. +// TestPBinCodeChunksFollowHeaderSlots composes one account's code, header slots +// and overflow storage: its leaves span all three zones, and the chunks must +// wait for the walk to leave the account zone. func TestPBinCodeChunksFollowHeaderSlots(t *testing.T) { t.Parallel() @@ -164,57 +164,50 @@ func TestPBinVisitOrderIsMonotonic(t *testing.T) { } // TestPBinCodeChunksSurviveAsRecordSiblings pins that a chunk leaf carries its -// own value: no state domain holds a chunk, so an untouched chunk sibling of a -// touched one has to hash from the branch record. +// own value: no state domain holds a chunk, so when a later batch writes into +// the code zone next to an earlier contract's chunks, those chunks have to hash +// from the branch records alone. func TestPBinCodeChunksSurviveAsRecordSiblings(t *testing.T) { t.Parallel() - addr := pbinOracleAddr(14) - // 62 bytes is two chunks; the redeploy to 31 touches only chunk 0. - deploy := new(pbinTestCorpus).accountWithCodeBytes(addr, 1, 10, pbinTestCode(62)) - stale := pbinChunkifyCode(pbinTestCode(62))[1] - redeploy := new(pbinTestCorpus).accountWithCodeBytes(addr, 2, 10, pbinTestCode(31)) + first := new(pbinTestCorpus).accountWithCodeBytes(pbinOracleAddr(14), 1, 10, pbinTestCode(62)) + second := new(pbinTestCorpus).accountWithCodeBytes(pbinOracleAddr(24), 1, 20, pbinTestCode(93)) - _, _, root := pbinTestBatches(t, deploy, redeploy) - - want := append(redeploy.entries(t), pbinOracleEntry{ - key: pbinTreeKeyCodeChunk(addr, 1), - value: stale[:], - }) - wantRoot := pbinOracleRoot(want) - require.Equal(t, wantRoot[:], root, "the untouched chunk keeps the value the record holds") + _, _, root := pbinTestBatches(t, first, second) + require.Equal(t, pbinTestUnion(first, second).oracleRoot(t), root) } -// TestPBinShorteningRedeployKeepsStaleChunks pins the residue a shorter redeploy -// leaves: EIP-8297 has no removal, so a forward run commits the chunks above the -// new length while a recompute from the state domains cannot know they exist. -// Both roots are internally consistent, which is what makes recompute-from-domains -// invalid as an oracle for a code-bearing account. -func TestPBinShorteningRedeployKeepsStaleChunks(t *testing.T) { +// TestPBinRedeployKeepsOldCodeChunks pins the residue a redeploy leaves: chunk +// keys derive from the code hash, so new code names a disjoint leaf set and +// EIP-8297 removes nothing here. A recompute from the state domains cannot know +// the old chunks exist, which is what makes it invalid as an oracle for a +// code-bearing account. +func TestPBinRedeployKeepsOldCodeChunks(t *testing.T) { t.Parallel() for _, tc := range []struct{ before, after int }{ - {before: 62, after: 31}, // 2 chunks down to 1: the residue is a leaf sibling - {before: 200, after: 62}, // 7 down to 2: the residue is a whole subtree + {before: 62, after: 31}, + {before: 200, after: 62}, + {before: 31, after: 62}, // growth keeps the residue too: the old hash names other leaves } { - t.Run(fmt.Sprintf("%d bytes down to %d", tc.before, tc.after), func(t *testing.T) { + t.Run(fmt.Sprintf("%d bytes to %d", tc.before, tc.after), func(t *testing.T) { t.Parallel() addr := pbinOracleAddr(15) - long, short := pbinTestCode(tc.before), pbinTestCode(tc.after) - deploy := new(pbinTestCorpus).accountWithCodeBytes(addr, 1, 10, long) - redeploy := new(pbinTestCorpus).accountWithCodeBytes(addr, 2, 10, short) + old, next := pbinTestCode(tc.before), pbinTestCode(tc.after) + deploy := new(pbinTestCorpus).accountWithCodeBytes(addr, 1, 10, old) + redeploy := new(pbinTestCorpus).accountWithCodeBytes(addr, 2, 10, next) _, _, forward := pbinTestBatches(t, deploy, redeploy) - _, rebuilt := new(pbinTestCorpus).accountWithCodeBytes(addr, 2, 10, short).process(t) + _, rebuilt := new(pbinTestCorpus).accountWithCodeBytes(addr, 2, 10, next).process(t) require.NotEqual(t, rebuilt, forward, "a rebuild from state cannot reproduce the stale chunks the forward run kept") want := redeploy.entries(t) - oldChunks := pbinChunkifyCode(long) - for i := len(pbinChunkifyCode(short)); i < len(oldChunks); i++ { - want = append(want, pbinOracleEntry{key: pbinTreeKeyCodeChunk(addr, i), value: oldChunks[i][:]}) + oldHash := keccak.Sum256(old) + for i, chunk := range pbinChunkifyCode(old) { + want = append(want, pbinOracleEntry{key: pbinTreeKeyCodeChunk(oldHash, i), value: chunk[:]}) } wantRoot := pbinOracleRoot(want) require.Equal(t, wantRoot[:], forward) @@ -236,8 +229,9 @@ func TestPBinClearedCodeKeepsChunks(t *testing.T) { _, _, forward := pbinTestBatches(t, deploy, cleared) want := cleared.entries(t) + desigHash := keccak.Sum256(designator) for i, chunk := range pbinChunkifyCode(designator) { - want = append(want, pbinOracleEntry{key: pbinTreeKeyCodeChunk(addr, i), value: chunk[:]}) + want = append(want, pbinOracleEntry{key: pbinTreeKeyCodeChunk(desigHash, i), value: chunk[:]}) } wantRoot := pbinOracleRoot(want) require.Equal(t, wantRoot[:], forward, "clearing code leaves its chunks in the tree") @@ -246,31 +240,45 @@ func TestPBinClearedCodeKeepsChunks(t *testing.T) { require.NotEqual(t, rebuilt, forward, "the state a rebuild reads no longer names the chunks") } -// TestPBinGrowingRedeployReplacesChunks is the case a rebuild does reproduce: -// growing code overwrites every chunk it had and adds the rest, leaving no -// residue for the forward tree and a rebuild from state to disagree over. -func TestPBinGrowingRedeployReplacesChunks(t *testing.T) { +// TestPBinZeroChunkEmitsNoLeaf pins the absence rule for chunks: a chunk is +// absent only when its whole 32-byte value is zero — 31 zero code bytes and a +// zero PUSHDATA count. The same zero bytes continuing an earlier chunk's PUSH +// keep their leaf, and code_size delimits the code either way. +func TestPBinZeroChunkEmitsNoLeaf(t *testing.T) { t.Parallel() - for _, tc := range []struct{ before, after int }{ - {before: 31, after: 62}, // 1 chunk to 2, both in the header - {before: 62, after: pbinHeaderCodeChunks*pbinChunkDataLen + 62}, // header-only to header plus code zone - } { - t.Run(fmt.Sprintf("%d bytes up to %d", tc.before, tc.after), func(t *testing.T) { - t.Parallel() + opcodes := pbinTestCode(31) // every byte below PUSH1, none zero - addr := pbinOracleAddr(20) - short, long := pbinTestCode(tc.before), pbinTestCode(tc.after) - deploy := new(pbinTestCorpus).accountWithCodeBytes(addr, 1, 10, short) - redeploy := new(pbinTestCorpus).accountWithCodeBytes(addr, 2, 10, long) + t.Run("zero tail chunk is absent", func(t *testing.T) { + t.Parallel() - _, _, forward := pbinTestBatches(t, deploy, redeploy) + code := append(bytes.Clone(opcodes), make([]byte, 31)...) + chunks := pbinChunkifyCode(code) + require.Len(t, chunks, 2) + require.Equal(t, [pbinValueLength]byte{}, chunks[1]) - _, rebuilt := new(pbinTestCorpus).accountWithCodeBytes(addr, 2, 10, long).process(t) - require.Equal(t, rebuilt, forward, "growth leaves no chunk of the old code behind") - require.Equal(t, redeploy.oracleRoot(t), forward) - }) - } + corpus := new(pbinTestCorpus).accountWithCodeBytes(pbinOracleAddr(25), 1, 10, code) + require.Equal(t, 2+1, corpus.leafCount(t), "the zero chunk contributes no leaf") + + _, root := corpus.process(t) + require.Equal(t, corpus.oracleRoot(t), root) + }) + + t.Run("pushdata continuation keeps the leaf", func(t *testing.T) { + t.Parallel() + + code := append(bytes.Clone(opcodes[:30]), byte(pbinPushOffset+31)) + code = append(code, make([]byte, 31)...) + chunks := pbinChunkifyCode(code) + require.Len(t, chunks, 2) + require.EqualValues(t, 31, chunks[1][0], "byte 0 counts the PUSH31 data") + + corpus := new(pbinTestCorpus).accountWithCodeBytes(pbinOracleAddr(26), 1, 10, code) + require.Equal(t, 2+2, corpus.leafCount(t), "the continuation chunk keeps its leaf") + + _, root := corpus.process(t) + require.Equal(t, corpus.oracleRoot(t), root) + }) } func TestPBinCodelessContextRefusesCodeBearingAccount(t *testing.T) { @@ -351,17 +359,16 @@ func TestPBinLeafValueRoutesByZone(t *testing.T) { require.NoError(t, err) require.Equal(t, chunk[:], got[:]) - // Inside the account zone, sub-indices at CODE_OFFSET and above are chunks, - // not storage. - addr := pbinOracleAddr(19) - got, err = pbinLeafValue(pbinTreeKeyCodeChunk(addr, 0), &u) + // Inside the account zone, sub-indices past the header storage span are + // reserved and carry their value verbatim, not as storage. + got, err = pbinLeafValue(pbinTreeKey(pbinAccountZone, make([]byte, 32), pbinHeaderStorageOffset+pbinHeaderStorageSlots), &u) require.NoError(t, err) require.Equal(t, chunk[:], got[:]) // A chunk leaf holding fewer than 32 value bytes cannot be left-padded into // place the way a storage value can: byte 0 is the PUSHDATA count. short := Update{Flags: StorageUpdate, StorageLen: 4} - _, err = pbinLeafValue(pbinTreeKeyCodeChunk(addr, 1), &short) + _, err = pbinLeafValue(pbinTreeKeyCodeChunk(keccak.Sum256(pbinTestCode(62)), 1), &short) require.ErrorIs(t, err, errPBinCellHash) } diff --git a/execution/commitment/pbin_conformance_test.go b/execution/commitment/pbin_conformance_test.go new file mode 100644 index 00000000000..8f4a4ea9f94 --- /dev/null +++ b/execution/commitment/pbin_conformance_test.go @@ -0,0 +1,277 @@ +package commitment + +// The cross-client conformance vectors from ethereum/execution-specs +// (projects/binary-trie), vendored verbatim as testdata/binary_trie_vectors.json +// and regenerated there by the reference implementation, which hashes with +// BLAKE3. +// +// The four primitive sections pin the embedding piece by piece; pbt_state pins +// their composition — whole accounts to a root, which is where an embedding +// mistake actually surfaces. + +import ( + "bytes" + "context" + "encoding/hex" + "encoding/json" + "math/big" + "os" + "sort" + "strconv" + "strings" + "testing" + + "github.com/holiman/uint256" + "github.com/stretchr/testify/require" + + "github.com/erigontech/erigon/common" +) + +type pbinConformance struct { + Source string `json:"source"` + SourceCommit string `json:"source_commit"` + + TrieRoots []pbinSpecTrieVector `json:"trie_roots"` + + Embedding struct { + Address20 string `json:"address20"` + Address32 string `json:"address32"` + BasicDataKey string `json:"basic_data_key"` + CodeHashKey string `json:"code_hash_key"` + DelegationKey string `json:"delegation_key"` + StorageSlotKeys map[string]string `json:"storage_slot_keys"` + CodeChunkKeys map[string]string `json:"code_chunk_keys"` + CodeHash string `json:"code_hash"` + } `json:"embedding"` + + ChunkifyCode []struct { + Name string `json:"name"` + Code string `json:"code"` + Chunks []string `json:"chunks"` + } `json:"chunkify_code"` + + EncodeBasicData []struct { + CodeSize uint64 `json:"code_size"` + Nonce uint64 `json:"nonce"` + Balance string `json:"balance"` + Encoded string `json:"encoded"` + } `json:"encode_basic_data"` + + PBTState []struct { + Name string `json:"name"` + Accounts map[string]struct { + Nonce uint64 `json:"nonce"` + Balance string `json:"balance"` + Code string `json:"code"` + CodeHash string `json:"code_hash"` + Storage map[string]string `json:"storage"` + } `json:"accounts"` + Root string `json:"root"` + } `json:"pbt_state"` +} + +func pbinLoadConformance(t *testing.T) *pbinConformance { + t.Helper() + raw, err := os.ReadFile("testdata/binary_trie_vectors.json") + require.NoError(t, err) + v := new(pbinConformance) + require.NoError(t, json.Unmarshal(raw, v)) + require.NotEmpty(t, v.SourceCommit) + return v +} + +func pbinUnhex(t *testing.T, s string) []byte { + t.Helper() + b, err := hex.DecodeString(strings.TrimPrefix(s, "0x")) + require.NoError(t, err) + return b +} + +// pbinSlotBytes parses a slot given as a full decimal expansion, which reaches +// 2**256-1 and so cannot go through a JSON number. +func pbinSlotBytes(t *testing.T, decimal string) []byte { + t.Helper() + n, ok := new(big.Int).SetString(decimal, 10) + require.True(t, ok, "slot %q", decimal) + var slot [32]byte + n.FillBytes(slot[:]) + return slot[:] +} + +// TestPBinConformanceTrieRoots pins raw trie semantics against the oracle. The +// engine cannot take these: their keys carry synthetic zone bytes chosen to +// exercise bit-level divergence, and the engine only admits allocated zones. +// TestPBinConformancePBTState is where the engine meets the same reference. +func TestPBinConformanceTrieRoots(t *testing.T) { + for _, c := range pbinLoadConformance(t).TrieRoots { + t.Run(c.Name, func(t *testing.T) { + tree := &pbinOracleTree{} + for _, e := range c.Entries { + tree.insert(pbinUnhex(t, e.Key), pbinUnhex(t, e.Value)) + } + got := pbinOracleMerkelizeWith(tree.root, pbinBlake3Sum) + require.Equal(t, c.Root, "0x"+hex.EncodeToString(got[:])) + }) + } +} + +func TestPBinConformanceEmbedding(t *testing.T) { + e := pbinLoadConformance(t).Embedding + addr := pbinUnhex(t, e.Address20) + codeHash := common.BytesToHash(pbinUnhex(t, e.CodeHash)) + keys := pbinDigestCache{sum: pbinBlake3Hash} + + require.Equal(t, e.Address32, "0x"+hex.EncodeToString(func() []byte { + a := pbinAddr32(addr) + return a[:] + }())) + + hexKey := func(k []byte) string { return "0x" + hex.EncodeToString(k) } + require.Equal(t, e.BasicDataKey, hexKey(keys.accountKey(addr, pbinBasicDataLeafKey))) + require.Equal(t, e.CodeHashKey, hexKey(keys.accountKey(addr, pbinCodeHashLeafKey))) + require.Equal(t, e.DelegationKey, hexKey(keys.accountKey(addr, pbinDelegationLeafKey))) + + for slot, want := range e.StorageSlotKeys { + require.Equal(t, want, hexKey(keys.storageKey(addr, pbinSlotBytes(t, slot))), "slot %s", slot) + } + + for chunk, want := range e.CodeChunkKeys { + id, err := strconv.Atoi(chunk) + require.NoError(t, err) + require.Equal(t, want, hexKey(keys.codeChunkKey(codeHash, id)), "chunk %s", chunk) + } +} + +func TestPBinConformanceChunkifyCode(t *testing.T) { + for _, c := range pbinLoadConformance(t).ChunkifyCode { + t.Run(c.Name, func(t *testing.T) { + chunks := pbinChunkifyCode(pbinUnhex(t, c.Code)) + require.Len(t, chunks, len(c.Chunks)) + for i, want := range c.Chunks { + require.Equal(t, want, "0x"+hex.EncodeToString(chunks[i][:]), "chunk %d", i) + } + }) + } +} + +func TestPBinConformanceEncodeBasicData(t *testing.T) { + for _, c := range pbinLoadConformance(t).EncodeBasicData { + balance, err := uint256.FromHex(c.Balance) + require.NoError(t, err) + got, err := pbinEncodeBasicData(c.Nonce, balance, c.CodeSize) + require.NoError(t, err) + require.Equal(t, c.Encoded, "0x"+hex.EncodeToString(got[:]), + "code_size=%d nonce=%d balance=%s", c.CodeSize, c.Nonce, c.Balance) + } +} + +// TestPBinConformancePBTState rebuilds each reference state leaf by leaf and +// checks the root, through the oracle and through the engine. Two rules decide +// what is not written: a leaf whose value is 32 zero bytes is absent, and code +// length comes from code_size rather than from which chunks exist. +func TestPBinConformancePBTState(t *testing.T) { + pbinRestoreHashSuite(t) + require.NoError(t, SetPBinHashSuite(PBinHashBlake3)) + + var zero [pbinValueLength]byte + for _, c := range pbinLoadConformance(t).PBTState { + t.Run(c.Name, func(t *testing.T) { + keys := pbinDigestCache{sum: pbinBlake3Hash} + leaves := map[string][]byte{} + put := func(key []byte, value [pbinValueLength]byte) { + if value == zero { + return + } + leaves[string(key)] = value[:] + } + + for addrHex, acc := range c.Accounts { + addr := pbinUnhex(t, addrHex) + code := pbinUnhex(t, acc.Code) + codeHash := common.BytesToHash(pbinUnhex(t, acc.CodeHash)) + balance, err := uint256.FromHex(acc.Balance) + require.NoError(t, err) + + basic, err := pbinEncodeBasicData(acc.Nonce, balance, uint64(len(code))) + require.NoError(t, err) + put(keys.accountKey(addr, pbinBasicDataLeafKey), basic) + if pbinIsDelegation(code) { + put(keys.accountKey(addr, pbinDelegationLeafKey), pbinEncodeDelegation(code)) + } else { + put(keys.accountKey(addr, pbinCodeHashLeafKey), pbinCodeHashValue(codeHash)) + for i, chunk := range pbinChunkifyCode(code) { + put(keys.codeChunkKey(codeHash, i), chunk) + } + } + + for slot, value := range acc.Storage { + put(keys.storageKey(addr, pbinSlotBytes(t, slot)), + pbinEncodeStorageValue(pbinUnhex(t, value))) + } + } + + ordered := make([]string, 0, len(leaves)) + for k := range leaves { + ordered = append(ordered, k) + } + sort.Strings(ordered) + + tree := &pbinOracleTree{} + for _, k := range ordered { + tree.insert([]byte(k), leaves[k]) + } + got := pbinOracleMerkelizeWith(tree.root, pbinBlake3Sum) + require.Equal(t, c.Root, "0x"+hex.EncodeToString(got[:]), "oracle, %d leaves", len(leaves)) + + if len(leaves) == 0 { + return // the engine needs a context to load a root it never stored + } + // The engine derives chunk and header leaves itself from an account + // update, so it is driven by accounts and slots rather than by the leaf + // set above — that is the point of running both. + corpus := &pbinTestCorpus{codes: map[string][]byte{}} + for addrHex, acc := range c.Accounts { + addr := pbinUnhex(t, addrHex) + code := pbinUnhex(t, acc.Code) + balance, err := uint256.FromHex(acc.Balance) + require.NoError(t, err) + u := Update{ + Flags: NonceUpdate | BalanceUpdate | CodeUpdate, + Nonce: acc.Nonce, + CodeHash: common.BytesToHash(pbinUnhex(t, acc.CodeHash)), + CodeSize: uint64(len(code)), + } + u.Balance.Set(balance) + corpus.plainKeys = append(corpus.plainKeys, addr) + corpus.updates = append(corpus.updates, u) + corpus.codes[string(addr)] = code + + for slot, value := range acc.Storage { + trimmed := pbinTrimLeft(pbinUnhex(t, value)) + su := Update{Flags: StorageUpdate, StorageLen: int8(len(trimmed))} + copy(su.Storage[:], trimmed) + corpus.plainKeys = append(corpus.plainKeys, append(bytes.Clone(addr), pbinSlotBytes(t, slot)...)) + corpus.updates = append(corpus.updates, su) + } + } + + pph, ms := pbinTestEngine(t) + hasher := pph.setHashSuite(pbinBlake3Hash) + corpus.applyTo(t, ms) + upd := WrapKeyUpdates(t, ModeDirect, hasher, corpus.plainKeys, corpus.updates) + root, err := pph.Process(context.Background(), upd, "", nil, WarmupConfig{}) + require.NoError(t, err) + require.Equal(t, c.Root, "0x"+hex.EncodeToString(root), "engine") + }) + } +} + +// pbinTrimLeft drops leading zero bytes, the trimmed form the domain layer keeps +// a storage value in. +func pbinTrimLeft(value []byte) []byte { + i := 0 + for i < len(value) && value[i] == 0 { + i++ + } + return value[i:] +} diff --git a/execution/commitment/pbin_delegation_test.go b/execution/commitment/pbin_delegation_test.go new file mode 100644 index 00000000000..375303d571f --- /dev/null +++ b/execution/commitment/pbin_delegation_test.go @@ -0,0 +1,143 @@ +// Copyright 2026 The Erigon Authors +// This file is part of Erigon. +// +// Erigon is free software: you can redistribute it and/or modify +// it under the terms of the GNU Lesser General Public License as published by +// the Free Software Foundation, either version 3 of the License, or +// (at your option) any later version. +// +// Erigon is distributed in the hope that it will be useful, +// but WITHOUT ANY WARRANTY; without even the implied warranty of +// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +// GNU Lesser General Public License for more details. +// +// You should have received a copy of the GNU Lesser General Public License +// along with Erigon. If not, see . + +package commitment + +import ( + "bytes" + "testing" + + keccak "github.com/erigontech/fastkeccak" + "github.com/stretchr/testify/require" + + "github.com/erigontech/erigon/common/empty" +) + +// TestPBinIsDelegationClassifiesByBytes pins that classification reads the code +// bytes and nothing else: 23 bytes opening with the marker. Code whose keccak +// hash begins with the marker is still code. +func TestPBinIsDelegationClassifiesByBytes(t *testing.T) { + t.Parallel() + + marker := []byte{0xEF, 0x01, 0x00} + indicator := append(bytes.Clone(marker), bytes.Repeat([]byte{0xAB}, 20)...) + require.True(t, pbinIsDelegation(indicator)) + + hashGrindsToMarker := pbinMustHex(t, "0x0000000000000000000000000000000000000000637401") + require.Len(t, hashGrindsToMarker, pbinDelegationCodeLength) + h := keccak.Sum256(hashGrindsToMarker) + require.Equal(t, marker, h[:3], "the ground value must still hash to the marker") + require.False(t, pbinIsDelegation(hashGrindsToMarker)) + + require.False(t, pbinIsDelegation(append(bytes.Clone(marker), bytes.Repeat([]byte{0xAB}, 19)...))) + require.False(t, pbinIsDelegation(append(bytes.Clone(marker), bytes.Repeat([]byte{0xAB}, 21)...))) + require.False(t, pbinIsDelegation(marker)) + require.False(t, pbinIsDelegation(nil)) +} + +func TestPBinEncodeDelegationPadsToThirtyTwo(t *testing.T) { + t.Parallel() + + indicator := append([]byte{0xEF, 0x01, 0x00}, bytes.Repeat([]byte{0xCD}, 20)...) + v := pbinEncodeDelegation(indicator) + require.Equal(t, indicator, v[:pbinDelegationCodeLength]) + require.Equal(t, make([]byte, pbinValueLength-pbinDelegationCodeLength), v[pbinDelegationCodeLength:]) + + chunk := pbinChunkifyCode(indicator)[0] + require.NotEqual(t, chunk, v, + "an indicator is not chunk-encoded: byte 0 carries code, not a PUSHDATA count") +} + +// TestPBinDelegationLeafIsExclusive pins the header rule: an account holds +// exactly one of the CODE_HASH and DELEGATION leaves, decided by its current +// code bytes, and every write removes the other leaf. +func TestPBinDelegationLeafIsExclusive(t *testing.T) { + t.Parallel() + + indicator := append([]byte{0xEF, 0x01, 0x00}, bytes.Repeat([]byte{0x11}, 20)...) + + t.Run("fresh EOA delegates", func(t *testing.T) { + t.Parallel() + + addr := pbinOracleAddr(91) + corpus := new(pbinTestCorpus).accountWithCodeBytes(addr, 1, 10, indicator) + _, root := corpus.process(t) + + basic, err := pbinEncodeBasicData(1, &corpus.updates[0].Balance, pbinDelegationCodeLength) + require.NoError(t, err) + delegation := pbinEncodeDelegation(indicator) + want := pbinOracleRoot([]pbinOracleEntry{ + {key: pbinTreeKeyAccount(addr, pbinBasicDataLeafKey), value: basic[:]}, + {key: pbinTreeKeyAccount(addr, pbinDelegationLeafKey), value: delegation[:]}, + }) + require.Equal(t, want[:], root, "a delegated account is BASIC_DATA plus the indicator: no code-hash leaf, no chunks") + }) + + t.Run("delegation replaces contract code", func(t *testing.T) { + t.Parallel() + + addr := pbinOracleAddr(92) + code := pbinTestCode(62) + deploy := new(pbinTestCorpus).accountWithCodeBytes(addr, 1, 10, code) + delegate := new(pbinTestCorpus).accountWithCodeBytes(addr, 2, 10, indicator) + _, _, forward := pbinTestBatches(t, deploy, delegate) + + want := delegate.entries(t) + oldHash := keccak.Sum256(code) + for i, chunk := range pbinChunkifyCode(code) { + want = append(want, pbinOracleEntry{key: pbinTreeKeyCodeChunk(oldHash, i), value: chunk[:]}) + } + wantRoot := pbinOracleRoot(want) + require.Equal(t, wantRoot[:], forward, + "the code-hash leaf goes; the old chunks stay, content-addressed by the old hash") + }) + + t.Run("delegation cleared to empty code", func(t *testing.T) { + t.Parallel() + + addr := pbinOracleAddr(93) + delegate := new(pbinTestCorpus).accountWithCodeBytes(addr, 1, 10, indicator) + cleared := new(pbinTestCorpus).account(addr, 2, 10, empty.CodeHash) + _, _, forward := pbinTestBatches(t, delegate, cleared) + + require.Equal(t, cleared.oracleRoot(t), forward, + "clearing restores the empty-code CODE_HASH leaf and removes the indicator") + }) + + t.Run("two authorities one target", func(t *testing.T) { + t.Parallel() + + a, b := pbinOracleAddr(94), pbinOracleAddr(95) + corpus := new(pbinTestCorpus). + accountWithCodeBytes(a, 1, 10, indicator). + accountWithCodeBytes(b, 2, 20, indicator) + _, root := corpus.process(t) + + basicA, err := pbinEncodeBasicData(1, &corpus.updates[0].Balance, pbinDelegationCodeLength) + require.NoError(t, err) + basicB, err := pbinEncodeBasicData(2, &corpus.updates[1].Balance, pbinDelegationCodeLength) + require.NoError(t, err) + delegation := pbinEncodeDelegation(indicator) + want := pbinOracleRoot([]pbinOracleEntry{ + {key: pbinTreeKeyAccount(a, pbinBasicDataLeafKey), value: basicA[:]}, + {key: pbinTreeKeyAccount(a, pbinDelegationLeafKey), value: delegation[:]}, + {key: pbinTreeKeyAccount(b, pbinBasicDataLeafKey), value: basicB[:]}, + {key: pbinTreeKeyAccount(b, pbinDelegationLeafKey), value: delegation[:]}, + }) + require.Equal(t, want[:], root, + "each authority holds its own header leaf; the shared target adds no shared leaf") + }) +} diff --git a/execution/commitment/pbin_domainwrite_test.go b/execution/commitment/pbin_domainwrite_test.go index 5c4cc54a6f1..688755e6b06 100644 --- a/execution/commitment/pbin_domainwrite_test.go +++ b/execution/commitment/pbin_domainwrite_test.go @@ -152,6 +152,48 @@ func pbinRequirePutsMatchStore(t *testing.T, puts []pbinRecordedPut, store map[s return overwrites } +// A removed account takes every record it owned with it. The drop stops the +// unfold at the account's subtree, so no fold reaches what is below and only an +// explicit sweep reclaims it — left behind, those records are unreachable bytes +// no prune collects, and a later rebuild of the same path would report a +// previous value that is not there. +func TestPBinAccountRemovalLeavesNoRecordBehind(t *testing.T) { + t.Parallel() + + keep, gone := pbinOracleAddr(11), pbinOracleAddr(22) + stored := new(pbinTestCorpus). + account(keep, 1, 2, common.Hash{0x11}). + account(gone, 3, 4, common.Hash{0x22}) + for i := range 16 { + stored.storage(gone, pbinOracleSlot(uint64(256+i)), byte(i+1)) + } + + pph, ctx, ms := pbinTestStrictEngine(t) + stored.applyTo(t, ms) + pbinTestProcess(t, pph, stored.plainKeys, stored.updates) + + snapshot := make(map[string][]byte, len(ms.cm)) + for k, v := range ms.cm { + snapshot[k] = bytes.Clone(v) + } + ctx.puts = nil + + removal := new(pbinTestCorpus).remove(gone) + require.NoError(t, ms.applyPlainUpdates(removal.plainKeys, removal.updates)) + pph.Reset() + root := pbinTestProcess(t, pph, removal.plainKeys, removal.updates) + + survivor := new(pbinTestCorpus).account(keep, 1, 2, common.Hash{0x11}) + require.Equal(t, survivor.oracleRoot(t), root) + pbinRequirePutsMatchStore(t, ctx.puts, snapshot) + + _, rebuilt := pbinTestEngine(t) + survivor.applyTo(t, rebuilt) + pbinTestProcess(t, NewPBinPatriciaHashed(rebuilt), survivor.plainKeys, survivor.updates) + require.Equal(t, pbinLiveRecordKeys(rebuilt), pbinLiveRecordKeys(ms), + "the forward run must hold exactly the records a rebuild does") +} + // Every branch write carries the record it replaces: empty on a fresh store, the // stored bytes on a rewrite. func TestPBinProcessPutBranchCarriesRealPrev(t *testing.T) { diff --git a/execution/commitment/pbin_fuzz_test.go b/execution/commitment/pbin_fuzz_test.go index 8373f15c26b..11e4f871ed3 100644 --- a/execution/commitment/pbin_fuzz_test.go +++ b/execution/commitment/pbin_fuzz_test.go @@ -31,47 +31,70 @@ import ( // boundaries or the account/storage zone split. var pbinFuzzSlots = []uint64{0, 1, 2, 63, 64, 65, 66, 127, 128, 255, 256, 257, 258, 511, 512, 1000, 1 << 20, 1<<20 + 1} -// pbinFuzzAccountBit is the selector bit choosing an account write over a slot. -const pbinFuzzAccountBit = 0x04 +// pbinFuzzAccountBit asks for an account write, pbinFuzzDeleteBit for an +// account removal; the low three bits of the selector pick the address. +const ( + pbinFuzzAccountBit = 0x08 + pbinFuzzDeleteBit = 0x10 +) -// pbinFuzzCodeSizes: the last entry is the only size that spills past the -// account header into the code zone. -var pbinFuzzCodeSizes = []int{0, 23, 31, 62, pbinHeaderCodeChunks*pbinChunkDataLen + 62} +// pbinFuzzCodeShapes is the code pool: a delegation indicator, codes ending in +// an all-zero chunk, and chunk counts straddling the 255/256 and 511/512 group +// boundaries. Address seeds fold onto shapes modulo four, so seeds four apart +// always share bytecode. +var pbinFuzzCodeShapes = [][]byte{ + nil, + pbinTestCode(23), + pbinTestIndicator(0x37), + pbinTestCode(2 * pbinChunkDataLen), + append(pbinTestCode(2*pbinChunkDataLen), make([]byte, pbinChunkDataLen)...), + pbinTestCode(255 * pbinChunkDataLen), + pbinTestCode(256 * pbinChunkDataLen), + append(pbinTestCode(256*pbinChunkDataLen), make([]byte, pbinChunkDataLen)...), + pbinTestCode(257 * pbinChunkDataLen), + pbinTestCode(511 * pbinChunkDataLen), + pbinTestCode(512 * pbinChunkDataLen), + pbinTestCode(513 * pbinChunkDataLen), +} // pbinFuzzCode keys the code on the address so it stays fixed for a whole run, // which is what keeps the oracle valid: a redeploy to shorter code leaves its // high chunks in the tree, and the oracle only knows the final state. func pbinFuzzCode(addrSeed, salt byte) []byte { - n := pbinFuzzCodeSizes[int(addrSeed+salt)%len(pbinFuzzCodeSizes)] - if n == 0 { - return nil - } - return pbinTestCode(n) + return pbinFuzzCodeShapes[(int(addrSeed%4)+int(salt))%len(pbinFuzzCodeShapes)] } // pbinFuzzCorpus reads the input three bytes at a time: what to write, where, -// and with what value. +// and with what value. A zero value byte writes zero storage, which is the +// deletion encoding. func pbinFuzzCorpus(data []byte, codeSalt byte) *pbinTestCorpus { c := new(pbinTestCorpus) for i := 0; i+2 < len(data); i += 3 { where, slot, value := data[i], data[i+1], data[i+2] - addrSeed := where & 0x03 + addrSeed := where & 0x07 addr := pbinOracleAddr(uint64(addrSeed)) - if where&pbinFuzzAccountBit != 0 { + switch { + case where&pbinFuzzDeleteBit != 0: + c.remove(addr) + case where&pbinFuzzAccountBit != 0: if code := pbinFuzzCode(addrSeed, codeSalt); code != nil { c.accountWithCodeBytes(addr, uint64(value), uint64(value)*1_000_000_007, code) } else { c.account(addr, uint64(value), uint64(value)*1_000_000_007, common.Hash{value, 0xC0}) } - continue + case value == 0: + c.storage(addr, pbinOracleSlot(pbinFuzzSlots[int(slot)%len(pbinFuzzSlots)])) + default: + c.storage(addr, pbinOracleSlot(pbinFuzzSlots[int(slot)%len(pbinFuzzSlots)]), value, value^0xFF) } - c.storage(addr, pbinOracleSlot(pbinFuzzSlots[int(slot)%len(pbinFuzzSlots)]), value, value^0xFF) } return c } // pbinFuzzBatches cuts the corpus in two, so a run also covers what one Process -// call leaves for the next to read back. +// call leaves for the next to read back — and on which side of the cut a +// removal lands, which decides whether an account created and destroyed by the +// corpus ever materializes. func pbinFuzzBatches(data []byte, cut, codeSalt byte) []*pbinTestCorpus { c := pbinFuzzCorpus(data, codeSalt) if len(c.plainKeys) == 0 { @@ -90,21 +113,60 @@ func pbinFuzzBatches(data []byte, cut, codeSalt byte) []*pbinTestCorpus { return batches } +// TestPBinFuzzCorpusCoversNewShapes pins the generator's reach, so the fuzz +// seeds cannot go vacuous: delegation, shared bytecode, all-zero chunks, both +// group-boundary straddles, and account removal. +func TestPBinFuzzCorpusCoversNewShapes(t *testing.T) { + t.Parallel() + + require.True(t, pbinIsDelegation(pbinFuzzCode(0, 2))) + require.Equal(t, pbinFuzzCode(1, 2), pbinFuzzCode(5, 2), "address seeds four apart share a shape") + require.NotEmpty(t, pbinFuzzCode(1, 2)) + + counts := make(map[int]bool, len(pbinFuzzCodeShapes)) + zeroTails := 0 + for _, shape := range pbinFuzzCodeShapes { + chunks := pbinChunkifyCode(shape) + counts[len(chunks)] = true + if len(chunks) > 0 && chunks[len(chunks)-1] == ([pbinValueLength]byte{}) { + zeroTails++ + } + } + for _, straddle := range []int{255, 256, 257, 511, 512, 513} { + require.True(t, counts[straddle], "no shape holds %d chunks", straddle) + } + require.NotZero(t, zeroTails, "no shape ends in an all-zero chunk") + + removal := pbinFuzzCorpus([]byte{pbinFuzzDeleteBit, 0, 0}, 0) + require.Len(t, removal.updates, 1) + require.True(t, removal.updates[0].Deleted()) +} + // FuzzPBinProcessMatchesOracle: whatever the generator produces, the engine's // root must equal the reference tree's over the same leaves, and the records it // left behind must rebuild that root on their own. // // go test ./execution/commitment/ -run=Fuzz -fuzz=FuzzPBinProcessMatchesOracle -fuzztime=60s func FuzzPBinProcessMatchesOracle(f *testing.F) { - // Seeds are (selector, slot, value) triples: bit 2 of the selector asks for an - // account, its low bits pick the address, and the slot byte indexes the pool. - f.Add([]byte{0x04, 0, 1, 0x05, 0, 2}, byte(0), byte(0)) // two accounts, no code + // Seeds are (selector, slot, value) triples: bit 3 of the selector asks for an + // account, bit 4 for its removal, the low bits pick the address, and the slot + // byte indexes the pool. + f.Add([]byte{0x08, 0, 1, 0x09, 0, 2}, byte(0), byte(0)) // two accounts, no code f.Add([]byte{0x00, 10, 1, 0x00, 11, 2, 0x00, 12, 3}, byte(2), byte(0)) // three slots of one group - f.Add([]byte{0x00, 3, 1, 0x00, 4, 2, 0x04, 0, 3}, byte(1), byte(0)) // the 63/64 zone boundary plus a header - f.Add([]byte{0x04, 0, 1, 0x00, 15, 2, 0x01, 15, 3, 0x02, 16, 4}, byte(3), byte(0)) // one slot per address + f.Add([]byte{0x00, 3, 1, 0x00, 4, 2, 0x08, 0, 3}, byte(1), byte(0)) // the 63/64 zone boundary plus a header + f.Add([]byte{0x08, 0, 1, 0x00, 15, 2, 0x01, 15, 3, 0x02, 16, 4}, byte(3), byte(0)) // one slot per address f.Add([]byte{0x00, 10, 1, 0x00, 10, 2, 0x00, 10, 3}, byte(1), byte(0)) // the same slot rewritten - f.Add([]byte{0x04, 0, 1, 0x00, 5, 2, 0x04, 0, 3}, byte(1), byte(1)) // code interleaved with a header slot - f.Add([]byte{0x04, 0, 1, 0x05, 0, 2, 0x00, 17, 3}, byte(2), byte(4)) // code spilling into the code zone + f.Add([]byte{0x08, 0, 1, 0x00, 5, 2, 0x08, 0, 3}, byte(1), byte(1)) // code interleaved with a header slot + f.Add([]byte{0x08, 0, 1, 0x09, 0, 2, 0x00, 17, 3}, byte(2), byte(4)) // a zero-tailed code beside a 255-chunk one + f.Add([]byte{0x08, 0, 1, 0x18, 0, 0}, byte(1), byte(2)) // a delegation inserted, then its account removed + f.Add([]byte{0x08, 0, 1, 0x08, 0, 2, 0x0C, 0, 3}, byte(1), byte(2)) // a delegation rewritten, plus a second authority on the target + f.Add([]byte{0x08, 0, 1}, byte(0), byte(7)) // a zero chunk alone in its group + f.Add([]byte{0x09, 0, 1, 0x0D, 0, 2, 0x15, 0, 0}, byte(2), byte(2)) // shared code outliving one holder + f.Add([]byte{0x09, 0, 1, 0x0D, 0, 2, 0x15, 0, 0}, byte(0), byte(2)) // shared code whose second holder dies in the writing batch + f.Add([]byte{0x08, 0, 1, 0x09, 0, 2, 0x0A, 0, 3, 0x0B, 0, 4}, byte(0), byte(5)) // chunk counts straddling 255/256 + f.Add([]byte{0x08, 0, 1, 0x09, 0, 2, 0x0A, 0, 3}, byte(0), byte(9)) // chunk counts straddling 511/512 + f.Add([]byte{0x00, 10, 1, 0x00, 10, 0}, byte(1), byte(0)) // a live slot zeroed by the next batch + f.Add([]byte{0x08, 0, 5, 0x08, 0, 0}, byte(1), byte(0)) // basic data zeroed while the code-hash leaf stays f.Fuzz(func(t *testing.T, data []byte, cut, codeSalt byte) { batches := pbinFuzzBatches(data, cut, codeSalt) @@ -120,12 +182,13 @@ func FuzzPBinProcessMatchesOracle(f *testing.F) { } require.Len(t, root, length.Hash) - union := pbinTestUnion(batches...) - require.Equal(t, union.oracleRoot(t), root) + final := pbinTestFinalEntries(t, batches...) + want := pbinOracleRoot(final) + require.Equal(t, want[:], root) // A tree of one leaf is that leaf and writes no record. - if leaves := union.leafCount(t); leaves > 1 { - pbinTestVerifyRecords(t, ms, root, leaves) + if len(final) > 1 { + pbinTestVerifyRecords(t, ms, root, len(final)) } }) } diff --git a/execution/commitment/pbin_hash.go b/execution/commitment/pbin_hash.go index a5b1cbda5fd..51e13314de6 100644 --- a/execution/commitment/pbin_hash.go +++ b/execution/commitment/pbin_hash.go @@ -28,7 +28,7 @@ import ( "github.com/erigontech/erigon/common/length" ) -// Node tags separating the two preimage shapes EIP-8297 defines (eip:191-206). +// Node tags separating the two preimage shapes EIP-8297 defines (eip:"Node merkelization"). const ( pbinLeafTag = 0x00 pbinBranchTag = 0x01 @@ -38,13 +38,15 @@ const ( pbinHashBufLen = 1 + 2 + (pbinMaxPathBits+7)/8 + 2*length.Hash ) -// pbinEmptyTreeHash is the hash of an absent subtree: 32 zero bytes (eip:208). +// pbinEmptyTreeHash is the hash of an absent subtree: 32 zero bytes +// (eip:"Node merkelization"). // Not empty.RootHash — the RLP empty-string MPT root would build a different tree. var pbinEmptyTreeHash common.Hash var errPBinCellHash = errors.New("pbin: cell cannot be hashed") -// pbinHashFn is H, which EIP-8297 leaves open (eip:511-513). Tree-key derivation +// pbinHashFn is H, which EIP-8297 leaves open +// (eip:"SNARK friendliness and post-quantum security"). Tree-key derivation // hashes with H too, so a suite is only fully swapped when pbinDigestCache is // swapped with it. type pbinHashFn func([]byte) common.Hash @@ -83,8 +85,9 @@ func PBinHashSuiteName() string { // pbinHasher applies H to node preimages. Its zero value is ready and hashes with // Keccak-256. type pbinHasher struct { - buf [pbinHashBufLen]byte - sum pbinHashFn + buf [pbinHashBufLen]byte + sum pbinHashFn + tracer witnessTracer // nil on the normal commitment path; see pbin_witness.go } func (h *pbinHasher) hash(preimage []byte) common.Hash { @@ -94,7 +97,7 @@ func (h *pbinHasher) hash(preimage []byte) common.Hash { return keccak.Sum256(preimage) } -// pbinAppendBitPrefix is the spec's encode_bit_prefix (eip:196-201). The leading +// pbinAppendBitPrefix is the spec's encode_bit_prefix (eip:"Node merkelization"). The leading // bit count is what keeps a 7-bit prefix distinct from an 8-bit one that agrees // with it on the pad bit. func pbinAppendBitPrefix(dst []byte, p *pbinBitpath) []byte { @@ -107,7 +110,9 @@ func (h *pbinHasher) branchHash(prefix *pbinBitpath, left, right *common.Hash) c buf := pbinAppendBitPrefix(append(h.buf[:0], pbinBranchTag), prefix) buf = append(buf, left[:]...) buf = append(buf, right[:]...) - return h.hash(buf) + hash := h.hash(buf) + h.emitNode(buf, &hash) + return hash } // cellHash hashes the cell reached by path; a leaf's complete key is path @@ -144,7 +149,7 @@ func (h *pbinHasher) leafCellHash(c *pbinCell, path *pbinBitpath) (common.Hash, buf := full.appendPackedBits(append(h.buf[:0], pbinLeafTag)) key := buf[1:] // Key length is fixed per zone, which is what keeps the key space prefix-free - // (eip:284-288). + // (eip:"Tree embedding"). if want, known := pbinZoneKeyLength(key[0]); !known || len(key) != want { return common.Hash{}, fmt.Errorf("%w: leaf key %x is no key of zone %#x", errPBinCellHash, key, key[0]) } @@ -152,7 +157,10 @@ func (h *pbinHasher) leafCellHash(c *pbinCell, path *pbinBitpath) (common.Hash, if err != nil { return common.Hash{}, err } - return h.hash(append(buf, value[:]...)), nil + buf = append(buf, value[:]...) + hash := h.hash(buf) + h.emitNode(buf, &hash) + return hash, nil } func pbinLeafValue(key []byte, u *Update) ([pbinValueLength]byte, error) { @@ -170,12 +178,14 @@ func pbinLeafValue(key []byte, u *Update) ([pbinValueLength]byte, error) { return pbinEncodeBasicData(u.Nonce, &u.Balance, u.CodeSize) case subIndex == pbinCodeHashLeafKey: return pbinCodeHashValue(u.CodeHash), nil - case subIndex >= pbinHeaderStorageOffset && subIndex < pbinCodeOffset: + case subIndex == pbinDelegationLeafKey: + // An EIP-7702 indicator is no account field, so the leaf carries its own bytes. + return pbinRecordLeafValue(u) + case subIndex >= pbinHeaderStorageOffset && subIndex < pbinHeaderStorageOffset+pbinHeaderStorageSlots: return pbinEncodeStorageValue(u.Storage[:u.StorageLen]), nil default: - // Code chunks from CODE_OFFSET on, plus the sub-indices the embedding - // reserves below HEADER_STORAGE_OFFSET (eip:255-257): neither is packed from - // state, so the value must already be 32 whole bytes. + // Sub-indices the embedding reserves (eip:"Header values"): not packed from state, + // so the value must already be 32 whole bytes. return pbinRecordLeafValue(u) } } diff --git a/execution/commitment/pbin_hash_test.go b/execution/commitment/pbin_hash_test.go index ba31a3d9e4f..19a9b8256a2 100644 --- a/execution/commitment/pbin_hash_test.go +++ b/execution/commitment/pbin_hash_test.go @@ -88,7 +88,7 @@ func pbinTestOracleLeaf(addr, slot uint64) *pbinOracleLeaf { } } -// EIP-8297's empty subtree is 32 zero bytes (eip:208), not the empty-MPT root +// EIP-8297's empty subtree is 32 zero bytes (eip:"Node merkelization"), not the empty-MPT root // the rest of erigon reaches for. func TestPBinEmptyTreeHash(t *testing.T) { t.Parallel() @@ -307,7 +307,7 @@ func TestPBinCellHashRejectsMalformedLeaf(t *testing.T) { require.ErrorIs(t, err, errPBinCellHash) }) t.Run("account-zone sub-index naming no leaf", func(t *testing.T) { - bad := pbinPathFromBytes(pbinTreeKey(pbinAccountZone, make([]byte, 32), pbinCodeOffset)) + bad := pbinPathFromBytes(pbinTreeKey(pbinAccountZone, make([]byte, 32), pbinHeaderStorageOffset+pbinHeaderStorageSlots)) path := bad.slice(0, 100) c := pbinCell{kind: pbinNodeLeaf, prefix: bad.slice(100, bad.bitLen)} _, err := h.cellHash(&c, &path) diff --git a/execution/commitment/pbin_hashsuite_test.go b/execution/commitment/pbin_hashsuite_test.go index e352854144c..153cf66bef0 100644 --- a/execution/commitment/pbin_hashsuite_test.go +++ b/execution/commitment/pbin_hashsuite_test.go @@ -101,7 +101,7 @@ func TestPBinBlake3SuiteMatchesSpecRoots(t *testing.T) { require.Equal(t, tc.Root[2:], rootOf(t, tc)) if len(tc.Entries) == 0 { - return // the empty tree is 32 zero bytes under any hash (eip:208) + return // the empty tree is 32 zero bytes under any hash (eip:"Node merkelization") } require.NoError(t, SetPBinHashSuite(PBinHashKeccak)) require.NotEqual(t, tc.Root[2:], rootOf(t, tc), "keccak must not reproduce a blake3 reference root") diff --git a/execution/commitment/pbin_hazard_test.go b/execution/commitment/pbin_hazard_test.go index c4a1995d489..c6c56220274 100644 --- a/execution/commitment/pbin_hazard_test.go +++ b/execution/commitment/pbin_hazard_test.go @@ -18,6 +18,7 @@ package commitment import ( "bytes" + "maps" "math/rand" "slices" "testing" @@ -73,6 +74,7 @@ func (c *pbinTestCorpus) permute(order []int) *pbinTestCorpus { out.plainKeys = append(out.plainKeys, c.plainKeys[i]) out.updates = append(out.updates, c.updates[i]) } + out.codes = maps.Clone(c.codes) return out } diff --git a/execution/commitment/pbin_keys.go b/execution/commitment/pbin_keys.go index 728a6ca99ac..1b46444298c 100644 --- a/execution/commitment/pbin_keys.go +++ b/execution/commitment/pbin_keys.go @@ -27,12 +27,13 @@ import ( "github.com/erigontech/erigon/common/length" ) -// EIP-8297 embedding constants (eip:261-278). +// EIP-8297 embedding constants (eip:"Tree embedding"). const ( pbinBasicDataLeafKey = 0 pbinCodeHashLeafKey = 1 + pbinDelegationLeafKey = 2 pbinHeaderStorageOffset = 64 - pbinCodeOffset = 128 + pbinHeaderStorageSlots = 64 pbinStemSubtreeWidth = 256 pbinAccountZone = 0x00 @@ -45,7 +46,7 @@ const ( ) // pbinZoneKeyLength gives the single key length a zone admits, which is what -// keeps that zone's keys prefix-free (eip:284-288). Zones 0x02..0xFE are +// keeps that zone's keys prefix-free (eip:"Tree embedding"). Zones 0x02..0xFE are // unallocated and have no length. func pbinZoneKeyLength(zone byte) (int, bool) { switch zone { @@ -60,7 +61,7 @@ func pbinZoneKeyLength(zone byte) (int, bool) { } } -// pbinAddr32 widens a legacy address to the spec's Address32 (eip:291-296). +// pbinAddr32 widens a legacy address to the spec's Address32 (eip:"Tree embedding"). func pbinAddr32(addr []byte) [32]byte { if len(addr) > 32 { panic(fmt.Sprintf("pbin: address of %d bytes exceeds 32", len(addr))) @@ -88,35 +89,37 @@ func pbinTreeKey(zone byte, treePosition []byte, subIndex byte) []byte { return key } -// pbinTreeKeyAccount returns the account-header key at subIndex (eip:311-320). +// pbinTreeKeyAccount returns the account-header key at subIndex (eip:"Header values"). func pbinTreeKeyAccount(addr []byte, subIndex byte) []byte { var c pbinDigestCache return c.accountKey(addr, subIndex) } // pbinTreeKeyStorage returns the key for a storage slot: slots below 64 live in -// the account header, the rest in the storage zone (eip:415-437). slot is +// the account header, the rest in the storage zone (eip:"Storage"). slot is // big-endian and at most 32 bytes. func pbinTreeKeyStorage(addr, slot []byte) []byte { var c pbinDigestCache return c.storageKey(addr, slot) } -// pbinTreeKeyCodeChunk returns the key for a code chunk the account header holds, -// sharing the account's own stem (eip:355-367). Higher chunks go through -// pbinTreeKeyCodeOverflow. -func pbinTreeKeyCodeChunk(addr []byte, chunkID int) []byte { - var c pbinDigestCache - return c.codeChunkKey(addr, chunkID) +// PBinStorageZoneProbeSlot is the lowest slot that lives outside the account +// header (eip:"Storage"), so a proof of its key walks the account's whole +// storage-zone prefix. That is what lets a witness answer EIP-7610's +// non-empty-storage predicate for the zone, which no leaf of the account's own +// header stem can report. +func PBinStorageZoneProbeSlot() common.Hash { + var slot common.Hash + slot[length.Hash-1] = pbinHeaderStorageSlots + return slot } -// pbinTreeKeyCodeOverflow returns the code-zone key for a chunk past the account -// header (eip:355-367). These chunks are content-addressed by code hash, so -// accounts running the same bytecode share the leaves and no address can derive -// the key. -func pbinTreeKeyCodeOverflow(codeHash common.Hash, chunkID int) []byte { +// pbinTreeKeyCodeChunk returns the code-zone key of a chunk (eip:"Code"). +// Chunks are content-addressed by code hash, so accounts running the same +// bytecode share the leaves and no address takes part in the derivation. +func pbinTreeKeyCodeChunk(codeHash common.Hash, chunkID int) []byte { var c pbinDigestCache - return c.codeOverflowKey(codeHash, chunkID) + return c.codeChunkKey(codeHash, chunkID) } // pbinKeyHasher returns a keyHasher deriving the primary leaf's tree key: @@ -147,7 +150,7 @@ func pbinKeyHasherWith(sum pbinHashFn) keyHasher { // pbinDigestCache memoizes the two hash-derived key components: key_hash(addr32) // per address and key_hash(addr32||tree_index) per 256-slot storage group -// (eip:411-414). The group entry is bound to the address as well as the index, so +// (eip:"Storage"). The group entry is bound to the address as well as the index, so // a changed address cannot yield a stale hit. type pbinDigestCache struct { sum pbinHashFn @@ -202,26 +205,31 @@ func (c *pbinDigestCache) accountKey(addr []byte, subIndex byte) []byte { return pbinTreeKey(pbinAccountZone, c.stemDigest(&addr32)[:], subIndex) } -func (c *pbinDigestCache) codeChunkKey(addr []byte, chunkID int) []byte { - if chunkID < 0 || chunkID >= pbinHeaderCodeChunks { - panic(fmt.Sprintf("pbin: code chunk %d lives outside the account header", chunkID)) - } - return c.accountKey(addr, byte(pbinCodeOffset+chunkID)) +// accountHeaderStem and accountStoragePrefix are the two key-space regions an +// account owns, both fixed by its address. Removing an account is removing these +// two subtrees (eip:"Zero values and deletion"). +func (c *pbinDigestCache) accountHeaderStem(addr []byte) []byte { + addr32 := pbinAddr32(addr) + return append([]byte{pbinAccountZone}, c.stemDigest(&addr32)[:]...) +} + +func (c *pbinDigestCache) accountStoragePrefix(addr []byte) []byte { + addr32 := pbinAddr32(addr) + return append([]byte{pbinStorageZone}, c.stemDigest(&addr32)[:]...) } -// codeOverflowKey derives the code-zone key of an overflow chunk. The digest is -// not memoized: one contract spans at most a handful of tree indexes, and the +// codeChunkKey derives the code-zone key of a chunk. The digest is not +// memoized: one contract spans at most a handful of tree indexes, and the // cache's entries are bound to an address these keys do not have. -func (c *pbinDigestCache) codeOverflowKey(codeHash common.Hash, chunkID int) []byte { - if chunkID < pbinHeaderCodeChunks { - panic(fmt.Sprintf("pbin: code chunk %d is a header chunk, not a code-zone one", chunkID)) +func (c *pbinDigestCache) codeChunkKey(codeHash common.Hash, chunkID int) []byte { + if chunkID < 0 { + panic(fmt.Sprintf("pbin: code chunk %d is negative", chunkID)) } - overflow := chunkID - pbinHeaderCodeChunks var preimage [2 * length.Hash]byte copy(preimage[:], codeHash[:]) - binary.BigEndian.PutUint64(preimage[2*length.Hash-8:], uint64(overflow/pbinStemSubtreeWidth)) + binary.BigEndian.PutUint64(preimage[2*length.Hash-8:], uint64(chunkID/pbinStemSubtreeWidth)) position := c.hash(preimage[:]) - return pbinTreeKey(pbinCodeZone, position[:], byte(overflow%pbinStemSubtreeWidth)) + return pbinTreeKey(pbinCodeZone, position[:], byte(chunkID%pbinStemSubtreeWidth)) } func (c *pbinDigestCache) storageKey(addr, slot []byte) []byte { @@ -262,5 +270,5 @@ func pbinSlotInHeader(slot *[32]byte) bool { return false } } - return slot[31] < pbinCodeOffset-pbinHeaderStorageOffset + return slot[31] < pbinHeaderStorageSlots } diff --git a/execution/commitment/pbin_keys_test.go b/execution/commitment/pbin_keys_test.go index f8c85914c9a..f9a05732eb7 100644 --- a/execution/commitment/pbin_keys_test.go +++ b/execution/commitment/pbin_keys_test.go @@ -25,6 +25,8 @@ import ( "github.com/stretchr/testify/assert" "github.com/stretchr/testify/require" "golang.org/x/crypto/sha3" + + "github.com/erigontech/erigon/common/length" ) // pbinTestKeccak is an independent Keccak-256 (x/crypto, not the fastkeccak the @@ -48,7 +50,7 @@ func pbinTestAddr(t *testing.T, s string) []byte { return b } -// pbinTestAddress32 is the spec's address20_to_address32 (eip:291-296). +// pbinTestAddress32 is the spec's address20_to_address32 (eip:"Tree embedding"). func pbinTestAddress32(addr []byte) []byte { a := make([]byte, 32) copy(a[32-len(addr):], addr) @@ -71,7 +73,7 @@ func pbinTestConcat(parts ...[]byte) []byte { return out } -// Pins the derivation against the spec's test cases (eip:583-630). +// Pins the derivation against the spec's test cases (eip:"Test Cases"). func TestPBinTreeKeyEIPVectors(t *testing.T) { t.Parallel() @@ -270,3 +272,21 @@ func TestPBinDigestCacheMatchesFreshDerivation(t *testing.T) { } } } + +// The witness builder proves an account's storage zone occupied or empty by +// touching one slot in it, so the probe slot has to derive a key under the +// account's storage prefix and never a header one. +func TestPBinStorageZoneProbeSlotLandsInTheZone(t *testing.T) { + t.Parallel() + + probe := PBinStorageZoneProbeSlot() + var c pbinDigestCache + for _, s := range []string{ + "0102030405060708090a0b0c0d0e0f1011121314", + "cafebabe000000000000000000000000deadbeef", + } { + addr := pbinTestAddr(t, s) + require.Equal(t, c.accountStoragePrefix(addr), c.storageKey(addr, probe[:])[:1+length.Hash], + "the probe key has to sit under the account's storage prefix") + } +} diff --git a/execution/commitment/pbin_oracle_test.go b/execution/commitment/pbin_oracle_test.go index 351ea36e2c3..88e86a36dcd 100644 --- a/execution/commitment/pbin_oracle_test.go +++ b/execution/commitment/pbin_oracle_test.go @@ -33,7 +33,8 @@ import ( "github.com/erigontech/erigon/common/length" ) -// The reference implementation of EIP-8297's binary tree (eip:112-222), +// The reference implementation of EIP-8297's binary tree +// (eip:"Tree structure", "Node merkelization", "Insertion and deletion"), // transcribed from the spec's Python with no optimisation — no memoised hashes, // no shared buffers, one bit per byte — because it is the ground truth the // engine is diffed against and has to stay recognisably the same algorithm. Its @@ -133,7 +134,7 @@ func pbinOracleInsert(node pbinOracleNode, bits, key, value []byte, depth int) p return branch } - // The key diverges inside the prefix (eip:171-182). The survivor keeps the + // The key diverges inside the prefix (eip:"Insertion and deletion"). The survivor keeps the // bits after the divergence, dropping the bit the new branch consumes. survivor := &pbinOracleBranch{ prefix: slices.Clone(branch.prefix[matched+1:]), @@ -150,7 +151,7 @@ func pbinOracleInsert(node pbinOracleNode, bits, key, value []byte, depth int) p return newBranch } -// pbinOracleEncodeBitPrefix is the spec's encode_bit_prefix (eip:196-201). +// pbinOracleEncodeBitPrefix is the spec's encode_bit_prefix (eip:"Node merkelization"). func pbinOracleEncodeBitPrefix(prefix []byte) []byte { if len(prefix) >= 1<<16 { panic(fmt.Sprintf("pbin oracle: prefix of %d bits exceeds the encodable count", len(prefix))) @@ -421,7 +422,7 @@ func TestPBinOracleEncodeBitPrefixLongRun(t *testing.T) { require.Equal(t, bytes.Repeat([]byte{0xFF}, 66), got[2:]) } -// The empty tree is 32 zero bytes (eip:208), not the empty-MPT root the rest of +// The empty tree is 32 zero bytes (eip:"Node merkelization"), not the empty-MPT root the rest of // erigon uses. func TestPBinOracleEmptyTreeHash(t *testing.T) { t.Parallel() @@ -442,7 +443,7 @@ func TestPBinOracleSingleKeyRootIsLeafHash(t *testing.T) { var tree pbinOracleTree tree.insert(e.key, e.value) - require.IsType(t, &pbinOracleLeaf{}, tree.root, "a one-key tree's root is the leaf itself (eip:133-135)") + require.IsType(t, &pbinOracleLeaf{}, tree.root, "a one-key tree's root is the leaf itself (eip:\"Tree structure\")") want := pbinTestKeccak(t, []byte{0x00}, e.key, e.value) got := tree.rootHash() @@ -496,7 +497,7 @@ func TestPBinOracleSplitAtLastBit(t *testing.T) { require.Equal(t, want, got[:]) } -// Pins the shape of the split-inside-prefix branch (eip:171-182): the bit the +// Pins the shape of the split-inside-prefix branch (eip:"Insertion and deletion"): the bit the // new branch consumes must not reappear in the survivor below it. func TestPBinOracleSplitInsidePrefix(t *testing.T) { t.Parallel() diff --git a/execution/commitment/pbin_overflow_test.go b/execution/commitment/pbin_overflow_test.go index c49e5ac312e..0658169636a 100644 --- a/execution/commitment/pbin_overflow_test.go +++ b/execution/commitment/pbin_overflow_test.go @@ -17,7 +17,9 @@ package commitment import ( + "encoding/hex" "fmt" + "strconv" "testing" "github.com/stretchr/testify/require" @@ -27,60 +29,89 @@ import ( ) // pbinTestSpecCodeChunkKey transcribes get_tree_key_for_code_chunk -// (eip:355-367) from the spec's Python, hashing with the independent Keccak the +// (eip:"Code") from the spec's Python, hashing with the independent Keccak the // tests use. It is the ground truth for the cache-backed derivation. -func pbinTestSpecCodeChunkKey(t *testing.T, addr []byte, codeHash common.Hash, chunkID int) []byte { +func pbinTestSpecCodeChunkKey(t *testing.T, codeHash common.Hash, chunkID int) []byte { t.Helper() - if chunkID < pbinStemSubtreeWidth-pbinCodeOffset { - stem := pbinTestKeccak(t, pbinTestAddress32(addr)) - return append(append([]byte{pbinAccountZone}, stem...), byte(pbinCodeOffset+chunkID)) - } - overflow := chunkID - (pbinStemSubtreeWidth - pbinCodeOffset) - position := pbinTestKeccak(t, codeHash[:], pbinTestBE32(uint64(overflow/pbinStemSubtreeWidth))) - key := append(append([]byte{pbinCodeZone}, position...), byte(overflow%pbinStemSubtreeWidth)) + position := pbinTestKeccak(t, codeHash[:], pbinTestBE32(uint64(chunkID/pbinStemSubtreeWidth))) + key := append(append([]byte{pbinCodeZone}, position...), byte(chunkID%pbinStemSubtreeWidth)) require.Len(t, key, pbinCodeKeyLength) return key } -// TestPBinCodeOverflowKeyMatchesSpec pins the second half of the code embedding: -// past the account header a chunk is content-addressed by code hash, with the -// overflow index split into a 32-byte tree index and a sub-index. -func TestPBinCodeOverflowKeyMatchesSpec(t *testing.T) { +// TestPBinChunkKeyMatchesSpec pins the code embedding: every chunk is +// content-addressed by code hash, with the chunk id split into a 32-byte tree +// index and a sub-index. +func TestPBinChunkKeyMatchesSpec(t *testing.T) { t.Parallel() - addr := pbinOracleAddr(60) codeHash := common.Hash{0x82, 0x97} for _, chunkID := range []int{ - pbinHeaderCodeChunks, // the first overflow chunk - pbinHeaderCodeChunks + 1, // its neighbour on the same code stem - pbinHeaderCodeChunks + pbinStemSubtreeWidth - 1, // the last of the first code stem - pbinHeaderCodeChunks + pbinStemSubtreeWidth, // the first of the second - 792, // the last chunk MaxCodeSize produces + 0, + 1, + pbinStemSubtreeWidth - 1, // the last of the first code group + pbinStemSubtreeWidth, // the first of the second + 792, // the last chunk MaxCodeSize produces } { t.Run(fmt.Sprintf("chunk %d", chunkID), func(t *testing.T) { t.Parallel() - got := pbinTreeKeyCodeOverflow(codeHash, chunkID) - require.Equal(t, pbinTestSpecCodeChunkKey(t, addr, codeHash, chunkID), got) + got := pbinTreeKeyCodeChunk(codeHash, chunkID) + require.Equal(t, pbinTestSpecCodeChunkKey(t, codeHash, chunkID), got) require.Len(t, got, pbinCodeKeyLength) require.EqualValues(t, pbinCodeZone, got[0]) }) } - require.Panics(t, func() { pbinTreeKeyCodeOverflow(codeHash, pbinHeaderCodeChunks-1) }, - "a header chunk has no code-zone key") + require.Panics(t, func() { pbinTreeKeyCodeChunk(codeHash, -1) }, + "a negative chunk id names no key") +} + +// TestPBinChunkKeyMatchesVectorIndices pins the derivation against the +// reference corpus at every chunk id the corpus carries — both sides of the +// 255/256 and 511/512 group boundaries, and the last chunk of MAX_CODE_SIZE. +func TestPBinChunkKeyMatchesVectorIndices(t *testing.T) { + e := pbinLoadConformance(t).Embedding + codeHash := common.BytesToHash(pbinUnhex(t, e.CodeHash)) + keys := pbinDigestCache{sum: pbinBlake3Hash} + + wantIDs := []int{0, 1, 255, 256, 257, 511, 512, 2114} + require.Len(t, e.CodeChunkKeys, len(wantIDs)) + for _, id := range wantIDs { + want, ok := e.CodeChunkKeys[strconv.Itoa(id)] + require.True(t, ok, "the corpus carries no chunk %d", id) + require.Equal(t, want, "0x"+hex.EncodeToString(keys.codeChunkKey(codeHash, id)), "chunk %d", id) + } +} + +// TestPBinChunkKeyIgnoresAddress: the derivation takes no address, so a digest +// cache warmed on an account stem must not leak its memoized digests into a +// chunk key. +func TestPBinChunkKeyIgnoresAddress(t *testing.T) { + t.Parallel() + + codeHash := common.Hash{0x82, 0x97} + var a, b pbinDigestCache + a.accountKey(pbinOracleAddr(60), pbinBasicDataLeafKey) + b.accountKey(pbinOracleAddr(61), pbinBasicDataLeafKey) + + for _, chunkID := range []int{0, pbinStemSubtreeWidth - 1, pbinStemSubtreeWidth, 2114} { + fresh := pbinTreeKeyCodeChunk(codeHash, chunkID) + require.Equal(t, fresh, a.codeChunkKey(codeHash, chunkID), "chunk %d", chunkID) + require.Equal(t, fresh, b.codeChunkKey(codeHash, chunkID), "chunk %d", chunkID) + } } // TestPBinCodeKeyNeverRoutesToTheStorageZone pins that a code key cannot reach -// the storage zone. An overflow key derives from code_hash ‖ tree_index, a -// 64-byte preimage that is not a plain key at all, and the stream's key hasher -// accepts only the two plain-key shapes. +// the storage zone. A chunk key derives from code_hash ‖ tree_index, a 64-byte +// preimage that is not a plain key at all, and the stream's key hasher accepts +// only the two plain-key shapes. func TestPBinCodeKeyNeverRoutesToTheStorageZone(t *testing.T) { t.Parallel() codeHash := common.Hash{0x11} - for chunkID := pbinHeaderCodeChunks; chunkID < pbinHeaderCodeChunks+600; chunkID += 37 { - key := pbinTreeKeyCodeOverflow(codeHash, chunkID) + for chunkID := 0; chunkID < 600; chunkID += 37 { + key := pbinTreeKeyCodeChunk(codeHash, chunkID) require.EqualValues(t, pbinCodeZone, key[0], "chunk %d", chunkID) require.Len(t, key, pbinCodeKeyLength, "chunk %d", chunkID) } @@ -89,25 +120,24 @@ func TestPBinCodeKeyNeverRoutesToTheStorageZone(t *testing.T) { for _, plainKey := range [][]byte{ make([]byte, pbinCodeKeyLength), // a code key handed back as a plain key make([]byte, pbinCodeKeyLength-1), // its stem - make([]byte, 2*length.Hash), // the overflow preimage itself + make([]byte, 2*length.Hash), // the chunk-position preimage itself } { require.Panics(t, func() { hasher(plainKey) }, "a %d-byte plain key is neither an account nor a storage key", len(plainKey)) } } -// TestPBinEngineCommitsOverflowCodeChunks is the code zone end to end: code -// outgrowing the account header keeps its first 128 chunks on the account stem -// and puts the rest in the code zone. -func TestPBinEngineCommitsOverflowCodeChunks(t *testing.T) { +// TestPBinChunksCrossGroupBoundary is the code zone end to end: chunk 256 opens +// a second code group on its own stem, and the engine commits both groups. +func TestPBinChunksCrossGroupBoundary(t *testing.T) { t.Parallel() for _, tc := range []struct { name string chunks int }{ - {name: "one chunk past the header", chunks: pbinHeaderCodeChunks + 1}, - {name: "crosses a code stem", chunks: pbinHeaderCodeChunks + pbinStemSubtreeWidth + 1}, + {name: "fills group 0", chunks: pbinStemSubtreeWidth}, + {name: "one chunk into group 1", chunks: pbinStemSubtreeWidth + 1}, } { t.Run(tc.name, func(t *testing.T) { t.Parallel() @@ -125,26 +155,31 @@ func TestPBinEngineCommitsOverflowCodeChunks(t *testing.T) { pbinTestVerifyRecords(t, ms, root, corpus.leafCount(t)) }) } + + h := common.Hash{0xAB} + last, first := pbinTreeKeyCodeChunk(h, pbinStemSubtreeWidth-1), pbinTreeKeyCodeChunk(h, pbinStemSubtreeWidth) + require.NotEqual(t, last[1:33], first[1:33], "group 1 sits on its own stem") + require.EqualValues(t, pbinStemSubtreeWidth-1, last[33]) + require.EqualValues(t, 0, first[33], "the sub-index wraps at the group boundary") } -// TestPBinOverflowChunksAreSharedByIdenticalCode pins the point of -// content-addressing (eip:352-354): two accounts running the same bytecode name -// the same code-zone leaves, so the zone holds one copy of them. -func TestPBinOverflowChunksAreSharedByIdenticalCode(t *testing.T) { +// TestPBinSharedBytecodeEmitsOneChunkSet pins the point of content addressing +// (eip:"Code"): two accounts running the same bytecode name the same code-zone +// leaves, so the zone holds one copy of them. +func TestPBinSharedBytecodeEmitsOneChunkSet(t *testing.T) { t.Parallel() - code := pbinTestCode((pbinHeaderCodeChunks+2)*pbinChunkDataLen - 3) + code := pbinTestCode((pbinStemSubtreeWidth+2)*pbinChunkDataLen - 3) a, b := pbinOracleAddr(62), pbinOracleAddr(63) corpus := new(pbinTestCorpus). accountWithCodeBytes(a, 1, 10, code). accountWithCodeBytes(b, 2, 20, code) chunks := len(pbinChunkifyCode(code)) - overflow := chunks - pbinHeaderCodeChunks - require.Equal(t, 2, overflow) - // Two accounts: four header leaves, two full sets of header chunks, one - // shared set in the code zone. - require.Equal(t, 2*(2+pbinHeaderCodeChunks)+overflow, corpus.leafCount(t)) + require.Equal(t, pbinStemSubtreeWidth+2, chunks) + // Two accounts: four header leaves and one shared chunk set spanning two + // code groups. + require.Equal(t, 2*2+chunks, corpus.leafCount(t)) pph, ms := pbinTestEngine(t) corpus.applyTo(t, ms) @@ -154,14 +189,14 @@ func TestPBinOverflowChunksAreSharedByIdenticalCode(t *testing.T) { pbinTestVerifyRecords(t, ms, root, corpus.leafCount(t)) } -// TestPBinOverflowChunksFollowEveryAccountZoneKey pins where the code-zone block +// TestPBinCodeChunksFollowEveryAccountZoneKey pins where the code-zone block // sits in the visit order: the zone byte puts it after every account-header key // and before every storage-zone one, so the chunks of an account visited early // have to wait for the last account of the run. -func TestPBinOverflowChunksFollowEveryAccountZoneKey(t *testing.T) { +func TestPBinCodeChunksFollowEveryAccountZoneKey(t *testing.T) { t.Parallel() - code := pbinTestCode((pbinHeaderCodeChunks + 1) * pbinChunkDataLen) + code := pbinTestCode(5 * pbinChunkDataLen) early := pbinOracleAddr(64) corpus := new(pbinTestCorpus).accountWithCodeBytes(early, 1, 10, code) for i := uint64(65); i < 70; i++ { diff --git a/execution/commitment/pbin_pathlimit_test.go b/execution/commitment/pbin_pathlimit_test.go new file mode 100644 index 00000000000..28818b3fe2f --- /dev/null +++ b/execution/commitment/pbin_pathlimit_test.go @@ -0,0 +1,87 @@ +// Copyright 2026 The Erigon Authors +// This file is part of Erigon. +// +// Erigon is free software: you can redistribute it and/or modify +// it under the terms of the GNU Lesser General Public License as published by +// the Free Software Foundation, either version 3 of the License, or +// (at your option) any later version. +// +// Erigon is distributed in the hope that it will be useful, +// but WITHOUT ANY WARRANTY; without even the implied warranty of +// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +// GNU Lesser General Public License for more details. +// +// You should have received a copy of the GNU Lesser General Public License +// along with Erigon. If not, see . + +package commitment + +import ( + "bytes" + "testing" + + "github.com/stretchr/testify/require" + + "github.com/erigontech/erigon/common" + "github.com/erigontech/erigon/db/kv" +) + +// A key path of exactly pbinMaxPathBits is decodable off the wire but cannot be +// a branch: naming either child needs one bit more than a path holds. Both +// places that descend from a branch build that child path, so an untrusted node +// set must be refused there rather than panic inside the bit-path arithmetic. + +func pbinTestFullLengthPath() pbinBitpath { + return pbinPathFromBits(bytes.Repeat([]byte{0xAA}, pbinMaxPathBits/8), pbinMaxPathBits) +} + +type pbinTestFixedBranchCtx struct { + PatriciaContext + record []byte +} + +func (c *pbinTestFixedBranchCtx) Branch([]byte) ([]byte, kv.Step, error) { return c.record, 0, nil } + +// TestPBinWitnessRefusesBranchAtMaxPath: a witness whose node at the longest +// representable path is a branch is malformed, and reading its record must say +// so instead of panicking. +func TestPBinWitnessRefusesBranchAtMaxPath(t *testing.T) { + t.Parallel() + + prefix := pbinTestFullLengthPath() + root := common.Hash{0x01} + tree := &pbinWitnessTree{ + root: root, + nodes: map[common.Hash]pbinWitnessNode{ + root: {tag: pbinBranchTag, prefix: prefix, children: [2]common.Hash{{0x02}, {0x03}}}, + }, + } + + _, _, err := pbinNewWitnessContext(tree).Branch(pbinEncodeBitPath(&prefix)) + require.ErrorIs(t, err, errPBinWitnessNode) +} + +// TestPBinMaterializeRefusesBranchAtMaxPath: a prefix decoded from a witness is +// bounded on its own, so a cell reached at 528-n bits may carry an n-bit prefix +// that lands the branch exactly at the limit. +func TestPBinMaterializeRefusesBranchAtMaxPath(t *testing.T) { + t.Parallel() + + empty := pbinNewWitnessContext(&pbinWitnessTree{nodes: map[common.Hash]pbinWitnessNode{}}) + var atRoot pbinBitpath + record, err := empty.branchRecord(&pbinWitnessNode{ + tag: pbinBranchTag, children: [2]common.Hash{{0x02}, {0x03}}, + }, &atRoot) + require.NoError(t, err) + + pph := NewPBinPatriciaHashed(&pbinTestFixedBranchCtx{record: record}) + defer pph.Release() + + var cell pbinCell + cell.reset() + cell.kind = pbinNodeBranch + cell.prefix = pbinPathFromBits([]byte{0xAA}, 8) + path := pbinPathFromBits(bytes.Repeat([]byte{0xAA}, pbinMaxPathBits/8-1), pbinMaxPathBits-8) + + require.ErrorIs(t, pph.materializeBranch(&cell, &path), errPBinCellHash) +} diff --git a/execution/commitment/pbin_patricia_hashed.go b/execution/commitment/pbin_patricia_hashed.go index ed2e870c38e..8b559ee110e 100644 --- a/execution/commitment/pbin_patricia_hashed.go +++ b/execution/commitment/pbin_patricia_hashed.go @@ -103,6 +103,7 @@ func (pph *PBinPatriciaHashed) Reset() { pph.rootPrev = nil pph.updateStream.reset() pph.lastKeyLen = 0 + pph.hasher.tracer = nil } // setHashSuite swaps the hash on both seams at once — node hashing here and the @@ -128,7 +129,7 @@ func (pph *PBinPatriciaHashed) Release() { var ( errPBinMissingBranch = errors.New("pbin: branch record missing") - errPBinDeleteUnsupported = errors.New("pbin: EIP-8297 defines no deletion") + errPBinDeleteUnsupported = errors.New("pbin: account record outlived its state") errPBinVisitOrder = errors.New("pbin: visit order is not ascending") ) @@ -173,27 +174,37 @@ func (pph *PBinPatriciaHashed) Process(ctx context.Context, updates *Updates, lo } // followAndUpdate moves the grid onto treeKey and writes the update into the -// cell that lands there. Visits must ascend: a fold writes the row's record -// outright, so returning to a folded row would rewrite it under a touch map -// that no longer names what the first write touched. +// cell that lands there. func (pph *PBinPatriciaHashed) followAndUpdate(treeKey, plainKey []byte, update *Update) error { + probe, err := pph.seek(treeKey) + if err != nil { + return err + } + return pph.updateCell(plainKey, &probe, update) +} + +// seek moves the grid onto treeKey and returns the path to it. Visits must +// ascend: a fold writes the row's record outright, so returning to a folded row +// would rewrite it under a touch map that no longer names what the first write +// touched. +func (pph *PBinPatriciaHashed) seek(treeKey []byte) (pbinBitpath, error) { if pph.lastKeyLen > 0 && bytes.Compare(treeKey, pph.lastKey[:pph.lastKeyLen]) <= 0 { - return fmt.Errorf("%w: %x after %x", errPBinVisitOrder, treeKey, pph.lastKey[:pph.lastKeyLen]) + return pbinBitpath{}, fmt.Errorf("%w: %x after %x", errPBinVisitOrder, treeKey, pph.lastKey[:pph.lastKeyLen]) } pph.lastKeyLen = int16(copy(pph.lastKey[:], treeKey)) probe := pbinPathFromBytes(treeKey) for !probe.hasPrefix(&pph.currentKey) { if err := pph.fold(); err != nil { - return err + return probe, err } } for u := pph.needUnfolding(&probe); u.action != pbinUnfoldNone; u = pph.needUnfolding(&probe) { if err := pph.unfold(&probe, u); err != nil { - return err + return probe, err } } - return pph.updateCell(plainKey, &probe, update) + return probe, nil } // updateCell writes one leaf into the deepest open row. Unfolding has already @@ -217,17 +228,38 @@ func (pph *PBinPatriciaHashed) updateCell(plainKey []byte, probe *pbinBitpath, u c = &g.rows[row][bit] } - // A key with no state reads back as a delete. With no leaf here there is - // nothing to remove; over a live one see pbinZeroedLeafUpdate. - if update.Deleted() { - if c.kind != pbinNodeLeaf { + // An absent key and a zero-valued one are the same state, so both a delete and + // a value of 32 zero bytes remove the leaf rather than store it. Code length + // therefore comes from code_size, never from which chunks are present. + drop := update.Deleted() + if !drop { + zero, err := pbinLeafValueIsZero(probe, update) + if err != nil { + return err + } + drop = zero + } + if drop { + // A probe shorter than a whole key names a subtree, and dropping it drops + // everything under it. + if c.kind == pbinNodeEmpty { return nil } - zeroed, err := pbinZeroedLeafUpdate(plainKey) - if err != nil { + slot := pph.currentKey + if g.activeRows != 0 { + slot.appendBit(bit) + } + if err := pph.dropSubtreeRecords(c, &slot); err != nil { return err } - update = &zeroed + c.reset() + if g.activeRows == 0 { + pph.rootTouched, pph.rootPresent = true, false + return nil + } + g.touchMap[row] |= uint16(1) << bit + g.afterMap[row] &^= uint16(1) << bit + return nil } if g.activeRows == 0 { @@ -268,20 +300,25 @@ func (pph *PBinPatriciaHashed) updateCell(plainKey []byte, probe *pbinBitpath, u return nil } -// pbinZeroedLeafUpdate reinterprets an absent read over a live leaf. The domain -// encodes zero and absent the same way, while EIP-8297 has no removal and holds -// a zero value as a present leaf, so a zeroed storage slot keeps its leaf at 32 -// zero bytes. An absent account would be a removal the EIP does not define, so -// it stays refused rather than guessed at. -func pbinZeroedLeafUpdate(plainKey []byte) (Update, error) { - if len(plainKey) != length.Addr+length.Hash { - return Update{}, fmt.Errorf("%w: %x", errPBinDeleteUnsupported, plainKey) +// pbinLeafValueIsZero reports whether the leaf at path would hold 32 zero bytes. +// The path is the whole key, so the value is formed the same way the hasher forms +// it and the two cannot drift. +func pbinLeafValueIsZero(path *pbinBitpath, u *Update) (bool, error) { + if path.bitLen%8 != 0 { + return false, fmt.Errorf("pbin: leaf key of %d bits is not whole bytes", path.bitLen) + } + var buf [pbinStorageKeyLength]byte + key := path.appendPackedBits(buf[:0]) + value, err := pbinLeafValue(key, u) + if err != nil { + return false, err } - return Update{Flags: StorageUpdate}, nil + return value == [pbinValueLength]byte{}, nil } // RootHash hashes whatever the root cell holds: a one-key tree's root is the -// leaf itself (eip:133-135) and an empty tree is 32 zero bytes (eip:208), so +// leaf itself (eip:"Tree structure") and an empty tree is 32 zero bytes +// (eip:"Node merkelization"), so // neither shape needs a special case. func (pph *PBinPatriciaHashed) RootHash() ([]byte, error) { if pph.grid.activeRows != 0 { @@ -328,11 +365,11 @@ func (pph *PBinPatriciaHashed) storeRoot() error { // loadRoot reads the stored root cell into the grid; an absent record is the // empty tree. func (pph *PBinPatriciaHashed) loadRoot() error { - pph.rootChecked = true data, _, err := pph.ctx.Branch(pbinRootKey) if err != nil { return fmt.Errorf("pbin: read root cell: %w", err) } + pph.rootChecked = true if len(data) == 0 { pph.rootPrev = []byte{} return nil @@ -416,6 +453,12 @@ func (pph *PBinPatriciaHashed) needUnfolding(probe *pbinBitpath) pbinUnfolding { matched := pbinCommonPrefixBitsAt(probe, depth, &cell.prefix) if matched < cell.prefix.bitLen { + if depth+matched == probe.bitLen { + // The probe ended inside the cell's prefix without diverging: it names + // a subtree wholly containing this node, so the cell itself is the + // probe's slot. Only a subtree drop probes short of a whole key. + return pbinUnfolding{} + } return pbinUnfolding{action: pbinUnfoldSplit, matched: matched} } if cell.kind == pbinNodeLeaf { @@ -676,8 +719,8 @@ func (pph *PBinPatriciaHashed) foldBranch(row int, bit uint64, upDepth, depth in } // foldPropagate collapses a row down to its sole survivor. The node moves up -// rather than being rewritten: no record is written, and the bits the row -// consumed are prepended to the survivor's own prefix. +// rather than being rewritten, so the row's own record describes nothing once +// the bits it consumed are prepended to the survivor's prefix. func (pph *PBinPatriciaHashed) foldPropagate(row int, bit uint64, upDepth, depth int16, upCell *pbinCell) error { g := &pph.grid pph.propagateTouch(row, bit) @@ -693,7 +736,7 @@ func (pph *PBinPatriciaHashed) foldPropagate(row int, bit uint64, upDepth, depth return fmt.Errorf("pbin: propagate at row %d formed a %d-bit prefix, want %d", row, upCell.prefix.bitLen, want) } pph.rehashAfterPrefixChange(upCell) - return nil + return pph.deleteRowRecord(row) } // foldDelete drops a row that kept nothing, taking the record it came from with @@ -709,11 +752,71 @@ func (pph *PBinPatriciaHashed) foldDelete(row int, bit uint64, upCell *pbinCell) } } upCell.reset() - if !g.branchBefore[row] { + return pph.deleteRowRecord(row) +} + +// pbinDerivedContext marks a context that derives its branch records from a node +// set instead of storing them. A decoded witness is the only one, and it carries +// no node the proof paths did not need. +type pbinDerivedContext interface{ pbinRecordsAreDerived() } + +// dropSubtreeRecords deletes the stored records under a cell a subtree drop +// discards. Unfolding stops at the drop probe, so no fold ever reaches them and +// nothing else reclaims them — commitment pruning goes by step, not by +// reachability. A derived context stores nothing to reclaim and would refuse the +// preimages the sweep asks it for. +func (pph *PBinPatriciaHashed) dropSubtreeRecords(c *pbinCell, slot *pbinBitpath) error { + if c.kind != pbinNodeBranch { + return nil + } + if _, derived := pph.ctx.(pbinDerivedContext); derived { + return nil + } + + head := *slot + head.append(&c.prefix) + pending := []pbinBitpath{head} + var cells [2]pbinCell + for len(pending) > 0 { + path := pending[len(pending)-1] + pending = pending[:len(pending)-1] + + key := pbinEncodeBitPath(&path) + data, _, err := pph.ctx.Branch(key) + if err != nil { + return fmt.Errorf("pbin: read branch at %x: %w", key, err) + } + if len(data) == 0 { + return fmt.Errorf("%w at %x (%d bits)", errPBinMissingBranch, key, path.bitLen) + } + _, afterMap, err := pbinDecodeBranch(data, &cells) + if err != nil { + return fmt.Errorf("pbin: decode branch at %x: %w", key, err) + } + for bit := range cells { + if afterMap&(uint16(1)< 0 && !c.loaded.account() { plainKey := c.accountAddr[:c.accountAddrLen] @@ -785,11 +909,9 @@ func (pph *PBinPatriciaHashed) loadCellState(c *pbinCell) error { return fmt.Errorf("pbin: read storage %x: %w", plainKey, err) } if update.Deleted() { - zeroed, err := pbinZeroedLeafUpdate(plainKey) - if err != nil { - return err - } - update = &zeroed + // A stored leaf whose state reads absent: the record outlived the value. + // Carry the zero it stands for; the update path is what removes leaves. + update = &Update{Flags: StorageUpdate} } c.setFromUpdate(update) c.loaded = c.loaded.addFlag(cellLoadStorage) @@ -802,6 +924,13 @@ func (pph *PBinPatriciaHashed) loadCellState(c *pbinCell) error { // its record, so the record key is the cell's path followed by that prefix. func (pph *PBinPatriciaHashed) materializeBranch(c *pbinCell, path *pbinBitpath) error { nodeKey := *path + // A prefix decoded from a witness is bounded on its own, not against the depth + // it was reached at, so the sum can overflow where append would panic. A branch + // landing exactly on the limit is out too: its children need one bit more. + if int(nodeKey.bitLen)+int(c.prefix.bitLen) >= pbinMaxPathBits { + return fmt.Errorf("%w: branch at %d bits with a %d-bit prefix overflows the path", + errPBinCellHash, nodeKey.bitLen, c.prefix.bitLen) + } nodeKey.append(&c.prefix) key := pbinEncodeBitPath(&nodeKey) diff --git a/execution/commitment/pbin_process_test.go b/execution/commitment/pbin_process_test.go index 95d01efbb10..7d55c1062a6 100644 --- a/execution/commitment/pbin_process_test.go +++ b/execution/commitment/pbin_process_test.go @@ -68,47 +68,101 @@ func (c *pbinTestCorpus) storage(addr, slot []byte, value ...byte) *pbinTestCorp return c } -// entries is the leaf set the corpus stands for. An account is two leaves — -// stated here independently of the engine. +func (c *pbinTestCorpus) remove(addr []byte) *pbinTestCorpus { + c.plainKeys = append(c.plainKeys, bytes.Clone(addr)) + c.updates = append(c.updates, Update{Flags: DeleteUpdate}) + return c +} + +// entries is the leaf set the corpus stands for as a single batch. func (c *pbinTestCorpus) entries(t *testing.T) []pbinOracleEntry { t.Helper() - entries := make([]pbinOracleEntry, 0, len(c.plainKeys)) - for i, plainKey := range c.plainKeys { - u := &c.updates[i] - switch len(plainKey) { - case length.Addr: - basic, err := pbinEncodeBasicData(u.Nonce, &u.Balance, u.CodeSize) - require.NoError(t, err) - code := pbinCodeHashValue(u.CodeHash) - entries = append(entries, - pbinOracleEntry{key: pbinTreeKeyAccount(plainKey, pbinBasicDataLeafKey), value: basic[:]}, - pbinOracleEntry{key: pbinTreeKeyAccount(plainKey, pbinCodeHashLeafKey), value: code[:]}) - for j, chunk := range pbinChunkifyCode(c.codes[string(plainKey)]) { - entries = append(entries, pbinOracleEntry{ - key: pbinTestChunkKey(plainKey, u.CodeHash, j), - value: chunk[:], - }) + return pbinTestFinalEntries(t, c) +} + +// pbinTestFinalEntries is the leaf set the batches leave behind, stated +// independently of the engine. Within a batch only a plain key's last update +// counts, because the engine re-reads post-state; an account removal drops the +// header and storage leaves derived from the address, while content-addressed +// chunk leaves stay once a materialized account inserted them. A value of 32 +// zero bytes is the same state as an absent key, so it removes the leaf. An +// account holds exactly one of the CODE_HASH and DELEGATION leaves, decided by +// its code bytes. +func pbinTestFinalEntries(t *testing.T, batches ...*pbinTestCorpus) []pbinOracleEntry { + t.Helper() + var zero [pbinValueLength]byte + var order []string + values := make(map[string][]byte) + owners := make(map[string]string) + set := func(key []byte, value [pbinValueLength]byte, owner []byte) { + k := string(key) + if _, seen := values[k]; !seen { + order = append(order, k) + } + if value == zero { + values[k] = nil + } else { + values[k] = bytes.Clone(value[:]) + } + if owner != nil { + owners[k] = string(owner) + } + } + for _, b := range batches { + last := make(map[string]int, len(b.plainKeys)) + for i, plainKey := range b.plainKeys { + last[string(plainKey)] = i + } + // Removals first: an account's header stem sorts before every other key + // it owns, so its drop always lands before the batch's re-inserts. + for i, plainKey := range b.plainKeys { + if last[string(plainKey)] != i || len(plainKey) != length.Addr || !b.updates[i].Deleted() { + continue + } + for k, owner := range owners { + if owner == string(plainKey) { + values[k] = nil + } + } + } + for i, plainKey := range b.plainKeys { + if last[string(plainKey)] != i { + continue + } + u := &b.updates[i] + switch len(plainKey) { + case length.Addr: + if u.Deleted() { + continue + } + basic, err := pbinEncodeBasicData(u.Nonce, &u.Balance, u.CodeSize) + require.NoError(t, err) + set(pbinTreeKeyAccount(plainKey, pbinBasicDataLeafKey), basic, plainKey) + if code := b.codes[string(plainKey)]; pbinIsDelegation(code) { + set(pbinTreeKeyAccount(plainKey, pbinDelegationLeafKey), pbinEncodeDelegation(code), plainKey) + set(pbinTreeKeyAccount(plainKey, pbinCodeHashLeafKey), zero, plainKey) + } else { + set(pbinTreeKeyAccount(plainKey, pbinCodeHashLeafKey), pbinCodeHashValue(u.CodeHash), plainKey) + set(pbinTreeKeyAccount(plainKey, pbinDelegationLeafKey), zero, plainKey) + for j, chunk := range pbinChunkifyCode(code) { + set(pbinTreeKeyCodeChunk(u.CodeHash, j), chunk, nil) + } + } + case length.Addr + length.Hash: + set(pbinTreeKeyStorage(plainKey[:length.Addr], plainKey[length.Addr:]), + pbinEncodeStorageValue(u.Storage[:u.StorageLen]), plainKey[:length.Addr]) + default: + t.Fatalf("plain key of %d bytes is neither an account nor a storage key", len(plainKey)) } - case length.Addr + length.Hash: - value := pbinEncodeStorageValue(u.Storage[:u.StorageLen]) - entries = append(entries, pbinOracleEntry{ - key: pbinTreeKeyStorage(plainKey[:length.Addr], plainKey[length.Addr:]), - value: value[:], - }) - default: - t.Fatalf("plain key of %d bytes is neither an account nor a storage key", len(plainKey)) } } - return entries -} - -// pbinTestChunkKey: header chunks live in the account's own stem, the rest in -// the content-addressed code zone. -func pbinTestChunkKey(addr []byte, codeHash common.Hash, chunkID int) []byte { - if chunkID < pbinHeaderCodeChunks { - return pbinTreeKeyCodeChunk(addr, chunkID) + entries := make([]pbinOracleEntry, 0, len(order)) + for _, k := range order { + if values[k] != nil { + entries = append(entries, pbinOracleEntry{key: []byte(k), value: values[k]}) + } } - return pbinTreeKeyCodeOverflow(codeHash, chunkID) + return entries } func (c *pbinTestCorpus) oracleRoot(t *testing.T) []byte { @@ -146,7 +200,7 @@ func pbinTestProcess(t *testing.T, pph *PBinPatriciaHashed, plainKeys [][]byte, } // TestPBinRootHashEmptyEngine: an empty EIP-8297 tree is 32 zero bytes -// (eip:208), not the empty-MPT root the rest of erigon reaches for. +// (eip:"Node merkelization"), not the empty-MPT root the rest of erigon reaches for. func TestPBinRootHashEmptyEngine(t *testing.T) { t.Parallel() @@ -157,7 +211,7 @@ func TestPBinRootHashEmptyEngine(t *testing.T) { require.NotEqual(t, empty.RootHash[:], root) } -// TestPBinProcessSingleKeyRootIsLeaf pins eip:133-135: with one entry the root +// TestPBinProcessSingleKeyRootIsLeaf pins eip:"Tree structure": with one entry the root // is the leaf itself, not a branch wrapping it. func TestPBinProcessSingleKeyRootIsLeaf(t *testing.T) { t.Parallel() @@ -304,10 +358,9 @@ func TestPBinProcessAccountFansOutToCodeHash(t *testing.T) { require.Equal(t, codeHash[:], code[:]) } -// TestPBinProcessRejectsStreamDelete: EIP-8297 never removes an entry, so a -// delete arriving on the update stream is an error rather than a silently -// applied removal. -func TestPBinProcessRejectsStreamDelete(t *testing.T) { +// TestPBinProcessStreamDeleteOnAbsentKeyIsNoop: a delete for a key that has no +// leaf removes nothing, so the tree it leaves is the empty one. +func TestPBinProcessStreamDeleteOnAbsentKeyIsNoop(t *testing.T) { t.Parallel() pph, ms := pbinTestEngine(t) @@ -316,8 +369,9 @@ func TestPBinProcessRejectsStreamDelete(t *testing.T) { require.NoError(t, ms.applyPlainUpdates(plainKeys, updates)) upd := WrapKeyUpdates(t, ModeUpdate, pbinKeyHasher(), plainKeys, updates) - _, err := pph.Process(context.Background(), upd, "", nil, WarmupConfig{}) - require.ErrorIs(t, err, errPBinDeleteUnsupported) + root, err := pph.Process(context.Background(), upd, "", nil, WarmupConfig{}) + require.NoError(t, err) + require.Equal(t, make([]byte, length.Hash), root) } // TestPBinProcessMissingStateIsAbsent: a context read for a key with no state diff --git a/execution/commitment/pbin_reclaim_test.go b/execution/commitment/pbin_reclaim_test.go new file mode 100644 index 00000000000..39ca8ebd06e --- /dev/null +++ b/execution/commitment/pbin_reclaim_test.go @@ -0,0 +1,125 @@ +// Copyright 2026 The Erigon Authors +// This file is part of Erigon. +// +// Erigon is free software: you can redistribute it and/or modify +// it under the terms of the GNU Lesser General Public License as published by +// the Free Software Foundation, either version 3 of the License, or +// (at your option) any later version. +// +// Erigon is distributed in the hope that it will be useful, +// but WITHOUT ANY WARRANTY; without even the implied warranty of +// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +// GNU Lesser General Public License for more details. +// +// You should have received a copy of the GNU Lesser General Public License +// along with Erigon. If not, see . + +package commitment + +import ( + "bytes" + "context" + "testing" + + keccak "github.com/erigontech/fastkeccak" + "github.com/stretchr/testify/require" + + "github.com/erigontech/erigon/common" +) + +// Code reclamation on account removal. A removed account's chunk leaves are +// dropped only if they were absent from the parent state and no account in the +// batch's post-state holds the code hash — and both together mean the leaves +// were never inserted, since an account created and destroyed inside one batch +// merges to a bare deletion before the stream sees it. So the engine keeps +// every chunk leaf it holds, and these tests pin the three directions of that +// rule. + +// pbinMergedRemoval is the update an in-batch create-and-destroy merges to: a +// bare deletion still carrying the code fields the create touched. The stream +// must treat it as codeless — the account's code is gone from the code domain. +func pbinMergedRemoval(code []byte) Update { + return Update{Flags: DeleteUpdate, CodeHash: keccak.Sum256(code), CodeSize: uint64(len(code))} +} + +func pbinTestProcessMerged(t *testing.T, pph *PBinPatriciaHashed, plainKeys [][]byte, updates []Update) []byte { + t.Helper() + upd := WrapKeyUpdates(t, ModeUpdate, pbinKeyHasher(), plainKeys, updates) + root, err := pph.Process(context.Background(), upd, "", nil, WarmupConfig{}) + require.NoError(t, err) + return root +} + +func pbinTestChunkEntries(entries []pbinOracleEntry, code []byte) []pbinOracleEntry { + codeHash := keccak.Sum256(code) + for i, chunk := range pbinChunkifyCode(code) { + entries = append(entries, pbinOracleEntry{key: pbinTreeKeyCodeChunk(codeHash, i), value: bytes.Clone(chunk[:])}) + } + return entries +} + +func TestPBinReclaimDropsCodeWithNoSurvivor(t *testing.T) { + t.Parallel() + + bystander := pbinOracleAddr(91) + code := bytes.Repeat([]byte{0x5B}, 31*3) + stored := new(pbinTestCorpus).account(bystander, 1, 2, common.Hash{0x91}) + + pph, ms := pbinTestEngine(t) + require.NoError(t, ms.applyPlainUpdates(stored.plainKeys, stored.updates)) + + plainKeys := append([][]byte{pbinOracleAddr(92)}, stored.plainKeys...) + updates := append([]Update{pbinMergedRemoval(code)}, stored.updates...) + root := pbinTestProcessMerged(t, pph, plainKeys, updates) + + require.Equal(t, stored.oracleRoot(t), root, + "a code deployed and destroyed inside the batch leaves no chunk leaf") + + withChunks := pbinOracleRoot(pbinTestChunkEntries(stored.entries(t), code)) + require.NotEqual(t, withChunks[:], root, "keeping the dead code's chunks must change the root") +} + +func TestPBinReclaimKeepsCodeForBatchSibling(t *testing.T) { + t.Parallel() + + sibling := pbinOracleAddr(93) + code := bytes.Repeat([]byte{0x5B}, 31*3) + survivors := new(pbinTestCorpus).accountWithCodeBytes(sibling, 1, 5, code) + + pph, ms := pbinTestEngine(t) + survivors.applyTo(t, ms) + + plainKeys := append([][]byte{pbinOracleAddr(94)}, survivors.plainKeys...) + updates := append([]Update{pbinMergedRemoval(code)}, survivors.updates...) + root := pbinTestProcessMerged(t, pph, plainKeys, updates) + + require.Equal(t, survivors.oracleRoot(t), root, + "a sibling written in the same batch keeps the shared chunk set") + + noChunks := new(pbinTestCorpus).accountWithCode(sibling, 1, 5, keccak.Sum256(code), uint64(len(code))) + require.NotEqual(t, noChunks.oracleRoot(t), root, "the surviving holder's chunks must stay in the tree") +} + +// TestPBinReclaimKeepsCodeForPreexistingHolder is the case a referenced-set +// rule gets wrong: the code hash never appears in the removal batch except on +// the deletion itself, and the untouched holder's leaves must survive. +func TestPBinReclaimKeepsCodeForPreexistingHolder(t *testing.T) { + t.Parallel() + + holder := pbinOracleAddr(95) + code := bytes.Repeat([]byte{0x5B}, 31*3) + stored := new(pbinTestCorpus).accountWithCodeBytes(holder, 2, 9, code) + + pph, ms := pbinTestEngine(t) + stored.applyTo(t, ms) + before := pbinTestProcess(t, pph, stored.plainKeys, stored.updates) + + pph.Reset() + root := pbinTestProcessMerged(t, pph, [][]byte{pbinOracleAddr(96)}, []Update{pbinMergedRemoval(code)}) + + require.Equal(t, before, root, "an untouched pre-existing holder keeps its code") + require.Equal(t, stored.oracleRoot(t), root) + + noChunks := new(pbinTestCorpus).accountWithCode(holder, 2, 9, keccak.Sum256(code), uint64(len(code))) + require.NotEqual(t, noChunks.oracleRoot(t), root, "dropping the holder's chunks must change the root") +} diff --git a/execution/commitment/pbin_specengine_test.go b/execution/commitment/pbin_specengine_test.go index 34729cd5712..f9b4d212a9f 100644 --- a/execution/commitment/pbin_specengine_test.go +++ b/execution/commitment/pbin_specengine_test.go @@ -69,7 +69,7 @@ func pbinLeafFromVector(key, value []byte, seq int) pbinEngineLeaf { l.update.Flags = CodeUpdate l.update.CodeHash = common.BytesToHash(value) return l - case sub >= pbinHeaderStorageOffset && sub < pbinCodeOffset: + case sub >= pbinHeaderStorageOffset && sub < pbinHeaderStorageOffset+pbinHeaderStorageSlots: storageLeaf() return l default: diff --git a/execution/commitment/pbin_specroots_test.go b/execution/commitment/pbin_specroots_test.go index adbdd10c50a..9b4e3b2386b 100644 --- a/execution/commitment/pbin_specroots_test.go +++ b/execution/commitment/pbin_specroots_test.go @@ -20,9 +20,9 @@ func pbinBlake3Sum(b []byte) [32]byte { return blake3.Sum256(b) } var pbinBlake3Hash pbinHashFn = func(b []byte) common.Hash { return common.Hash(blake3.Sum256(b)) } -// pbinOracleRootOf rebuilds the oracle trie from the whole key set. A delete is -// applied the same way — the EIP's insert has no removal — so nothing here -// depends on a delete algorithm. +// pbinOracleRootOf rebuilds the oracle trie from the whole key set, so a removed +// key is simply one the set no longer holds and nothing here depends on an +// incremental delete algorithm. func pbinOracleRootOf(t *testing.T, entries map[string][]byte) [32]byte { t.Helper() keys := make([]string, 0, len(entries)) diff --git a/execution/commitment/pbin_storage_layout_test.go b/execution/commitment/pbin_storage_layout_test.go new file mode 100644 index 00000000000..355bccb99d7 --- /dev/null +++ b/execution/commitment/pbin_storage_layout_test.go @@ -0,0 +1,172 @@ +package commitment + +// Measures what EIP-8297's storage layout costs and what its co-location buys. +// +// The embedding splits storage three ways: slots 0..63 sit in the account header +// under 34-byte keys sharing the account's stem; slots from 64 on go to the +// storage zone under 66-byte keys carrying BOTH the account stem digest and a +// per-256-slot group digest; and slots inside one group share that stem, which is +// the co-location the design is built around. +// +// Each pattern below writes the same NUMBER of slots to the same account, so the +// only thing that varies is where the embedding puts them. + +import ( + "context" + "encoding/binary" + "fmt" + "testing" + + "github.com/stretchr/testify/require" +) + +// pbinSlotAt returns a 32-byte big-endian slot number. +func pbinSlotAt(n uint64) []byte { + var s [32]byte + binary.BigEndian.PutUint64(s[24:], n) + return s[:] +} + +func TestPBinStorageLayoutCost(t *testing.T) { + t.Parallel() + + const slots = 16 + addr := pbinOracleAddr(3) + + patterns := []struct { + name string + slot func(i int) uint64 + }{ + // 0..15: account header, 34-byte keys, one shared stem. + {"header slots 0..15", func(i int) uint64 { return uint64(i) }}, + // 64..79: storage zone, one group (64/256 == 79/256 == 0), shared group stem. + {"one group, adjacent 64..79", func(i int) uint64 { return 64 + uint64(i) }}, + // 256 apart: every slot lands in its own group, so no group stem is shared. + {"one slot per group, 256 apart", func(i int) uint64 { return 256 * uint64(i+1) }}, + // Far apart: distinct groups and distinct high bits, the worst case for sharing. + {"scattered across the zone", func(i int) uint64 { return 1 << (uint(i) + 20) }}, + } + + type row struct { + name string + nodes, total int + leafKey, branch int + leaves, branches int + } + rows := make([]row, 0, len(patterns)) + + for _, p := range patterns { + c := new(pbinTestCorpus).account(addr, 1, 100, pbinTestCodeHash(0)) + for i := range slots { + c = c.storage(addr, pbinSlotAt(p.slot(i)), byte(i+1)) + } + + rec := &pbinWitnessRecorder{} + _, pph := pbinWitnessProcess(t, c, rec) + defer pph.Release() + + r := row{name: p.name} + for _, n := range rec.byHash(t) { + r.total += len(n) + r.nodes++ + switch n[0] { + case pbinLeafTag: + r.leaves++ + r.leafKey += len(n) - 1 - pbinValueLength + case pbinBranchTag: + r.branches++ + r.branch += len(n) + default: + t.Fatalf("unknown tag %#x", n[0]) + } + } + rows = append(rows, r) + } + + t.Logf("%d storage slots on one account, by where the embedding puts them:", slots) + t.Logf("%-32s %6s %8s %8s %8s %7s %9s", "pattern", "nodes", "bytes", "leafkey", "branch", "leaves", "key/leaf") + for _, r := range rows { + t.Logf("%-32s %6d %8d %8d %8d %7d %9.1f", + r.name, r.nodes, r.total, r.leafKey, r.branch, r.leaves, + float64(r.leafKey)/float64(max(r.leaves, 1))) + } + + base := rows[0].total + for _, r := range rows[1:] { + t.Logf(" %-30s %.2fx the header-slot case", r.name, float64(r.total)/float64(base)) + } + + // The header window is 64 slots wide, so slot 63 is a 34-byte key and slot 64 + // is a 66-byte one — the embedding's sharpest discontinuity. + require.Less(t, rows[0].leafKey, rows[1].leafKey, + "header slots must carry less key material than storage-zone slots") +} + +// TestPBinStorageGroupSharing isolates co-location: the same 16 slots, once packed +// into one group and once spread one-per-group. Both are storage-zone keys of the +// same width, so any difference is the shared group stem alone. +// +// Two things this has to get right, both of which were got wrong first: +// +// - Measure the PRUNED witness. Witnesses returns a superset that callers prune +// with PBinWitnessNodesForKeys; the superset carries off-path siblings re-hashed +// during the fold, and counting those reverses the sign of the result. +// - Vary the right axis. Other accounts' storage diverges above this account's +// stem and cancels in the difference, so filler on other accounts cannot move +// the number. What matters is how many OTHER groups this account already holds. +func TestPBinStorageGroupSharing(t *testing.T) { + t.Parallel() + + const slots = 16 + addr := pbinOracleAddr(5) + + // Two passes. The first builds the whole tree so the branch records exist; the + // second proves ONLY the 16 slots. Measuring the first pass would include every + // filler account and hide exactly the effect under test. + measure := func(filler int, step uint64) (nodes, total int) { + // Filler is untouched storage on the SAME account: that is the axis the + // co-location property is about. Other accounts' keys diverge above this + // account's stem and cancel between the two arms. + full := new(pbinTestCorpus).account(addr, 1, 100, pbinTestCodeHash(0)) + for i := range filler { + full = full.storage(addr, pbinSlotAt(1<<20+256*uint64(i)), 0x7f) + } + touched := new(pbinTestCorpus) + for i := range slots { + full = full.storage(addr, pbinSlotAt(64+step*uint64(i)), byte(i+1)) + touched = touched.storage(addr, pbinSlotAt(64+step*uint64(i)), byte(i+1)) + } + + pph, ms := pbinTestEngine(t) + defer pph.Release() + full.applyTo(t, ms) + pbinTestProcess(t, pph, full.plainKeys, full.updates) + + pph.Reset() + upd := WrapKeyUpdates(t, ModeDirect, pbinKeyHasher(), touched.plainKeys, touched.updates) + got, proved, root, err := pph.Witnesses(context.Background(), upd, false, "") + require.NoError(t, err) + lean, err := PBinWitnessNodesForKeys(got, root, proved) + require.NoError(t, err) + for _, n := range lean { + total += len(n) + nodes++ + } + return nodes, total + } + + t.Logf("%8s %10s %10s %12s %10s", "other grps", "adjacent", "per-group", "co-loc saves", "of total") + for _, filler := range []int{0, 16, 64, 256, 1024, 4096} { + adjN, adjB := measure(filler, 1) + sepN, sepB := measure(filler, 256) + t.Logf("%8d %6d/%4dB %6d/%4dB %10dB %9.1f%%", + filler, adjN, adjB, sepN, sepB, sepB-adjB, 100*float64(sepB-adjB)/float64(sepB)) + } +} + +func pbinTestCodeHash(n byte) (h [32]byte) { + h[31] = n + return h +} + +var _ = fmt.Sprintf diff --git a/execution/commitment/pbin_unfold_test.go b/execution/commitment/pbin_unfold_test.go index 3a598f942a7..5035c33bcb2 100644 --- a/execution/commitment/pbin_unfold_test.go +++ b/execution/commitment/pbin_unfold_test.go @@ -263,7 +263,7 @@ func TestPBinUnfoldEmptyRoot(t *testing.T) { // The divergence bit walks both word boundaries of the [9]uint64 path. A split // moves the node below one level down and re-cuts its prefix, dropping the bit -// the new row branches on (eip:174-176). +// the new row branches on (eip:"Insertion and deletion"). func TestPBinUnfoldSplitsInsidePrefix(t *testing.T) { t.Parallel() diff --git a/execution/commitment/pbin_update_stream.go b/execution/commitment/pbin_update_stream.go index 2ce3416800e..ae967650f7a 100644 --- a/execution/commitment/pbin_update_stream.go +++ b/execution/commitment/pbin_update_stream.go @@ -22,6 +22,9 @@ import ( "fmt" "slices" + keccak "github.com/erigontech/fastkeccak" + + "github.com/erigontech/erigon/common" "github.com/erigontech/erigon/common/length" ) @@ -31,19 +34,21 @@ type pbinUpdateStream struct { state PatriciaContext emit pbinUpdateSink - siblingKey [pbinAccountKeyLength]byte - pendingCode pbinPendingCode - overflowCode []pbinOverflowChunk - keyDigest pbinDigestCache -} + siblingKey [pbinAccountKeyLength]byte + codeChunks []pbinCodeChunk + keyDigest pbinDigestCache + + // witness is what the parent state cannot tell a witness pass about the block. + // See chunkSource and removesAccount. + witness PBinWitnessBlock + witnessPass bool -type pbinPendingCode struct { - stem [pbinAccountKeyLength - 1]byte - plainKey [length.Addr]byte - chunks [][pbinValueLength]byte + // pendingRemoval holds storage-subtree prefixes waiting for the walk to reach + // their zone. Removals are queued in account order, which is prefix order. + pendingRemoval [][]byte } -type pbinOverflowChunk struct { +type pbinCodeChunk struct { key [pbinCodeKeyLength]byte value [pbinValueLength]byte } @@ -68,10 +73,10 @@ func (s *pbinUpdateStream) process(ctx context.Context, updates *Updates, state if err != nil { return processed, err } - if err = s.flushPendingCode(); err != nil { + if err = s.flushCodeChunks(); err != nil { return processed, err } - if err = s.flushOverflowCode(); err != nil { + if err = s.flushRemovals(nil); err != nil { return processed, err } return processed, nil @@ -79,25 +84,23 @@ func (s *pbinUpdateStream) process(ctx context.Context, updates *Updates, state func (s *pbinUpdateStream) reset() { s.state, s.emit = nil, nil - s.pendingCode = pbinPendingCode{} - s.overflowCode = s.overflowCode[:0] + s.codeChunks = s.codeChunks[:0] + s.pendingRemoval = s.pendingRemoval[:0] } func (s *pbinUpdateStream) release() { s.reset() s.keyDigest = pbinDigestCache{} + s.witness = PBinWitnessBlock{} } -// processKey expands an account into its basic-data and code-hash leaves. Code -// chunks are delayed until emitting them cannot move the ordered trie walk back. +// processKey expands an account into its header leaves. Code chunks are delayed +// until emitting them cannot move the ordered trie walk back. func (s *pbinUpdateStream) processKey(treeKey, plainKey []byte, stateUpdate *Update) error { - if stateUpdate != nil && stateUpdate.Deleted() { - return fmt.Errorf("%w: update for %x", errPBinDeleteUnsupported, plainKey) - } - if err := s.flushPendingCodeBefore(treeKey); err != nil { + if err := s.flushCodeChunksBefore(treeKey); err != nil { return err } - if err := s.flushOverflowCodeBefore(treeKey); err != nil { + if err := s.flushRemovalsBefore(treeKey); err != nil { return err } update := stateUpdate @@ -107,83 +110,164 @@ func (s *pbinUpdateStream) processKey(treeKey, plainKey []byte, stateUpdate *Upd return err } } + if len(plainKey) == length.Addr && s.removesAccount(plainKey, update) { + if err := s.removeAccount(plainKey); err != nil { + return err + } + } if err := s.emit(treeKey, plainKey, update); err != nil { return err } if len(plainKey) != length.Addr { return nil } - codeKey, err := s.codeHashKey(treeKey) + return s.emitCodeLeaves(treeKey, plainKey, update) +} + +// emitCodeLeaves writes the header sibling the account's code selects — +// CODE_HASH, or DELEGATION for an EIP-7702 indicator — and removes the other. +// The stream is told nothing about what the account held before, so both +// removals are unconditional. The indicator is no account field, so its leaf +// carries the value itself and no plain key. +func (s *pbinUpdateStream) emitCodeLeaves(basicDataKey, plainKey []byte, update *Update) error { + code, codeHash, err := s.chunkSource(plainKey, update) if err != nil { return err } - if err = s.emit(codeKey, plainKey, update); err != nil { + if pbinIsDelegation(code) { + if err := s.emitSibling(basicDataKey, pbinCodeHashLeafKey, plainKey, &Update{Flags: DeleteUpdate}); err != nil { + return err + } + indicator := Update{Flags: StorageUpdate, StorageLen: pbinValueLength, Storage: pbinEncodeDelegation(code)} + return s.emitSibling(basicDataKey, pbinDelegationLeafKey, nil, &indicator) + } + if err := s.emitSibling(basicDataKey, pbinCodeHashLeafKey, plainKey, update); err != nil { + return err + } + if err := s.emitSibling(basicDataKey, pbinDelegationLeafKey, plainKey, &Update{Flags: DeleteUpdate}); err != nil { + return err + } + s.queueChunks(code, codeHash) + return nil +} + +// removesAccount reports whether the block removes this account. A witness pass +// reads the parent state, where an account the block creates is absent too, so +// there it has to be told rather than infer it. +func (s *pbinUpdateStream) removesAccount(plainKey []byte, update *Update) bool { + if s.witnessPass { + _, removed := s.witness.Removed[string(plainKey)] + return removed + } + return update.Deleted() +} + +// removeAccount drops the two subtrees an account owns — its header stem, and +// its storage prefix once the walk reaches that zone — rather than the leaves it +// holds, which for storage nothing enumerates. Code chunks always stay, which +// parts from the reference suite when the removed account was the sole holder; +// EIP-6780 bounds that to states the chain cannot reach, since an account +// deleted with its code was created in the same transaction and a +// create-and-destroy merges to a bare deletion that inserts no chunk +// (eip:"Zero values and deletion"). +func (s *pbinUpdateStream) removeAccount(plainKey []byte) error { + drop := Update{Flags: DeleteUpdate} + if err := s.emit(s.keyDigest.accountHeaderStem(plainKey), plainKey, &drop); err != nil { return err } - return s.queueCode(treeKey, plainKey, update) + s.pendingRemoval = append(s.pendingRemoval, s.keyDigest.accountStoragePrefix(plainKey)) + return nil } -func (s *pbinUpdateStream) queueCode(basicDataKey, plainKey []byte, update *Update) error { - if update.CodeSize == 0 { +func (s *pbinUpdateStream) flushRemovalsBefore(treeKey []byte) error { + if len(s.pendingRemoval) == 0 || treeKey[0] < pbinStorageZone { return nil } - if len(s.pendingCode.chunks) != 0 { - return fmt.Errorf("pbin: code for %x queued while %x is still pending: the stem exit was missed", - plainKey, s.pendingCode.plainKey[:]) + return s.flushRemovals(treeKey) +} + +// flushRemovals emits the queued storage-prefix drops that sort before upTo, or +// all of them when upTo is nil. A drop has to land before any storage key it +// covers; a queue out of order would fail the engine's ascending-visit check +// rather than pass silently. +func (s *pbinUpdateStream) flushRemovals(upTo []byte) error { + drop := Update{Flags: DeleteUpdate} + sent := 0 + for _, prefix := range s.pendingRemoval { + if upTo != nil && bytes.Compare(prefix, upTo) >= 0 { + break + } + if err := s.emit(prefix, nil, &drop); err != nil { + return err + } + sent++ + } + s.pendingRemoval = append(s.pendingRemoval[:0], s.pendingRemoval[sent:]...) + return nil +} + +// chunkSource is the code an account's chunk keys derive from, with the hash +// addressing its chunks. A witness pass walks the parent state, where a +// contract the block creates has no code, so it needs the override to reach the +// same keys the fold did. Only key derivation moves; values stay pre-state. +// A deletion's code fields are whatever the batch merge left behind, not +// state, so a removed account is codeless here. +func (s *pbinUpdateStream) chunkSource(plainKey []byte, update *Update) ([]byte, common.Hash, error) { + if code, ok := s.witness.Code[string(plainKey)]; ok && s.witnessPass { + return code, common.Hash(keccak.Sum256(code)), nil + } + if update.Deleted() || update.CodeSize == 0 { + return nil, common.Hash{}, nil } code, err := s.codeOf(plainKey) if err != nil { - return err + return nil, common.Hash{}, err } if uint64(len(code)) != update.CodeSize { - return fmt.Errorf("pbin: account %x says %d code bytes, the code domain holds %d", + return nil, common.Hash{}, fmt.Errorf("pbin: account %x says %d code bytes, the code domain holds %d", plainKey, update.CodeSize, len(code)) } - chunks := pbinChunkifyCode(code) - if len(chunks) > pbinHeaderCodeChunks { - for i := pbinHeaderCodeChunks; i < len(chunks); i++ { - var oc pbinOverflowChunk - copy(oc.key[:], s.keyDigest.codeOverflowKey(update.CodeHash, i)) - oc.value = chunks[i] - s.overflowCode = append(s.overflowCode, oc) - } - chunks = chunks[:pbinHeaderCodeChunks] + return code, update.CodeHash, nil +} + +func (s *pbinUpdateStream) queueChunks(code []byte, codeHash common.Hash) { + for i, chunk := range pbinChunkifyCode(code) { + var cc pbinCodeChunk + copy(cc.key[:], s.keyDigest.codeChunkKey(codeHash, i)) + cc.value = chunk + s.codeChunks = append(s.codeChunks, cc) } - s.pendingCode.chunks = chunks - copy(s.pendingCode.stem[:], basicDataKey) - copy(s.pendingCode.plainKey[:], plainKey) - return nil } -func (s *pbinUpdateStream) flushOverflowCodeBefore(treeKey []byte) error { - if len(s.overflowCode) == 0 || treeKey[0] <= pbinCodeZone { +func (s *pbinUpdateStream) flushCodeChunksBefore(treeKey []byte) error { + if len(s.codeChunks) == 0 || treeKey[0] <= pbinCodeZone { return nil } - return s.flushOverflowCode() + return s.flushCodeChunks() } -func (s *pbinUpdateStream) flushOverflowCode() error { - if len(s.overflowCode) == 0 { +func (s *pbinUpdateStream) flushCodeChunks() error { + if len(s.codeChunks) == 0 { return nil } - slices.SortFunc(s.overflowCode, func(a, b pbinOverflowChunk) int { return bytes.Compare(a.key[:], b.key[:]) }) + slices.SortFunc(s.codeChunks, func(a, b pbinCodeChunk) int { return bytes.Compare(a.key[:], b.key[:]) }) - var prev *pbinOverflowChunk - for i := range s.overflowCode { - oc := &s.overflowCode[i] - if prev != nil && oc.key == prev.key { - if oc.value != prev.value { - return fmt.Errorf("pbin: code chunk %x carries two values", oc.key[:]) + var prev *pbinCodeChunk + for i := range s.codeChunks { + cc := &s.codeChunks[i] + if prev != nil && cc.key == prev.key { + if cc.value != prev.value { + return fmt.Errorf("pbin: code chunk %x carries two values", cc.key[:]) } continue } - update := Update{Flags: StorageUpdate, StorageLen: pbinValueLength, Storage: oc.value} - if err := s.emit(oc.key[:], nil, &update); err != nil { + update := Update{Flags: StorageUpdate, StorageLen: pbinValueLength, Storage: cc.value} + if err := s.emit(cc.key[:], nil, &update); err != nil { return err } - prev = oc + prev = cc } - s.overflowCode = s.overflowCode[:0] + s.codeChunks = s.codeChunks[:0] return nil } @@ -200,28 +284,6 @@ func (s *pbinUpdateStream) codeOf(plainKey []byte) ([]byte, error) { return code, nil } -func (s *pbinUpdateStream) flushPendingCodeBefore(treeKey []byte) error { - if len(s.pendingCode.chunks) == 0 || bytes.HasPrefix(treeKey, s.pendingCode.stem[:]) { - return nil - } - return s.flushPendingCode() -} - -func (s *pbinUpdateStream) flushPendingCode() error { - p := &s.pendingCode - var key [pbinAccountKeyLength]byte - copy(key[:], p.stem[:]) - for i := range p.chunks { - key[pbinAccountKeyLength-1] = byte(pbinCodeOffset + i) - update := Update{Flags: StorageUpdate, StorageLen: pbinValueLength, Storage: p.chunks[i]} - if err := s.emit(key[:], nil, &update); err != nil { - return err - } - } - p.chunks = nil - return nil -} - func (s *pbinUpdateStream) stateOf(plainKey []byte) (*Update, error) { if len(plainKey) == length.Addr { update, err := s.state.Account(plainKey) @@ -237,11 +299,11 @@ func (s *pbinUpdateStream) stateOf(plainKey []byte) (*Update, error) { return update, nil } -func (s *pbinUpdateStream) codeHashKey(basicDataKey []byte) ([]byte, error) { +func (s *pbinUpdateStream) emitSibling(basicDataKey []byte, subIndex byte, plainKey []byte, update *Update) error { if len(basicDataKey) != pbinAccountKeyLength || basicDataKey[pbinAccountKeyLength-1] != pbinBasicDataLeafKey { - return nil, fmt.Errorf("pbin: %x is not a BASIC_DATA key", basicDataKey) + return fmt.Errorf("pbin: %x is not a BASIC_DATA key", basicDataKey) } copy(s.siblingKey[:], basicDataKey) - s.siblingKey[pbinAccountKeyLength-1] = pbinCodeHashLeafKey - return s.siblingKey[:], nil + s.siblingKey[pbinAccountKeyLength-1] = subIndex + return s.emit(s.siblingKey[:], plainKey, update) } diff --git a/execution/commitment/pbin_values.go b/execution/commitment/pbin_values.go index e0b5bc22164..87906a7947d 100644 --- a/execution/commitment/pbin_values.go +++ b/execution/commitment/pbin_values.go @@ -28,10 +28,10 @@ import ( "github.com/erigontech/erigon/common/length" ) -// pbinValueLength is the one leaf value size EIP-8297 admits (eip:132). +// pbinValueLength is the one leaf value size EIP-8297 admits (eip:"Tree structure"). const pbinValueLength = 32 -// BASIC_DATA field offsets within the leaf value (eip:332-339). Byte 0 (version) +// BASIC_DATA field offsets within the leaf value (eip:"Header values"). Byte 0 (version) // and the reserved bytes 1..3 stay zero. const ( pbinBasicDataCodeSizeOffset = 4 @@ -64,7 +64,7 @@ func pbinEncodeBasicData(nonce uint64, balance *uint256.Int, codeSize uint64) ([ // pbinCodeHashValue returns the CODE_HASH leaf value, mapping an unset hash to // the empty-bytecode hash as the spec requires for a codeless account -// (eip:345-347). +// (eip:"Header values"). func pbinCodeHashValue(codeHash common.Hash) [pbinValueLength]byte { if codeHash == (common.Hash{}) { return empty.CodeHash @@ -72,6 +72,28 @@ func pbinCodeHashValue(codeHash common.Hash) [pbinValueLength]byte { return codeHash } +// EIP-7702 delegation indicators (eip:"Delegation"). Classification reads the +// code bytes, never the hash — a code hash may begin with the marker too. +var pbinDelegationMarker = [3]byte{0xEF, 0x01, 0x00} + +const pbinDelegationCodeLength = 23 + +func pbinIsDelegation(code []byte) bool { + return len(code) == pbinDelegationCodeLength && [3]byte(code) == pbinDelegationMarker +} + +// pbinEncodeDelegation right-pads the indicator into the DELEGATION leaf value. +// This is not the chunk encoding: an indicator never executes, so byte 0 holds +// code rather than a PUSHDATA count. +func pbinEncodeDelegation(code []byte) [pbinValueLength]byte { + if len(code) != pbinDelegationCodeLength { + panic(fmt.Sprintf("pbin: delegation indicator of %d bytes, want %d", len(code), pbinDelegationCodeLength)) + } + var v [pbinValueLength]byte + copy(v[:], code) + return v +} + func pbinEncodeStorageValue(value []byte) [pbinValueLength]byte { if len(value) > length.Hash { panic(fmt.Sprintf("pbin: storage value of %d bytes exceeds %d", len(value), length.Hash)) diff --git a/execution/commitment/pbin_verify_test.go b/execution/commitment/pbin_verify_test.go index 825809fe325..ac0b10bbc69 100644 --- a/execution/commitment/pbin_verify_test.go +++ b/execution/commitment/pbin_verify_test.go @@ -212,46 +212,50 @@ func (v *pbinVerifier) recordAt(nodePath *pbinBitpath) ([2]pbinCell, error) { return cells, nil } -// checkPlainKeys asserts every stored leaf sits where its own key derivation puts -// it: the record's path, the child bit and the cell's prefix must spell exactly -// treeKey(plainKey). A slot routed into the wrong zone still builds a tree that -// hashes consistently, so position against derivation is what catches it. +// checkPlainKeys asserts every reachable stored leaf sits where its own key +// derivation puts it: the record's path, the child bit and the cell's prefix +// must spell exactly treeKey(plainKey). A slot routed into the wrong zone still +// builds a tree that hashes consistently, so position against derivation is +// what catches it. The walk starts at the stored root cell: a dropped subtree's +// records stay behind unreferenced — nothing enumerates them to delete them — +// so the reachable set is the tree. func (v *pbinVerifier) checkPlainKeys() (int, error) { root, err := v.rootCell() if err != nil { return 0, err } - leaves := 0 - if root.kind == pbinNodeLeaf { - var start pbinBitpath - if err = v.checkLeafPosition(&start, &root); err != nil { + var start pbinBitpath + return v.checkCellLeaves(&start, &root) +} + +func (v *pbinVerifier) checkCellLeaves(start *pbinBitpath, c *pbinCell) (int, error) { + switch c.kind { + case pbinNodeLeaf: + if err := v.checkLeafPosition(start, c); err != nil { return 0, err } - leaves++ - } - paths, err := v.recordPaths() - if err != nil { - return 0, err - } - for _, path := range paths { - cells, err := v.recordAt(&path) + return 1, nil + case pbinNodeBranch: + nodePath := *start + nodePath.append(&c.prefix) + cells, err := v.recordAt(&nodePath) if err != nil { return 0, err } + leaves := 0 for bit := range cells { - c := &cells[bit] - if c.kind != pbinNodeLeaf { - continue - } - start := path - start.appendBit(uint64(bit)) - if err = v.checkLeafPosition(&start, c); err != nil { + childStart := nodePath + childStart.appendBit(uint64(bit)) + n, err := v.checkCellLeaves(&childStart, &cells[bit]) + if err != nil { return 0, err } - leaves++ + leaves += n } + return leaves, nil + default: + return 0, fmt.Errorf("pbin verify: cell at %d bits has no node kind", start.bitLen) } - return leaves, nil } func (v *pbinVerifier) checkLeafPosition(start *pbinBitpath, c *pbinCell) error { @@ -288,14 +292,13 @@ func pbinVerifyDerivedKey(c *pbinCell, key []byte) ([]byte, error) { return pbinTreeKeyStorage(addr, slot), nil default: // A record-resident leaf holds no plain key to re-derive from, so what is - // checked is where it may sit: only a code chunk carries its own value, and - // a chunk is either at the top of an account stem or in the code zone — - // never in the storage zone. - switch { - case len(key) == pbinCodeKeyLength && key[0] == pbinCodeZone: - case len(key) == pbinAccountKeyLength && key[0] == pbinAccountZone && key[pbinAccountKeyLength-1] >= pbinCodeOffset: - default: - return nil, fmt.Errorf("%w: value-carrying leaf at %x is no code chunk", errPBinVerifyPosition, key) + // checked is where it may sit: a code chunk in the code zone, or a + // delegation indicator at its header sub-index — never anywhere else. + if len(key) == pbinAccountKeyLength && key[0] == pbinAccountZone && key[pbinAccountKeyLength-1] == pbinDelegationLeafKey { + return key, nil + } + if len(key) != pbinCodeKeyLength || key[0] != pbinCodeZone { + return nil, fmt.Errorf("%w: value-carrying leaf at %x is neither code chunk nor delegation leaf", errPBinVerifyPosition, key) } return key, nil } diff --git a/execution/commitment/pbin_witness.go b/execution/commitment/pbin_witness.go new file mode 100644 index 00000000000..124f565968c --- /dev/null +++ b/execution/commitment/pbin_witness.go @@ -0,0 +1,126 @@ +// Copyright 2026 The Erigon Authors +// This file is part of Erigon. +// +// Erigon is free software: you can redistribute it and/or modify +// it under the terms of the GNU Lesser General Public License as published by +// the Free Software Foundation, either version 3 of the License, or +// (at your option) any later version. +// +// Erigon is distributed in the hope that it will be useful, +// but WITHOUT ANY WARRANTY; without even the implied warranty of +// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +// GNU Lesser General Public License for more details. +// +// You should have received a copy of the GNU Lesser General Public License +// along with Erigon. If not, see . + +package commitment + +import ( + "bytes" + "context" + "fmt" + + "github.com/erigontech/erigon/common" +) + +// Witness capture for the binary trie. The tap sits in pbinHasher, not in the +// fold: sibling cells (hashRowCell) and the root cell (RootHash) are hashed +// outside foldBranch, and a fold-level tap would miss them. + +// emitNode hands a node's consensus preimage and hash to the tracer. The +// preimage is pbinHasher's scratch buffer, overwritten by the next hash, so a +// tracer that keeps it must copy. +func (h *pbinHasher) emitNode(preimage []byte, hash *common.Hash) { + if h.tracer == nil { + return + } + h.tracer.onNode(preimage, hash[:]) +} + +// setWitnessTracer taps every node this engine hashes. Reset detaches, so it +// must be called after any reset and never survives into a pooled reuse. +func (pph *PBinPatriciaHashed) setWitnessTracer(tracer witnessTracer) { + pph.hasher.tracer = tracer +} + +// pbinWitnessReadOnly drops the branch writes a fold makes on its way up. The +// witness pass folds rows it never modified, so writing them back would rewrite +// stored records under this pass's empty touch map. +type pbinWitnessReadOnly struct{ PatriciaContext } + +func (pbinWitnessReadOnly) PutBranch(prefix, data, prevData []byte) error { return nil } + +// Code forwards the code seam the update stream reaches for by type assertion; +// without it the wrapper would hide the wrapped context's own Code. +func (c pbinWitnessReadOnly) Code(plainKey []byte) ([]byte, error) { + inner, ok := c.PatriciaContext.(pbinCodeContext) + if !ok { + return nil, fmt.Errorf("%w: %T serves no code", ErrPBinUnsupported, c.PatriciaContext) + } + return inner.Code(plainKey) +} + +// PBinWitnessBlock is what a witness pass cannot read out of the parent state: +// the code the block writes, whose chunk keys would otherwise go unwalked, and +// the accounts it removes, which are indistinguishable there from accounts it +// creates. Both are keyed by account plain key. +type PBinWitnessBlock struct { + Code map[string][]byte + Removed map[string]struct{} +} + +// SetWitnessBlock supplies the next witness pass with what the parent state +// cannot say. Cleared when Witnesses returns. +func (pph *PBinPatriciaHashed) SetWitnessBlock(b PBinWitnessBlock) { + pph.updateStream.witness = b +} + +// Witnesses walks the tree along every key the update stream expands to, taps +// each node as it is hashed, and returns the captured superset (root first), the +// keys walked, and the root hash. Callers prune to the lean set. +// +// No update is applied: the caller checks the returned root against the parent +// block's, so it must be the pre-state one. +// +// produceExclusionProofs is accepted and ignored. It materializes the branch an +// extension node hides, and EIP-8297 has no extension node. The collapse +// survivors a removal re-hashes are captured unconditionally instead — see +// captureBranchPreimage. +func (pph *PBinPatriciaHashed) Witnesses(ctx context.Context, updates *Updates, produceExclusionProofs bool, logPrefix string) (nodes [][]byte, provedKeys [][]byte, rootHash []byte, err error) { + set := newWitnessNodeSet() + pph.setWitnessTracer(set) + defer pph.setWitnessTracer(nil) + pph.updateStream.witnessPass = true + defer func() { pph.updateStream.witness, pph.updateStream.witnessPass = PBinWitnessBlock{}, false }() + + stateCtx := pph.ctx + pph.ctx = pbinWitnessReadOnly{PatriciaContext: stateCtx} + defer func() { pph.ctx = stateCtx }() + + pph.lastKeyLen = 0 + provedKeys = make([][]byte, 0, updates.Size()) + // The proved keys are the stream's, not HashSort's: one account touch expands + // into a BASIC_DATA leaf, a CODE_HASH leaf and one leaf per code chunk, and + // only the sink sees all of them. + _, err = pph.updateStream.process(ctx, updates, pph.ctx, func(treeKey, _ []byte, _ *Update) error { + provedKeys = append(provedKeys, bytes.Clone(treeKey)) + _, err := pph.seek(treeKey) + return err + }) + if err != nil { + return nil, nil, nil, fmt.Errorf("pbin: witness %s: %w", logPrefix, err) + } + for pph.grid.activeRows > 0 { + if err = pph.fold(); err != nil { + return nil, nil, nil, fmt.Errorf("pbin: witness final fold: %w", err) + } + } + if rootHash, err = pph.RootHash(); err != nil { + return nil, nil, nil, err + } + if nodes, err = set.nodes(rootHash); err != nil { + return nil, nil, nil, err + } + return nodes, provedKeys, rootHash, nil +} diff --git a/execution/commitment/pbin_witness_codezone_test.go b/execution/commitment/pbin_witness_codezone_test.go new file mode 100644 index 00000000000..749828f7f5d --- /dev/null +++ b/execution/commitment/pbin_witness_codezone_test.go @@ -0,0 +1,314 @@ +// Copyright 2026 The Erigon Authors +// This file is part of Erigon. +// +// Erigon is free software: you can redistribute it and/or modify +// it under the terms of the GNU Lesser General Public License as published by +// the Free Software Foundation, either version 3 of the License, or +// (at your option) any later version. +// +// Erigon is distributed in the hope that it will be useful, +// but WITHOUT ANY WARRANTY; without even the implied warranty of +// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +// GNU Lesser General Public License for more details. +// +// You should have received a copy of the GNU Lesser General Public License +// along with Erigon. If not, see . + +package commitment + +import ( + "bytes" + "context" + "encoding/hex" + "testing" + + keccak "github.com/erigontech/fastkeccak" + "github.com/holiman/uint256" + "github.com/stretchr/testify/require" + + "github.com/erigontech/erigon/common" +) + +// Deploying a contract writes leaves into the content-addressed code zone. +// When another contract's chunks are already there, the new leaves split an +// existing subtree, and the witness has to carry the node they split. + +// pbinSpillingCode returns code of chunkCount chunks, distinct per seed so two +// accounts land on different code-zone stems. +func pbinSpillingCode(seed byte, chunkCount int) []byte { + code := bytes.Repeat([]byte{0x01}, 31*chunkCount) + code[0] = seed + return code +} + +// pbinDeployCorpus is one account deploying code, the shape a create block has: +// the account's leaves and its chunks all arrive at once. +func pbinDeployCorpus(addrSeed uint64, code []byte) *pbinTestCorpus { + c := new(pbinTestCorpus) + return c.accountWithCodeBytes(pbinOracleAddr(addrSeed), 1, 1, code) +} + +// pbinStreamKeys runs the update stream over state and returns the tree keys it +// expands to, in emission order. +func pbinStreamKeys(t *testing.T, state PatriciaContext, c *pbinTestCorpus, block PBinWitnessBlock, witness bool) []string { + t.Helper() + upd := WrapKeyUpdates(t, ModeDirect, pbinKeyHasher(), c.plainKeys, c.updates) + s := &pbinUpdateStream{witness: block, witnessPass: witness} + var keys []string + _, err := s.process(context.Background(), upd, state, func(treeKey, _ []byte, _ *Update) error { + keys = append(keys, hex.EncodeToString(treeKey)) + return nil + }) + require.NoError(t, err) + return keys +} + +// TestPBinWitnessCodeOverrideMatchesFoldKeys is the property the override exists +// for: a witness pass reading the parent state has to expand an account to the +// same tree keys the fold did against the state the block leaves behind. Without +// the override the parent has no code for a contract the block creates, and the +// chunk keys go missing. +func TestPBinWitnessCodeOverrideMatchesFoldKeys(t *testing.T) { + t.Parallel() + + addr := pbinOracleAddr(72) + code := pbinSpillingCode(0xC0, 136) + deploy := pbinDeployCorpus(72, code) + + post := NewMockState(t) + require.NoError(t, post.applyPlainUpdates(deploy.plainKeys, deploy.updates)) + post.setCode(addr, code) + fold := pbinStreamKeys(t, post, deploy, PBinWitnessBlock{}, false) + + parent := NewMockState(t) + witness := pbinStreamKeys(t, parent, deploy, PBinWitnessBlock{Code: map[string][]byte{string(addr): code}}, true) + + require.Equal(t, fold, witness) + require.Len(t, fold, 3+136, "three header keys and one key per chunk") + + // Without it the parent state yields the account's header keys only. + require.Len(t, pbinStreamKeys(t, parent, deploy, PBinWitnessBlock{}, true), 3) +} + +// pbinWitnessStateFor commits the corpus, proves a touch of addr and decodes the +// pruned witness back into a readable state. +func pbinWitnessStateFor(t *testing.T, corpus *pbinTestCorpus, addr []byte) (*PBinWitnessState, [][]byte, []byte) { + t.Helper() + ms, parentRoot := pbinWitnessCommitted(t, corpus) + + upd := WrapKeyUpdates(t, ModeDirect, pbinKeyHasher(), [][]byte{addr}, []Update{{}}) + nodes, provedKeys, root := pbinWitnessesOf(t, ms, upd, false) + require.Equal(t, parentRoot, root) + + lean, err := PBinWitnessNodesForKeys(nodes, root, provedKeys) + require.NoError(t, err) + state, err := PBinNewWitnessState(lean, root) + require.NoError(t, err) + return state, lean, root +} + +// TestPBinWitnessDelegatedAccountIsPresent: a delegated account holds no +// CODE_HASH leaf, so the delegation leaf has to mark it present, with the code +// hash EXTCODEHASH defines — the keccak of the indicator bytes. +func TestPBinWitnessDelegatedAccountIsPresent(t *testing.T) { + t.Parallel() + + addr := pbinOracleAddr(75) + indicator := append([]byte{0xEF, 0x01, 0x00}, bytes.Repeat([]byte{0x22}, 20)...) + corpus := new(pbinTestCorpus).accountWithCodeBytes(addr, 3, 700, indicator) + + state, _, _ := pbinWitnessStateFor(t, corpus, addr) + + acc, ok, err := state.Account(addr) + require.NoError(t, err) + require.True(t, ok, "the delegation leaf marks the account present") + require.Equal(t, uint64(3), acc.Nonce) + require.Equal(t, uint64(700), acc.Balance.Uint64()) + require.Equal(t, uint64(pbinDelegationCodeLength), acc.CodeSize) + require.Equal(t, common.Hash(keccak.Sum256(indicator)), acc.CodeHash) +} + +// TestPBinWitnessDelegatedAccountCarriesNoChunks: the indicator is the code, read +// straight from the header leaf — the witness holds no code-zone leaf for it. +func TestPBinWitnessDelegatedAccountCarriesNoChunks(t *testing.T) { + t.Parallel() + + addr := pbinOracleAddr(76) + indicator := append([]byte{0xEF, 0x01, 0x00}, bytes.Repeat([]byte{0x33}, 20)...) + corpus := new(pbinTestCorpus).accountWithCodeBytes(addr, 1, 5, indicator) + + state, lean, root := pbinWitnessStateFor(t, corpus, addr) + + code, ok, err := state.Code(addr) + require.NoError(t, err) + require.True(t, ok) + require.Equal(t, indicator, code, "the code is the leading code_size bytes of the delegation leaf") + + tree, err := pbinDecodeWitness(lean, root) + require.NoError(t, err) + for _, node := range tree.nodes { + if node.isLeaf() { + require.NotEqual(t, byte(pbinCodeZone), node.key[0], "a delegated account owns no code-zone leaf") + } + } +} + +// TestPBinWitnessReassemblesCodeAcrossGroups: chunk 256 lives under tree_index 1, +// a different code-zone stem than chunks 0-255. The read has to cross that group +// boundary and come back byte-for-byte. +func TestPBinWitnessReassemblesCodeAcrossGroups(t *testing.T) { + t.Parallel() + + addr := pbinOracleAddr(77) + code := pbinSpillingCode(0xD0, pbinStemSubtreeWidth+1) + corpus := new(pbinTestCorpus).accountWithCodeBytes(addr, 1, 1, code) + + state, _, _ := pbinWitnessStateFor(t, corpus, addr) + + got, ok, err := state.Code(addr) + require.NoError(t, err) + require.True(t, ok) + require.Equal(t, code, got) +} + +// TestPBinWitnessDeployIntoPopulatedCodeZone: a witness for a deploy has to let +// a verifier reach the post-state root, whether or not the code zone already +// holds another contract's chunks. The empty-zone case passes on its own, so the +// populated one is what the shared subtree adds. +func TestPBinWitnessDeployIntoPopulatedCodeZone(t *testing.T) { + t.Parallel() + + const chunks = 136 + + for _, tc := range []struct { + name string + prior int // chunks the code zone already holds, 0 for an empty zone + }{ + {name: "empty code zone", prior: 0}, + {name: "prior contract of 136 chunks", prior: 136}, + {name: "prior contract of 129 chunks", prior: 129}, + {name: "prior contract of 256 chunks", prior: 256}, + } { + t.Run(tc.name, func(t *testing.T) { + t.Parallel() + + prior := new(pbinTestCorpus) + if tc.prior > 0 { + prior = pbinDeployCorpus(70, pbinSpillingCode(0xA0, tc.prior)) + } + ms, parentRoot := pbinWitnessCommitted(t, prior) + + code := pbinSpillingCode(0xB0, chunks) + deploy := pbinDeployCorpus(71, code) + // The block is executed, so its code is readable, but its leaves are + // not in the tree the witness proves: that is the parent's. + ms.setCode(pbinOracleAddr(71), code) + + upd := WrapKeyUpdates(t, ModeUpdate, pbinKeyHasher(), deploy.plainKeys, deploy.updates) + nodes, provedKeys, root := pbinWitnessesOf(t, ms, upd, false) + require.Equal(t, parentRoot, root, "the witness pass must prove the pre-state") + + lean, err := PBinWitnessNodesForKeys(nodes, root, provedKeys) + require.NoError(t, err) + + state, err := PBinNewWitnessState(lean, root) + require.NoError(t, err) + state.SetCode(pbinOracleAddr(71), code) + + got, err := state.Root(context.Background(), deploy.plainKeys, deploy.updates) + require.NoError(t, err, "the witness must carry every node the deploy descends through") + + deploy.applyTo(t, ms) + applied := WrapKeyUpdates(t, ModeUpdate, pbinKeyHasher(), deploy.plainKeys, deploy.updates) + want, err := NewPBinPatriciaHashed(ms).Process(context.Background(), applied, "", nil, WarmupConfig{}) + require.NoError(t, err) + require.Equal(t, want, got) + }) + } +} + +// TestPBinWitnessAccountMissingCodeHashIsMalformed: a present account holds +// exactly one of the CODE_HASH and DELEGATION leaves, so a witness that proves +// both absent under a live BASIC_DATA leaf describes a state the tree cannot +// hold. Reading it as an absent account would recompute a wrong root instead. +func TestPBinWitnessAccountMissingCodeHashIsMalformed(t *testing.T) { + t.Parallel() + + addr := pbinOracleAddr(77) + keys := pbinDigestCache{sum: pbinSelectedSum} + var value [pbinValueLength]byte + value[pbinBasicDataNonceOffset+7] = 1 + + preimage := append([]byte{pbinLeafTag}, keys.accountKey(addr, pbinBasicDataLeafKey)...) + preimage = append(preimage, value[:]...) + hasher := pbinHasher{sum: pbinSelectedSum} + root := hasher.hash(preimage) + + state, err := PBinNewWitnessState([][]byte{preimage}, root[:]) + require.NoError(t, err) + + _, _, err = state.Account(addr) + require.ErrorIs(t, err, errPBinWitnessNode) +} + +// TestPBinWitnessDelegationLeafPinsCodeSize: a DELEGATION leaf is a fixed shape, +// so its account's code_size is always the indicator length. A BASIC_DATA leaf +// claiming anything else describes a state the tree cannot hold, and reading it +// would report an account running code no one wrote. +func TestPBinWitnessDelegationLeafPinsCodeSize(t *testing.T) { + t.Parallel() + + addr := pbinOracleAddr(78) + indicator := append([]byte{0xEF, 0x01, 0x00}, bytes.Repeat([]byte{0x44}, 20)...) + keys := pbinDigestCache{sum: pbinSelectedSum} + hasher := pbinHasher{sum: pbinSelectedSum} + + leaf := func(key []byte, value [pbinValueLength]byte) ([]byte, common.Hash) { + preimage := append(append([]byte{pbinLeafTag}, key...), value[:]...) + return preimage, hasher.hash(preimage) + } + + basicKey := keys.accountKey(addr, pbinBasicDataLeafKey) + var balance uint256.Int + balance.SetUint64(2) + basic, err := pbinEncodeBasicData(1, &balance, pbinDelegationCodeLength-1) + require.NoError(t, err) + basicNode, basicHash := leaf(basicKey, basic) + delegNode, delegHash := leaf(keys.accountKey(addr, pbinDelegationLeafKey), pbinEncodeDelegation(indicator)) + + // Sub-indices 0 and 2 diverge two bits before the end of the key. + prefix := pbinPathFromBits(basicKey, int16(8*len(basicKey)-2)) + branch := pbinAppendBitPrefix([]byte{pbinBranchTag}, &prefix) + branch = append(branch, basicHash[:]...) + branch = append(branch, delegHash[:]...) + root := hasher.hash(branch) + + state, err := PBinNewWitnessState([][]byte{branch, basicNode, delegNode}, root[:]) + require.NoError(t, err) + + _, _, err = state.Account(addr) + require.ErrorIs(t, err, errPBinWitnessNode) +} + +// TestPBinFoldIgnoresWitnessBlock: the block override is a witness-pass input. +// A fold that finds one left behind has to derive its chunk keys from state +// anyway — honouring it would commit a wrong root on the execution path. +func TestPBinFoldIgnoresWitnessBlock(t *testing.T) { + t.Parallel() + + addr := pbinOracleAddr(73) + code := pbinSpillingCode(0xC1, 4) + deploy := pbinDeployCorpus(73, code) + + post := NewMockState(t) + require.NoError(t, post.applyPlainUpdates(deploy.plainKeys, deploy.updates)) + post.setCode(addr, code) + + stale := PBinWitnessBlock{ + Code: map[string][]byte{string(addr): pbinSpillingCode(0xC2, 9)}, + Removed: map[string]struct{}{string(addr): {}}, + } + require.Equal(t, + pbinStreamKeys(t, post, deploy, PBinWitnessBlock{}, false), + pbinStreamKeys(t, post, deploy, stale, false)) +} diff --git a/execution/commitment/pbin_witness_context.go b/execution/commitment/pbin_witness_context.go new file mode 100644 index 00000000000..ec396ba2c99 --- /dev/null +++ b/execution/commitment/pbin_witness_context.go @@ -0,0 +1,292 @@ +// Copyright 2026 The Erigon Authors +// This file is part of Erigon. +// +// Erigon is free software: you can redistribute it and/or modify +// it under the terms of the GNU Lesser General Public License as published by +// the Free Software Foundation, either version 3 of the License, or +// (at your option) any later version. +// +// Erigon is distributed in the hope that it will be useful, +// but WITHOUT ANY WARRANTY; without even the implied warranty of +// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +// GNU Lesser General Public License for more details. +// +// You should have received a copy of the GNU Lesser General Public License +// along with Erigon. If not, see . + +package commitment + +import ( + "bytes" + "encoding/binary" + "errors" + "fmt" + + "github.com/erigontech/erigon/common" + "github.com/erigontech/erigon/common/length" + "github.com/erigontech/erigon/db/kv" +) + +// A decoded witness served as a PatriciaContext, so PBinPatriciaHashed is itself +// the mutable trie a post-state root comes out of — leaf splitting, branch +// creation, BASIC_DATA packing and code chunking included — instead of a second +// binary trie written beside it. + +var ( + errPBinWitnessBlinded = errors.New("pbin: witness node is blinded") + errPBinWitnessNoState = errors.New("pbin: witness holds no state") +) + +// pbinWitnessContext turns node preimages into the branch records the engine +// unfolds. A record is derived on first read and cached; PutBranch replaces it, +// so a fold reads back what it wrote. +type pbinWitnessContext struct { + tree *pbinWitnessTree + records map[string][]byte + leaves map[string]Update + codes map[string][]byte + keys pbinDigestCache +} + +var ( + _ PatriciaContext = (*pbinWitnessContext)(nil) + _ pbinCodeContext = (*pbinWitnessContext)(nil) + _ pbinDerivedContext = (*pbinWitnessContext)(nil) +) + +func (c *pbinWitnessContext) pbinRecordsAreDerived() {} + +func pbinNewWitnessContext(tree *pbinWitnessTree) *pbinWitnessContext { + return &pbinWitnessContext{ + tree: tree, + records: make(map[string][]byte), + leaves: make(map[string]Update), + codes: make(map[string][]byte), + keys: pbinDigestCache{sum: pbinSelectedSum}, + } +} + +// setCode supplies bytecode the node set cannot hold: code a block deploys has +// no pre-state chunk leaves to reassemble. +func (c *pbinWitnessContext) setCode(plainKey, code []byte) { + c.codes[string(plainKey)] = bytes.Clone(code) +} + +func (c *pbinWitnessContext) Branch(prefix []byte) ([]byte, kv.Step, error) { + if record, ok := c.records[string(prefix)]; ok { + return record, 0, nil + } + record, err := c.deriveRecord(prefix) + if err != nil { + return nil, 0, err + } + c.records[string(prefix)] = record + return record, 0, nil +} + +func (c *pbinWitnessContext) PutBranch(prefix, data, prevData []byte) error { + c.records[string(prefix)] = bytes.Clone(data) + return nil +} + +func (c *pbinWitnessContext) Account(plainKey []byte) (*Update, error) { return c.leafState(plainKey) } + +func (c *pbinWitnessContext) Storage(plainKey []byte) (*Update, error) { return c.leafState(plainKey) } + +func (c *pbinWitnessContext) Code(plainKey []byte) ([]byte, error) { + if code, ok := c.codes[string(plainKey)]; ok { + return code, nil + } + code, err := c.codeFromLeaves(plainKey) + if err != nil { + return nil, err + } + if code == nil { + return nil, fmt.Errorf("%w: no code for %x", errPBinWitnessNoState, plainKey) + } + return code, nil +} + +// leafState resolves the handle a witness leaf cell carries in place of a plain +// key: the witness holds a leaf's value, never the address it was derived from. +// Anything else is refused — an empty read would hash a zeroed leaf into the +// root instead of failing. +func (c *pbinWitnessContext) leafState(plainKey []byte) (*Update, error) { + state, ok := c.leaves[string(plainKey)] + if !ok { + return nil, fmt.Errorf("%w for plain key %x", errPBinWitnessNoState, plainKey) + } + return &state, nil +} + +func (c *pbinWitnessContext) deriveRecord(prefix []byte) ([]byte, error) { + if bytes.Equal(prefix, pbinRootKey) { + return c.rootRecord() + } + path, err := pbinDecodeBitPath(prefix) + if err != nil { + return nil, err + } + node, err := c.nodeAt(&path) + if err != nil { + return nil, err + } + return c.branchRecord(&node, &path) +} + +// rootRecord holds the one cell no descent can name. An empty tree has no +// record at all, which is the only shape a caller may read as absent. +func (c *pbinWitnessContext) rootRecord() ([]byte, error) { + if c.tree.root == pbinEmptyTreeHash { + return []byte{}, nil + } + if _, ok := c.tree.nodes[c.tree.root]; !ok { + return nil, fmt.Errorf("%w: no preimage for root %x", errPBinWitnessBlinded, c.tree.root) + } + var cell pbinCell + cell.reset() + var path pbinBitpath + if err := c.fillCell(&cell, c.tree.root, &path); err != nil { + return nil, err + } + return pbinAppendCell(nil, &cell) +} + +func (c *pbinWitnessContext) branchRecord(node *pbinWitnessNode, path *pbinBitpath) ([]byte, error) { + if path.bitLen >= pbinMaxPathBits { + return nil, fmt.Errorf("%w: a branch at %d bits leaves no room for a child", + errPBinWitnessNode, path.bitLen) + } + var cells [2]pbinCell + for bit := range cells { + childPath := *path + childPath.appendBit(uint64(bit)) + cells[bit].reset() + if err := c.fillCell(&cells[bit], node.children[bit], &childPath); err != nil { + return nil, err + } + } + var encoder pbinBranchEncoder + // The touch map is write-time bookkeeping a read discards, so it says the same + // as the after map. + record, err := encoder.encode(pbinCellBits, pbinCellBits, &cells) + if err != nil { + return nil, err + } + return bytes.Clone(record), nil +} + +// fillCell describes one child of a branch. A child with no preimage is opaque: +// it hashes to what its parent commits to, and a descent into it fails in +// nodeAt, where the path is known. +func (c *pbinWitnessContext) fillCell(cell *pbinCell, hash common.Hash, path *pbinBitpath) error { + if hash == pbinEmptyTreeHash { + // A binary node with one child is a node the fold would collapse, quietly + // moving the root. + return fmt.Errorf("%w: branch child at bit %d is the empty tree", errPBinWitnessNode, path.bitLen) + } + node, ok := c.tree.nodes[hash] + if !ok || !node.isLeaf() { + cell.kind = pbinNodeBranch + if ok { + cell.prefix = node.prefix + } + cell.hash, cell.hashLen = hash, length.Hash + return nil + } + return c.fillLeafCell(cell, &node, hash, path) +} + +func (c *pbinWitnessContext) fillLeafCell(cell *pbinCell, node *pbinWitnessNode, hash common.Hash, path *pbinBitpath) error { + key := pbinPathFromBytes(node.key) + if !key.hasPrefix(path) { + return fmt.Errorf("%w: leaf %x does not sit under the %d-bit path it was reached by", + errPBinWitnessNode, node.key, path.bitLen) + } + cell.kind = pbinNodeLeaf + cell.prefix = key.slice(path.bitLen, key.bitLen) + + // A record holds a leaf value either verbatim or as the account fields it is + // packed from. Which one applies is decided by re-encoding, not by zone, so + // this cannot drift from pbinLeafValue. + verbatim := cell.Update + verbatim.Flags, verbatim.StorageLen = StorageUpdate, pbinValueLength + copy(verbatim.Storage[:], node.value) + if value, err := pbinLeafValue(node.key, &verbatim); err == nil && bytes.Equal(value[:], node.value) { + cell.Update = verbatim + return nil + } + + state, err := pbinWitnessLeafState(node.key, node.value) + if err != nil { + return err + } + handle := hash[:length.Addr] + if prev, seen := c.leaves[string(handle)]; seen && prev != state { + return fmt.Errorf("%w: two leaves share the handle %x", errPBinWitnessNode, handle) + } + c.leaves[string(handle)] = state + cell.accountAddrLen = length.Addr + copy(cell.accountAddr[:], handle) + return nil +} + +// pbinWitnessLeafState inverts the packing pbinLeafValue applies, for the leaves +// a record cannot carry verbatim: BASIC_DATA and CODE_HASH are built from +// account fields, so the cell has to hold those fields instead. The result is +// re-encoded before it is returned, which rejects any value the tree could not +// have produced. +func pbinWitnessLeafState(key, value []byte) (Update, error) { + var u Update + u.Reset() + if key[0] == pbinAccountZone { + switch key[len(key)-1] { + case pbinBasicDataLeafKey: + u.Flags = NonceUpdate | BalanceUpdate | CodeUpdate + u.CodeSize = uint64(binary.BigEndian.Uint32(value[pbinBasicDataCodeSizeOffset:])) + u.Nonce = binary.BigEndian.Uint64(value[pbinBasicDataNonceOffset:]) + u.Balance.SetBytes(value[pbinBasicDataBalanceOffset:]) + case pbinCodeHashLeafKey: + u.Flags = CodeUpdate + u.CodeHash = common.BytesToHash(value) + } + } + if u.Flags == 0 { + return u, fmt.Errorf("%w: leaf %x carries a value no record can hold", errPBinWitnessNode, key) + } + got, err := pbinLeafValue(key, &u) + if err != nil { + return u, err + } + if !bytes.Equal(got[:], value) { + return u, fmt.Errorf("%w: leaf %x holds %x, which no state packs to", errPBinWitnessNode, key, value) + } + return u, nil +} + +// nodeAt finds the node whose absolute path is p: the root node's path is its +// own prefix, and a child's is its parent's path, the bit it hangs off, and its +// own prefix. +func (c *pbinWitnessContext) nodeAt(p *pbinBitpath) (pbinWitnessNode, error) { + hash, pos := c.tree.root, int16(0) + for { + node, ok := c.tree.nodes[hash] + if !ok { + return node, fmt.Errorf("%w: no preimage for %x, reached at bit %d of the %d-bit path %x", + errPBinWitnessBlinded, hash, pos, p.bitLen, p.appendPackedBits(nil)) + } + if node.isLeaf() { + return node, fmt.Errorf("%w: a leaf covers bit %d of the %d-bit path %x", + errPBinWitnessNode, pos, p.bitLen, p.appendPackedBits(nil)) + } + end := pos + node.prefix.bitLen + if end > p.bitLen || pbinCommonPrefixBitsAt(p, pos, &node.prefix) != node.prefix.bitLen { + return node, fmt.Errorf("%w: no node at the %d-bit path %x", + errPBinWitnessNode, p.bitLen, p.appendPackedBits(nil)) + } + if end == p.bitLen { + return node, nil + } + hash, pos = node.children[p.bit(end)], end+1 + } +} diff --git a/execution/commitment/pbin_witness_context_test.go b/execution/commitment/pbin_witness_context_test.go new file mode 100644 index 00000000000..b2c4a76a8e3 --- /dev/null +++ b/execution/commitment/pbin_witness_context_test.go @@ -0,0 +1,197 @@ +// Copyright 2026 The Erigon Authors +// This file is part of Erigon. +// +// Erigon is free software: you can redistribute it and/or modify +// it under the terms of the GNU Lesser General Public License as published by +// the Free Software Foundation, either version 3 of the License, or +// (at your option) any later version. +// +// Erigon is distributed in the hope that it will be useful, +// but WITHOUT ANY WARRANTY; without even the implied warranty of +// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +// GNU Lesser General Public License for more details. +// +// You should have received a copy of the GNU Lesser General Public License +// along with Erigon. If not, see . + +package commitment + +import ( + "bytes" + "context" + "encoding/hex" + "testing" + + "github.com/stretchr/testify/require" + + "github.com/erigontech/erigon/common" +) + +// pbinWitnessContextCode is the code pbinWitnessCorpus commits for account 21. +// The pending set keeps it unchanged: the witness pass reads pre-state code and +// checks it against the update's code size, so a resized contract is a corpus +// the pass refuses before the context is ever reached. +func pbinWitnessContextCode() []byte { return bytes.Repeat([]byte{0x60}, 200) } + +// pbinWitnessContextPending touches a coded account, a fresh account and two +// slots, so the post-state pass has to split leaves, create branches, pack +// BASIC_DATA and chunk code over the witness alone. +func pbinWitnessContextPending() *pbinTestCorpus { + c := new(pbinTestCorpus) + c.accountWithCodeBytes(pbinOracleAddr(21), 5, 1500, pbinWitnessContextCode()) + c.account(pbinOracleAddr(23), 3, 300, common.Hash{0x23}) + c.storage(pbinOracleAddr(21), pbinOracleSlot(64), 0xEE) + c.storage(pbinOracleAddr(23), pbinOracleSlot(5), 0x55) + return c +} + +type pbinWitnessContextFixture struct { + state *MockState + pending *pbinTestCorpus + witness *pbinWitnessContext + tree *pbinWitnessTree + parentRoot []byte +} + +// pbinWitnessContextSetup commits a corpus, takes the witness of the pending +// updates against it, and hands back a context backed by nothing else. +func pbinWitnessContextSetup(t *testing.T) *pbinWitnessContextFixture { + t.Helper() + f := &pbinWitnessContextFixture{pending: pbinWitnessContextPending()} + f.state, f.parentRoot = pbinWitnessCommitted(t, pbinWitnessCorpus()) + + upd := WrapKeyUpdates(t, ModeUpdate, pbinKeyHasher(), f.pending.plainKeys, f.pending.updates) + nodes, _, root := pbinWitnessesOf(t, f.state, upd, false) + require.Equal(t, f.parentRoot, root) + + f.tree = pbinWitnessDecoded(t, nodes, root) + f.witness = pbinNewWitnessContext(f.tree) + for addr, code := range f.pending.codes { + f.witness.setCode([]byte(addr), code) + } + return f +} + +func (f *pbinWitnessContextFixture) apply(t *testing.T, ctx PatriciaContext) []byte { + t.Helper() + upd := WrapKeyUpdates(t, ModeUpdate, pbinKeyHasher(), f.pending.plainKeys, f.pending.updates) + root, err := NewPBinPatriciaHashed(ctx).Process(context.Background(), upd, "", nil, WarmupConfig{}) + require.NoError(t, err) + return root +} + +// TestPBinWitnessContextPostStateRoot is the point of the whole context: the +// engine applies the block's updates over the witness and reaches the root it +// reaches over full state, so no second mutable binary trie is needed. +func TestPBinWitnessContextPostStateRoot(t *testing.T) { + t.Parallel() + + f := pbinWitnessContextSetup(t) + got := f.apply(t, f.witness) + want := f.apply(t, f.state) + + require.Equal(t, want, got) + require.NotEqual(t, f.parentRoot, want, "the pending updates do not move the root, so the test proves nothing") +} + +// TestPBinWitnessContextProvesNothingItDoesNotHold: the witness stops at the +// touched paths, and the subtrees it leaves opaque are what the root still has +// to be recomputed through. +func TestPBinWitnessContextPartialWitness(t *testing.T) { + t.Parallel() + + f := pbinWitnessContextSetup(t) + _, blinded := pbinWitnessReachable(f.tree) + require.NotEmpty(t, blinded, "the witness holds every node, so it proves nothing about partial state") + + full, _ := pbinWitnessCapture(t, pbinWitnessCorpus()) + require.Less(t, len(f.tree.nodes), len(full), "the witness is not smaller than the whole tree") + + require.Equal(t, f.apply(t, f.state), f.apply(t, f.witness)) +} + +// TestPBinWitnessContextBlindedBranchErrors: a read that needs a node the +// witness left out must name the path and fail, never come back empty — an +// empty record reads as an absent subtree and builds a wrong root. +func TestPBinWitnessContextBlindedBranchErrors(t *testing.T) { + t.Parallel() + + f := pbinWitnessContextSetup(t) + path := pbinWitnessBlindedPath(t, f.tree) + + record, _, err := f.witness.Branch(pbinEncodeBitPath(&path)) + require.ErrorIs(t, err, errPBinWitnessBlinded) + require.Empty(t, record) + require.Contains(t, err.Error(), hex.EncodeToString(path.appendPackedBits(nil)), "the error does not name the path") +} + +// pbinWitnessBlindedPath walks to the first child the witness has no preimage +// for and returns its absolute path, which is the key the engine would read a +// record at. +func pbinWitnessBlindedPath(t *testing.T, w *pbinWitnessTree) pbinBitpath { + t.Helper() + var found pbinBitpath + var ok bool + var walk func(hash common.Hash, path pbinBitpath) + walk = func(hash common.Hash, path pbinBitpath) { + node, present := w.nodes[hash] + if !present || ok { + return + } + path.append(&node.prefix) + if node.isLeaf() { + return + } + for bit := range node.children { + child := path + child.appendBit(uint64(bit)) + if _, present := w.nodes[node.children[bit]]; !present { + found, ok = child, true + return + } + walk(node.children[bit], child) + } + } + walk(w.root, pbinBitpath{}) + require.True(t, ok, "the witness blinds no child") + return found +} + +// TestPBinWitnessContextRefusesUnknownState: the context serves the witness and +// nothing else. A plain key it never issued a handle for has no state, and +// answering with an empty update would hash a zeroed leaf into the root. +func TestPBinWitnessContextRefusesUnknownState(t *testing.T) { + t.Parallel() + + f := pbinWitnessContextSetup(t) + addr := pbinOracleAddr(21) + + _, err := f.witness.Account(addr) + require.ErrorIs(t, err, errPBinWitnessNoState) + + _, err = f.witness.Storage(append(bytes.Clone(addr), pbinOracleSlot(64)...)) + require.ErrorIs(t, err, errPBinWitnessNoState) + + _, err = f.witness.Code(pbinOracleAddr(99)) + require.ErrorIs(t, err, errPBinWitnessNoState) +} + +// TestPBinWitnessContextLeafHandlesRoundTrip: a BASIC_DATA leaf is packed from +// account fields, so a record carries those fields rather than the 32 bytes the +// witness holds. The handle the cell carries has to lead back to a state that +// packs to exactly those bytes. +func TestPBinWitnessContextLeafHandlesRoundTrip(t *testing.T) { + t.Parallel() + + f := pbinWitnessContextSetup(t) + f.apply(t, f.witness) + require.NotEmpty(t, f.witness.leaves, "no leaf needed a handle, so the packing path is untested") + + for handle, state := range f.witness.leaves { + update := state + got, err := f.witness.Account([]byte(handle)) + require.NoError(t, err) + require.Equal(t, &update, got) + require.False(t, got.Deleted()) + } +} diff --git a/execution/commitment/pbin_witness_decode.go b/execution/commitment/pbin_witness_decode.go new file mode 100644 index 00000000000..93492b24ba2 --- /dev/null +++ b/execution/commitment/pbin_witness_decode.go @@ -0,0 +1,202 @@ +// Copyright 2026 The Erigon Authors +// This file is part of Erigon. +// +// Erigon is free software: you can redistribute it and/or modify +// it under the terms of the GNU Lesser General Public License as published by +// the Free Software Foundation, either version 3 of the License, or +// (at your option) any later version. +// +// Erigon is distributed in the hope that it will be useful, +// but WITHOUT ANY WARRANTY; without even the implied warranty of +// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +// GNU Lesser General Public License for more details. +// +// You should have received a copy of the GNU Lesser General Public License +// along with Erigon. If not, see . + +package commitment + +import ( + "encoding/binary" + "errors" + "fmt" + + "github.com/erigontech/erigon/common" + "github.com/erigontech/erigon/common/length" +) + +// Reading back the preimages pbinHasher emits. A witness arrives from a peer, so +// every field the writer guarantees is checked here rather than assumed. + +var errPBinWitnessNode = errors.New("pbin: malformed witness node") + +// pbinWitnessNode is one decoded preimage. preimage, key and value all alias the +// bytes the node was decoded from, so a consumer that outlives them must copy. +type pbinWitnessNode struct { + tag byte + preimage []byte + key []byte // leaf: the whole tree key + value []byte // leaf: pbinValueLength bytes + prefix pbinBitpath + children [2]common.Hash // branch: an absent child is pbinEmptyTreeHash +} + +func (n *pbinWitnessNode) isLeaf() bool { return n.tag == pbinLeafTag } + +func pbinDecodeWitnessNode(preimage []byte) (pbinWitnessNode, error) { + if len(preimage) == 0 { + return pbinWitnessNode{}, fmt.Errorf("%w: empty preimage", errPBinWitnessNode) + } + var ( + node pbinWitnessNode + err error + ) + switch preimage[0] { + case pbinLeafTag: + node, err = pbinDecodeWitnessLeaf(preimage) + case pbinBranchTag: + node, err = pbinDecodeWitnessBranch(preimage) + default: + err = fmt.Errorf("%w: unknown node tag %#x", errPBinWitnessNode, preimage[0]) + } + if err != nil { + return pbinWitnessNode{}, err + } + node.preimage = preimage + return node, nil +} + +func pbinDecodeWitnessLeaf(preimage []byte) (pbinWitnessNode, error) { + body := preimage[1:] + if len(body) <= pbinValueLength { + return pbinWitnessNode{}, fmt.Errorf("%w: leaf of %d bytes carries no key", errPBinWitnessNode, len(preimage)) + } + key := body[:len(body)-pbinValueLength] + // Key length is fixed per zone, which is what keeps the key space prefix-free + // (eip:"Tree embedding"). + if want, known := pbinZoneKeyLength(key[0]); !known || len(key) != want { + return pbinWitnessNode{}, fmt.Errorf("%w: leaf key %x is no key of zone %#x", errPBinWitnessNode, key, key[0]) + } + return pbinWitnessNode{tag: pbinLeafTag, key: key, value: body[len(body)-pbinValueLength:]}, nil +} + +func pbinDecodeWitnessBranch(preimage []byte) (pbinWitnessNode, error) { + const head = 1 + 2 // tag, then the bit count encode_bit_prefix leads with + if len(preimage) < head { + return pbinWitnessNode{}, fmt.Errorf("%w: branch of %d bytes carries no bit count", errPBinWitnessNode, len(preimage)) + } + bitLen := int(binary.BigEndian.Uint16(preimage[1:head])) + if bitLen > pbinMaxPathBits { + return pbinWitnessNode{}, fmt.Errorf("%w: branch prefix of %d bits exceeds the %d-bit path", errPBinWitnessNode, bitLen, pbinMaxPathBits) + } + packed := (bitLen + 7) / 8 + if want := head + packed + 2*length.Hash; len(preimage) != want { + return pbinWitnessNode{}, fmt.Errorf("%w: branch of %d bytes, want %d for a %d-bit prefix", errPBinWitnessNode, len(preimage), want, bitLen) + } + if used := bitLen % 8; used != 0 && preimage[head+packed-1]&(byte(0xFF)>>uint(used)) != 0 { + return pbinWitnessNode{}, fmt.Errorf("%w: %w in a %d-bit branch prefix", errPBinWitnessNode, errPBinNonCanonicalPad, bitLen) + } + n := pbinWitnessNode{ + tag: pbinBranchTag, + prefix: pbinPathFromBits(preimage[head:head+packed], int16(bitLen)), + } + children := preimage[head+packed:] + n.children[0] = common.BytesToHash(children[:length.Hash]) + n.children[1] = common.BytesToHash(children[length.Hash:]) + return n, nil +} + +// pbinWitnessTree is a decoded node set indexed by H(preimage), rooted at the +// hash the capture reported. +type pbinWitnessTree struct { + nodes map[common.Hash]pbinWitnessNode + root common.Hash + hasher pbinHasher +} + +// pbinDecodeWitness decodes a captured node set rooted at root. The root is +// given rather than taken from the slice, so preimages may arrive in any order: +// the witness an RPC consumer receives is sorted, and re-rooting the tree on +// whatever leads the slice would turn a lost root node into a wrong answer +// instead of an error. +func pbinDecodeWitness(preimages [][]byte, root []byte) (*pbinWitnessTree, error) { + if len(root) != length.Hash { + return nil, fmt.Errorf("%w: witness root of %d bytes", errPBinWitnessNode, len(root)) + } + w := &pbinWitnessTree{ + nodes: make(map[common.Hash]pbinWitnessNode, len(preimages)), + root: common.BytesToHash(root), + hasher: pbinHasher{sum: pbinSelectedSum}, + } + if len(preimages) == 0 { + if w.root != pbinEmptyTreeHash { + return nil, fmt.Errorf("%w: no nodes for root %x", errPBinWitnessNode, root) + } + return w, nil + } + for i, preimage := range preimages { + node, err := pbinDecodeWitnessNode(preimage) + if err != nil { + return nil, fmt.Errorf("witness node %d: %w", i, err) + } + w.nodes[w.hasher.hash(preimage)] = node + } + if _, ok := w.nodes[w.root]; !ok { + return nil, fmt.Errorf("%w: no node for root %x", errPBinWitnessNode, root) + } + return w, nil +} + +// merkelize rehashes the tree from its root, so a decode that lost anything +// fails here instead of downstream. A child hash the set has no preimage for is +// blinded: opaque, and carried up as it stands. +func (w *pbinWitnessTree) merkelize() (common.Hash, error) { + if len(w.nodes) == 0 { + return pbinEmptyTreeHash, nil + } + got, err := w.merkelizeFrom(w.root, 0) + if err != nil { + return common.Hash{}, err + } + if got != w.root { + return common.Hash{}, fmt.Errorf("%w: root %x re-merkelizes to %x", errPBinWitnessNode, w.root, got) + } + return got, nil +} + +// merkelizeFrom rehashes the subtree at hash, sitting at bit position depth. The +// depth bounds the recursion: a branch consumes its prefix plus the bit it +// splits on, so nodes referencing each other in a cycle run out of path rather +// than running forever. +func (w *pbinWitnessTree) merkelizeFrom(hash common.Hash, depth int16) (common.Hash, error) { + node, ok := w.nodes[hash] + if !ok { + return hash, nil + } + if node.isLeaf() { + return w.hasher.leafNodeHash(node.key, node.value), nil + } + next := depth + node.prefix.bitLen + 1 + if int(next) > pbinMaxPathBits { + return common.Hash{}, fmt.Errorf("%w: branch at bit %d with a %d-bit prefix overflows the %d-bit path", + errPBinWitnessNode, depth, node.prefix.bitLen, pbinMaxPathBits) + } + left, err := w.merkelizeFrom(node.children[0], next) + if err != nil { + return common.Hash{}, err + } + right, err := w.merkelizeFrom(node.children[1], next) + if err != nil { + return common.Hash{}, err + } + return w.hasher.branchHash(&node.prefix, &left, &right), nil +} + +// leafNodeHash is H over a leaf preimage built from a decoded key, where +// leafCellHash packs the key from a path and a cell. +func (h *pbinHasher) leafNodeHash(key, value []byte) common.Hash { + buf := append(h.buf[:0], pbinLeafTag) + buf = append(buf, key...) + buf = append(buf, value...) + return h.hash(buf) +} diff --git a/execution/commitment/pbin_witness_decode_test.go b/execution/commitment/pbin_witness_decode_test.go new file mode 100644 index 00000000000..79b1c2c1e1a --- /dev/null +++ b/execution/commitment/pbin_witness_decode_test.go @@ -0,0 +1,325 @@ +// Copyright 2026 The Erigon Authors +// This file is part of Erigon. +// +// Erigon is free software: you can redistribute it and/or modify +// it under the terms of the GNU Lesser General Public License as published by +// the Free Software Foundation, either version 3 of the License, or +// (at your option) any later version. +// +// Erigon is distributed in the hope that it will be useful, +// but WITHOUT ANY WARRANTY; without even the implied warranty of +// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +// GNU Lesser General Public License for more details. +// +// You should have received a copy of the GNU Lesser General Public License +// along with Erigon. If not, see . + +package commitment + +import ( + "bytes" + "slices" + "testing" + + "github.com/stretchr/testify/require" + + "github.com/erigontech/erigon/common" + "github.com/erigontech/erigon/common/length" +) + +// pbinWitnessCapture folds the corpus with the node set attached, giving the +// root-first slice a witness carries. +func pbinWitnessCapture(t *testing.T, corpus *pbinTestCorpus) (nodes [][]byte, root []byte) { + t.Helper() + set := newWitnessNodeSet() + root, _ = pbinWitnessProcess(t, corpus, set) + root = bytes.Clone(root) + nodes, err := set.nodes(root) + require.NoError(t, err) + return nodes, root +} + +func pbinWitnessDecoded(t *testing.T, nodes [][]byte, root []byte) *pbinWitnessTree { + t.Helper() + w, err := pbinDecodeWitness(nodes, root) + require.NoError(t, err) + return w +} + +func pbinWitnessMerkelized(t *testing.T, nodes [][]byte, root []byte) []byte { + t.Helper() + got, err := pbinWitnessDecoded(t, nodes, root).merkelize() + require.NoError(t, err) + return got[:] +} + +// pbinWitnessReachable walks the decoded tree from its root, returning the nodes +// it reaches and the child hashes it could not resolve. Both are what a consumer +// of the witness actually sees; the captured set holds more. +func pbinWitnessReachable(w *pbinWitnessTree) (reached map[common.Hash]pbinWitnessNode, blinded []common.Hash) { + reached = make(map[common.Hash]pbinWitnessNode) + var walk func(hash common.Hash) + walk = func(hash common.Hash) { + node, ok := w.nodes[hash] + if !ok { + if hash != pbinEmptyTreeHash { + blinded = append(blinded, hash) + } + return + } + if _, seen := reached[hash]; seen { + return + } + reached[hash] = node + if !node.isLeaf() { + walk(node.children[0]) + walk(node.children[1]) + } + } + walk(w.root) + return reached, blinded +} + +// TestPBinDecodeWitnessNodeShapes pins the two preimage layouts against the +// reference transcription's encode_bit_prefix rather than against the encoder +// the engine uses. +func TestPBinDecodeWitnessNodeShapes(t *testing.T) { + t.Parallel() + + t.Run("leaf", func(t *testing.T) { + t.Parallel() + key := pbinTreeKeyAccount(pbinOracleAddr(1), pbinBasicDataLeafKey) + value := pbinOracleValue(9) + + node, err := pbinDecodeWitnessNode(slices.Concat([]byte{pbinLeafTag}, key, value)) + require.NoError(t, err) + require.True(t, node.isLeaf()) + require.Equal(t, key, node.key) + require.Equal(t, value, node.value) + }) + + t.Run("branch", func(t *testing.T) { + t.Parallel() + left, right := common.Hash{0xAA}, common.Hash{0xBB} + preimage := slices.Concat( + []byte{pbinBranchTag}, + pbinOracleEncodeBitPrefix([]byte{1, 0, 1}), + left[:], right[:]) + + node, err := pbinDecodeWitnessNode(preimage) + require.NoError(t, err) + require.False(t, node.isLeaf()) + require.Equal(t, pbinPathFromBits([]byte{0xA0}, 3), node.prefix) + require.Equal(t, [2]common.Hash{left, right}, node.children) + }) + + t.Run("branch with an empty prefix", func(t *testing.T) { + t.Parallel() + preimage := slices.Concat( + []byte{pbinBranchTag}, + pbinOracleEncodeBitPrefix(nil), + make([]byte, 2*length.Hash)) + + node, err := pbinDecodeWitnessNode(preimage) + require.NoError(t, err) + require.Equal(t, int16(0), node.prefix.bitLen) + require.Equal(t, [2]common.Hash{pbinEmptyTreeHash, pbinEmptyTreeHash}, node.children, + "an absent child is the empty-tree hash, never omitted") + }) +} + +// TestPBinDecodeWitnessNodeRejectsMalformed: a witness comes from a peer, so +// every one of these has to error rather than yield a node that hashes to +// something else. +func TestPBinDecodeWitnessNodeRejectsMalformed(t *testing.T) { + t.Parallel() + + key := pbinTreeKeyAccount(pbinOracleAddr(1), pbinBasicDataLeafKey) + value := pbinOracleValue(9) + children := make([]byte, 2*length.Hash) + + branch := func(bitLen uint16, packed []byte) []byte { + return slices.Concat([]byte{pbinBranchTag, byte(bitLen >> 8), byte(bitLen)}, packed, children) + } + + for _, tc := range []struct { + name string + preimage []byte + }{ + {name: "empty preimage", preimage: nil}, + {name: "unknown tag", preimage: slices.Concat([]byte{0x02}, key, value)}, + {name: "leaf without a key", preimage: slices.Concat([]byte{pbinLeafTag}, value)}, + {name: "leaf truncated inside its value", preimage: slices.Concat([]byte{pbinLeafTag}, key, value[:31])}, + {name: "leaf key of an unallocated zone", preimage: slices.Concat([]byte{pbinLeafTag, 0x02}, key[1:], value)}, + {name: "leaf key one byte short of its zone", preimage: slices.Concat([]byte{pbinLeafTag}, key[:len(key)-1], value)}, + {name: "leaf key one byte past its zone", preimage: slices.Concat([]byte{pbinLeafTag}, key, []byte{0}, value)}, + {name: "branch without a bit count", preimage: []byte{pbinBranchTag, 0x00}}, + {name: "branch prefix past the encodable path", preimage: branch(pbinMaxPathBits+1, make([]byte, 67))}, + {name: "branch prefix truncated", preimage: branch(16, []byte{0x00})}, + {name: "branch missing a child hash", preimage: slices.Concat([]byte{pbinBranchTag, 0, 0}, children[:length.Hash])}, + {name: "branch with a trailing byte", preimage: branch(0, []byte{0x00})}, + {name: "branch prefix padded non-canonically", preimage: branch(3, []byte{0xA1})}, + } { + t.Run(tc.name, func(t *testing.T) { + t.Parallel() + _, err := pbinDecodeWitnessNode(tc.preimage) + require.ErrorIs(t, err, errPBinWitnessNode) + }) + } +} + +// TestPBinWitnessDecodeRoundTrip: a captured fold decodes back into the leaves +// the corpus stands for and re-merkelizes to the root it was captured under. +func TestPBinWitnessDecodeRoundTrip(t *testing.T) { + t.Parallel() + + corpus := pbinWitnessCorpus() + nodes, root := pbinWitnessCapture(t, corpus) + require.Equal(t, corpus.oracleRoot(t), root) + + w := pbinWitnessDecoded(t, nodes, root) + reached, blinded := pbinWitnessReachable(w) + require.Empty(t, blinded, "a fold from empty state hashes every node, so nothing is blinded") + + leaves := make(map[string][]byte) + branches := 0 + for _, node := range reached { + if node.isLeaf() { + leaves[string(node.key)] = node.value + continue + } + branches++ + } + require.Positive(t, branches) + require.Len(t, leaves, corpus.leafCount(t)) + for _, e := range corpus.entries(t) { + require.Equal(t, e.value, leaves[string(e.key)], "leaf %x", e.key) + } + + got, err := w.merkelize() + require.NoError(t, err) + require.Equal(t, root, got[:]) +} + +// TestPBinWitnessDecodeBlindedChild: the witness of a few touched keys proves +// only their paths, and the subtrees it leaves out are opaque hashes the root +// still has to come out of. +func TestPBinWitnessDecodeBlindedChild(t *testing.T) { + t.Parallel() + + ms, parentRoot := pbinWitnessCommitted(t, pbinWitnessCorpus()) + pending := pbinWitnessPending() + + upd := WrapKeyUpdates(t, ModeUpdate, pbinKeyHasher(), pending.plainKeys, pending.updates) + nodes, _, root := pbinWitnessesOf(t, ms, upd, false) + require.Equal(t, parentRoot, root) + + w := pbinWitnessDecoded(t, nodes, root) + _, blinded := pbinWitnessReachable(w) + require.NotEmpty(t, blinded, "the witness resolves every child, so it proves nothing about blinding") + + require.Equal(t, root, pbinWitnessMerkelized(t, nodes, root)) +} + +// TestPBinWitnessDecodePermutationIndependence: the captured set depends on the +// key/value set, not on the order the keys were folded in, and the root it +// re-merkelizes to is the reference implementation's. +func TestPBinWitnessDecodePermutationIndependence(t *testing.T) { + t.Parallel() + + corpus := pbinWitnessCorpus() + forward := make([]int, len(corpus.plainKeys)) + for i := range forward { + forward[i] = i + } + reversed := slices.Clone(forward) + slices.Reverse(reversed) + interleaved := slices.Concat(forward[len(forward)/2:], forward[:len(forward)/2]) + + var tree pbinOracleTree + for _, e := range corpus.entries(t) { + tree.insert(e.key, e.value) + } + want := pbinOracleMerkelizeWith(tree.root, nil) + + for name, order := range map[string][]int{ + "forward": forward, + "reversed": reversed, + "interleaved": interleaved, + } { + t.Run(name, func(t *testing.T) { + t.Parallel() + nodes, root := pbinWitnessCapture(t, corpus.permute(order)) + require.Equal(t, want[:], pbinWitnessMerkelized(t, nodes, root)) + }) + } +} + +// TestPBinWitnessDecodeIgnoresNodeOrder: the witness an RPC consumer receives is +// sorted by node bytes, so the decode may not require the root to lead the slice +// — it is told the root and looks it up. +func TestPBinWitnessDecodeIgnoresNodeOrder(t *testing.T) { + t.Parallel() + + nodes, root := pbinWitnessCapture(t, pbinWitnessCorpus()) + require.Greater(t, len(nodes), 1) + + sorted := slices.Clone(nodes) + slices.SortFunc(sorted, bytes.Compare) + require.NotEqual(t, nodes[0], sorted[0], "the sort has to move the root off the front") + require.Equal(t, root, pbinWitnessMerkelized(t, sorted, root)) + + reversed := slices.Clone(nodes) + slices.Reverse(reversed) + require.Equal(t, root, pbinWitnessMerkelized(t, reversed, root)) +} + +// TestPBinWitnessDecodeSingleNodeRemoval: dropping a node blinds its subtree, +// which leaves the root alone. The one drop that could change it — the root +// node's — has to be caught instead. +func TestPBinWitnessDecodeSingleNodeRemoval(t *testing.T) { + t.Parallel() + + nodes, root := pbinWitnessCapture(t, pbinWitnessCorpus()) + require.Greater(t, len(nodes), 1) + + rejected := 0 + for i := range nodes { + short := make([][]byte, 0, len(nodes)-1) + short = append(short, nodes[:i]...) + short = append(short, nodes[i+1:]...) + + w, err := pbinDecodeWitness(short, root) + if err != nil { + rejected++ + continue + } + got, err := w.merkelize() + if err != nil { + rejected++ + continue + } + require.Equal(t, root, got[:], "dropping node %d moved the root instead of failing", i) + } + require.Equal(t, 1, rejected, "only the root node's removal is unrecoverable") +} + +// TestPBinWitnessDecodeEmptyTree: an empty tree is 32 zero bytes with no node +// behind it (eip:"Node merkelization"), and a witness claiming any other root with no nodes is +// unusable rather than empty. +func TestPBinWitnessDecodeEmptyTree(t *testing.T) { + t.Parallel() + + w, err := pbinDecodeWitness(nil, pbinEmptyTreeHash[:]) + require.NoError(t, err) + got, err := w.merkelize() + require.NoError(t, err) + require.Equal(t, pbinEmptyTreeHash, got) + + nonEmpty := common.Hash{0x01} + _, err = pbinDecodeWitness(nil, nonEmpty[:]) + require.ErrorIs(t, err, errPBinWitnessNode) + + _, err = pbinDecodeWitness(nil, nil) + require.ErrorIs(t, err, errPBinWitnessNode) +} diff --git a/execution/commitment/pbin_witness_prune.go b/execution/commitment/pbin_witness_prune.go new file mode 100644 index 00000000000..6cb7ebb4a30 --- /dev/null +++ b/execution/commitment/pbin_witness_prune.go @@ -0,0 +1,118 @@ +// Copyright 2026 The Erigon Authors +// This file is part of Erigon. +// +// Erigon is free software: you can redistribute it and/or modify +// it under the terms of the GNU Lesser General Public License as published by +// the Free Software Foundation, either version 3 of the License, or +// (at your option) any later version. +// +// Erigon is distributed in the hope that it will be useful, +// but WITHOUT ANY WARRANTY; without even the implied warranty of +// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +// GNU Lesser General Public License for more details. +// +// You should have received a copy of the GNU Lesser General Public License +// along with Erigon. If not, see . + +package commitment + +import ( + "fmt" + + "github.com/erigontech/erigon/common" +) + +// Pruning the captured superset down to the proof paths of the keys the fold +// walked — the binary analogue of trie.WitnessNodesForKeysFromNodes. + +// PBinWitnessNodesForKeys keeps the nodes on the proof path of every proved key, +// plus the sibling hanging off each branch along it, and drops the rest, +// returning them in walk order so the root leads. A path that runs into a leaf, +// a diverging branch prefix or a blinded child stops there — what it walked +// through is the proof that the key is absent. +func PBinWitnessNodesForKeys(nodes [][]byte, root []byte, provedKeys [][]byte) ([][]byte, error) { + if len(nodes) == 0 { + return nil, nil + } + tree, err := pbinDecodeWitness(nodes, root) + if err != nil { + return nil, err + } + p := pbinWitnessPruner{tree: tree, kept: make(map[common.Hash]struct{}, len(tree.nodes))} + // The root node leads the output even when no key descends past it. + p.keep(tree.root) + for _, key := range provedKeys { + if err := p.walk(key); err != nil { + return nil, err + } + } + out := make([][]byte, 0, len(p.order)) + for _, hash := range p.order { + out = append(out, tree.nodes[hash].preimage) + } + return out, nil +} + +type pbinWitnessPruner struct { + tree *pbinWitnessTree + kept map[common.Hash]struct{} + order []common.Hash +} + +func (p *pbinWitnessPruner) keep(hash common.Hash) { + if _, seen := p.kept[hash]; seen { + return + } + p.kept[hash] = struct{}{} + p.order = append(p.order, hash) +} + +// keepSibling keeps the child the walk turns away from. Its hash is committed by +// the branch above it either way; what the preimage adds is the ability to +// re-hash it under a longer prefix, which is what a removal on the other side of +// the branch makes the consumer do. A sibling the capture blinded is skipped — +// then the consumer can still read the branch, just not delete under it. +func (p *pbinWitnessPruner) keepSibling(hash common.Hash) { + if _, ok := p.tree.nodes[hash]; ok { + p.keep(hash) + } +} + +func (p *pbinWitnessPruner) walk(key []byte) error { + path, err := pbinWitnessProvedPath(key) + if err != nil { + return err + } + hash, pos := p.tree.root, int16(0) + for { + node, ok := p.tree.nodes[hash] + if !ok { + return nil + } + p.keep(hash) + if node.isLeaf() { + return nil + } + end := pos + node.prefix.bitLen + if end >= path.bitLen || pbinCommonPrefixBitsAt(&path, pos, &node.prefix) != node.prefix.bitLen { + return nil + } + bit := path.bit(end) + p.keepSibling(node.children[1-bit]) + hash, pos = node.children[bit], end+1 + } +} + +// pbinWitnessProvedPath rejects a key no zone admits rather than letting +// pbinPathFromBytes panic on it. A key shorter than its zone's length is a +// subtree prefix, which an account removal proves in place of the leaves it +// drops, so the walk stops where that subtree begins. +func pbinWitnessProvedPath(key []byte) (pbinBitpath, error) { + if len(key) == 0 { + return pbinBitpath{}, fmt.Errorf("%w: empty proved key", errPBinWitnessNode) + } + if want, known := pbinZoneKeyLength(key[0]); !known || len(key) > want { + return pbinBitpath{}, fmt.Errorf("%w: proved key %x is no key of zone %#x", errPBinWitnessNode, key, key[0]) + } + return pbinPathFromBytes(key), nil +} diff --git a/execution/commitment/pbin_witness_prune_test.go b/execution/commitment/pbin_witness_prune_test.go new file mode 100644 index 00000000000..54321091094 --- /dev/null +++ b/execution/commitment/pbin_witness_prune_test.go @@ -0,0 +1,371 @@ +// Copyright 2026 The Erigon Authors +// This file is part of Erigon. +// +// Erigon is free software: you can redistribute it and/or modify +// it under the terms of the GNU Lesser General Public License as published by +// the Free Software Foundation, either version 3 of the License, or +// (at your option) any later version. +// +// Erigon is distributed in the hope that it will be useful, +// but WITHOUT ANY WARRANTY; without even the implied warranty of +// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +// GNU Lesser General Public License for more details. +// +// You should have received a copy of the GNU Lesser General Public License +// along with Erigon. If not, see . + +package commitment + +import ( + "bytes" + "context" + "slices" + "testing" + + keccak "github.com/erigontech/fastkeccak" + "github.com/stretchr/testify/require" + + "github.com/erigontech/erigon/common" +) + +type pbinWitnessPruneFixture struct { + state *MockState + pending *pbinTestCorpus + nodes [][]byte + provedKeys [][]byte + root []byte + tree *pbinWitnessTree +} + +// pbinWitnessPruneSetup commits a corpus and captures the superset witness of +// the pending updates against it — the input the pruner has to cut down. +func pbinWitnessPruneSetup(t *testing.T) *pbinWitnessPruneFixture { + t.Helper() + f := &pbinWitnessPruneFixture{pending: pbinWitnessContextPending()} + f.state, f.root = pbinWitnessCommitted(t, pbinWitnessCorpus()) + + upd := WrapKeyUpdates(t, ModeUpdate, pbinKeyHasher(), f.pending.plainKeys, f.pending.updates) + nodes, provedKeys, root := pbinWitnessesOf(t, f.state, upd, false) + require.Equal(t, f.root, root) + f.nodes, f.provedKeys = nodes, provedKeys + f.tree = pbinWitnessDecoded(t, f.nodes, f.root) + return f +} + +func (f *pbinWitnessPruneFixture) prune(t *testing.T, provedKeys [][]byte) [][]byte { + t.Helper() + lean, err := PBinWitnessNodesForKeys(f.nodes, f.root, provedKeys) + require.NoError(t, err) + return lean +} + +// postStateRoot applies the pending updates over a node set through the witness +// context, which is what a pruned witness still has to support. +func (f *pbinWitnessPruneFixture) postStateRoot(t *testing.T, nodes [][]byte) []byte { + t.Helper() + witness := pbinNewWitnessContext(pbinWitnessDecoded(t, nodes, f.root)) + for addr, code := range f.pending.codes { + witness.setCode([]byte(addr), code) + } + return f.applyOver(t, witness) +} + +func (f *pbinWitnessPruneFixture) applyOver(t *testing.T, ctx PatriciaContext) []byte { + t.Helper() + upd := WrapKeyUpdates(t, ModeUpdate, pbinKeyHasher(), f.pending.plainKeys, f.pending.updates) + root, err := NewPBinPatriciaHashed(ctx).Process(context.Background(), upd, "", nil, WarmupConfig{}) + require.NoError(t, err) + return bytes.Clone(root) +} + +func pbinWitnessHashSet(t *testing.T, nodes [][]byte) map[common.Hash]struct{} { + t.Helper() + h := pbinHasher{sum: pbinSelectedSum} + out := make(map[common.Hash]struct{}, len(nodes)) + for _, node := range nodes { + out[h.hash(node)] = struct{}{} + } + return out +} + +// pbinWitnessOnPathNodes names the nodes the proved keys walk through and the +// sibling hanging off each branch they descend, stated as "the path taken to +// reach the node is a prefix of some proved key, or its parent's is" over a walk +// of the whole tree — not as the per-key descent the pruner runs. +func pbinWitnessOnPathNodes(w *pbinWitnessTree, provedKeys [][]byte) map[common.Hash]struct{} { + paths := make([]pbinBitpath, 0, len(provedKeys)) + for _, key := range provedKeys { + paths = append(paths, pbinPathFromBytes(key)) + } + onPath := func(arrival *pbinBitpath) bool { + for i := range paths { + if paths[i].hasPrefix(arrival) { + return true + } + } + return false + } + out := make(map[common.Hash]struct{}) + keep := func(hash common.Hash) { + if _, ok := w.nodes[hash]; ok { + out[hash] = struct{}{} + } + } + var walk func(hash common.Hash, arrival pbinBitpath) + walk = func(hash common.Hash, arrival pbinBitpath) { + node, ok := w.nodes[hash] + if !ok || !onPath(&arrival) { + return + } + out[hash] = struct{}{} + if node.isLeaf() { + return + } + child := [2]pbinBitpath{} + for bit := range node.children { + child[bit] = arrival + child[bit].append(&node.prefix) + child[bit].appendBit(uint64(bit)) + } + for bit := range node.children { + if onPath(&child[bit]) { + keep(node.children[1-bit]) + } + walk(node.children[bit], child[bit]) + } + } + walk(w.root, pbinBitpath{}) + return out +} + +// TestPBinWitnessPruneKeepsProofPaths: the lean set has to be a witness in its +// own right — it re-merkelizes to the same root and still carries the block's +// updates to the same post-state root the full capture does. +func TestPBinWitnessPruneKeepsProofPaths(t *testing.T) { + t.Parallel() + + f := pbinWitnessPruneSetup(t) + lean := f.prune(t, f.provedKeys) + + require.NotEmpty(t, lean) + require.Equal(t, f.nodes[0], lean[0], "root node is not first") + require.Equal(t, f.root, pbinWitnessMerkelized(t, lean, f.root)) + + require.Equal(t, f.postStateRoot(t, f.nodes), f.postStateRoot(t, lean)) + require.Equal(t, f.applyOver(t, f.state), f.postStateRoot(t, lean)) +} + +// TestPBinWitnessPruneDropsOffPathNodes: the capture holds nodes neither a proved +// key nor a collapse reaches — whole subtrees hanging two or more levels off a +// path, and branches re-hashed under a shorter prefix earlier in the fold. +// Keeping them is the whole cost the pruner exists to remove. +func TestPBinWitnessPruneDropsOffPathNodes(t *testing.T) { + t.Parallel() + + f := pbinWitnessPruneSetup(t) + lean := f.prune(t, f.provedKeys) + + full := pbinWitnessHashSet(t, f.nodes) + kept := pbinWitnessHashSet(t, lean) + require.Less(t, len(lean), len(f.nodes), "nothing was pruned, so the test proves nothing") + for hash := range kept { + require.Contains(t, full, hash, "the pruned set invented node %x", hash) + } + require.Equal(t, pbinWitnessOnPathNodes(f.tree, f.provedKeys), kept) +} + +// TestPBinWitnessPruneKeepsCodeLeaves: a contract's code leaves are proved keys +// of their own (they never reach HashSort), and a pruner walking only the account +// key would drop the code the post-state pass then cannot chunk. +func TestPBinWitnessPruneKeepsCodeLeaves(t *testing.T) { + t.Parallel() + + f := pbinWitnessPruneSetup(t) + lean := f.prune(t, f.provedKeys) + + addr := pbinOracleAddr(21) + code := pbinWitnessContextCode() + chunks := pbinChunkifyCode(code) + require.Greater(t, len(chunks), 1) + + leaves := make(map[string]struct{}) + for _, node := range lean { + decoded, err := pbinDecodeWitnessNode(node) + require.NoError(t, err) + if decoded.isLeaf() { + leaves[string(decoded.key)] = struct{}{} + } + } + for i := range chunks { + require.Contains(t, leaves, string(pbinTreeKeyCodeChunk(keccak.Sum256(code), i)), "code chunk %d was pruned away", i) + } + require.Contains(t, leaves, string(pbinTreeKeyAccount(addr, pbinCodeHashLeafKey))) +} + +// TestPBinWitnessPruneStopsAtBlindedChild: a key whose path leaves the witness +// keeps what it walked and stops. The key is built from a path the witness is +// known to blind, so the case cannot silently stop being one. +func TestPBinWitnessPruneStopsAtBlindedChild(t *testing.T) { + t.Parallel() + + f := pbinWitnessPruneSetup(t) + blind := pbinWitnessKeyThrough(t, pbinWitnessBlindedPath(t, f.tree)) + require.NotContains(t, pbinWitnessKeySet(f.provedKeys), string(blind)) + + lean := f.prune(t, [][]byte{blind}) + require.Greater(t, len(lean), 1, "the walk stopped at the root, so it never reached the blinded child") + require.Equal(t, f.root, pbinWitnessMerkelized(t, lean, f.root)) + require.Equal(t, pbinWitnessOnPathNodes(f.tree, [][]byte{blind}), pbinWitnessHashSet(t, lean)) + + both := f.prune(t, append(slices.Clone(f.provedKeys), blind)) + require.Equal(t, f.root, pbinWitnessMerkelized(t, both, f.root)) + require.Equal(t, f.postStateRoot(t, f.nodes), f.postStateRoot(t, both)) +} + +// pbinWitnessKeyThrough builds the tree key of the zone path leads into, so a +// walk of that key descends exactly the path. +func pbinWitnessKeyThrough(t *testing.T, path pbinBitpath) []byte { + t.Helper() + key := path.appendPackedBits(nil) + require.NotEmpty(t, key) + want, known := pbinZoneKeyLength(key[0]) + require.True(t, known, "path %x leads into no allocated zone", key) + require.LessOrEqual(t, len(key), want) + return append(key, make([]byte, want-len(key))...) +} + +func pbinWitnessKeySet(keys [][]byte) map[string]struct{} { + out := make(map[string]struct{}, len(keys)) + for _, key := range keys { + out[string(key)] = struct{}{} + } + return out +} + +// TestPBinWitnessPruneRejectsMalformedKey: a proved key of no zone would panic in +// the bit-path conversion, which an RPC handler must not do. +func TestPBinWitnessPruneRejectsMalformedKey(t *testing.T) { + t.Parallel() + + f := pbinWitnessPruneSetup(t) + for _, tc := range []struct { + name string + key []byte + }{ + {name: "empty key", key: nil}, + {name: "unallocated zone", key: bytes.Repeat([]byte{0x02}, pbinAccountKeyLength)}, + {name: "wrong length for its zone", key: make([]byte, pbinAccountKeyLength+1)}, + {name: "longer than the path", key: make([]byte, 2*pbinStorageKeyLength)}, + } { + t.Run(tc.name, func(t *testing.T) { + t.Parallel() + _, err := PBinWitnessNodesForKeys(f.nodes, f.root, [][]byte{tc.key}) + require.ErrorIs(t, err, errPBinWitnessNode) + }) + } +} + +// TestPBinWitnessPruneKeepsSubtreePrefix: an account removal proves the subtree +// it drops, not the leaves inside it, so a key shorter than its zone's length is +// a proved key and the walk stops where that subtree begins. +func TestPBinWitnessPruneKeepsSubtreePrefix(t *testing.T) { + t.Parallel() + + f := pbinWitnessPruneSetup(t) + var leafKey []byte + for _, key := range f.provedKeys { + if len(key) == pbinAccountKeyLength && key[0] == pbinAccountZone { + leafKey = key + break + } + } + require.NotEmpty(t, leafKey, "the capture proved no account-zone leaf") + stem := leafKey[:pbinAccountKeyLength-1] + + kept := pbinWitnessHashSet(t, f.prune(t, [][]byte{stem})) + require.NotEmpty(t, kept) + for hash := range kept { + require.Contains(t, pbinWitnessHashSet(t, f.prune(t, [][]byte{leafKey})), hash, + "the stem walk descended past the subtree the leaf key reaches") + } + require.Equal(t, f.root, pbinWitnessMerkelized(t, f.prune(t, [][]byte{stem}), f.root)) +} + +// TestPBinWitnessServesRemoval: a removal collapses the branch above the key and +// re-hashes the surviving sibling under a longer prefix. That needs the +// sibling's own preimage — a branch hash commits to the node under the prefix it +// had, so it cannot be re-prefixed — which the capture has to hash and the +// pruner has to keep. Both sibling shapes are covered: a leaf, which the fold +// hashes on its way past, and a branch, which arrives as a bare hash out of its +// parent's record. +func TestPBinWitnessServesRemoval(t *testing.T) { + t.Parallel() + + addr, bystander := pbinOracleAddr(41), pbinOracleAddr(42) + // Storage-zone sub-indices split on the low bits of the slot: 0 and 1 sit + // under one branch, 2 under the other. + stored := func(slots ...uint64) *pbinTestCorpus { + c := new(pbinTestCorpus).account(bystander, 1, 2, common.Hash{0x42}) + for _, slot := range slots { + c.storage(addr, pbinOracleSlot(slot), 0x01) + } + return c + } + for _, tc := range []struct { + name string + stored, survivors *pbinTestCorpus + gone uint64 + }{ + { + name: "collapse onto a leaf sibling", + stored: stored(256, 257, 258, 259), + survivors: stored(257, 258, 259), + gone: 256, + }, + { + name: "collapse onto a branch sibling", + stored: stored(256, 257, 258), + survivors: stored(256, 257), + gone: 258, + }, + } { + t.Run(tc.name, func(t *testing.T) { + t.Parallel() + + ms, root := pbinWitnessCommitted(t, tc.stored) + zeroed := new(pbinTestCorpus).storage(addr, pbinOracleSlot(tc.gone)) + + upd := WrapKeyUpdates(t, ModeUpdate, pbinKeyHasher(), zeroed.plainKeys, zeroed.updates) + nodes, provedKeys, captured := pbinWitnessesOf(t, ms, upd, false) + require.Equal(t, root, captured) + + lean, err := PBinWitnessNodesForKeys(nodes, root, provedKeys) + require.NoError(t, err) + require.Less(t, len(lean), len(nodes), "nothing was pruned, so the test proves nothing") + + want := tc.survivors.oracleRoot(t) + require.NotEqual(t, root, want, "the removal did not move the root") + for _, set := range []struct { + name string + nodes [][]byte + }{{"superset", nodes}, {"lean", lean}} { + witness := pbinNewWitnessContext(pbinWitnessDecoded(t, set.nodes, root)) + got, err := NewPBinPatriciaHashed(witness).Process(context.Background(), + WrapKeyUpdates(t, ModeUpdate, pbinKeyHasher(), zeroed.plainKeys, zeroed.updates), + "", nil, WarmupConfig{}) + require.NoError(t, err, "%s cannot serve the removal", set.name) + require.Equal(t, want, got, "%s reached the wrong post-state root", set.name) + } + }) + } +} + +// TestPBinWitnessPruneEmptyCapture: no capture, nothing to prune. The empty +// result is what an update set touching nothing produces. +func TestPBinWitnessPruneEmptyCapture(t *testing.T) { + t.Parallel() + + lean, err := PBinWitnessNodesForKeys(nil, pbinEmptyTreeHash[:], nil) + require.NoError(t, err) + require.Empty(t, lean) +} diff --git a/execution/commitment/pbin_witness_state.go b/execution/commitment/pbin_witness_state.go new file mode 100644 index 00000000000..b46f1c2020a --- /dev/null +++ b/execution/commitment/pbin_witness_state.go @@ -0,0 +1,317 @@ +// Copyright 2026 The Erigon Authors +// This file is part of Erigon. +// +// Erigon is free software: you can redistribute it and/or modify +// it under the terms of the GNU Lesser General Public License as published by +// the Free Software Foundation, either version 3 of the License, or +// (at your option) any later version. +// +// Erigon is distributed in the hope that it will be useful, +// but WITHOUT ANY WARRANTY; without even the implied warranty of +// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +// GNU Lesser General Public License for more details. +// +// You should have received a copy of the GNU Lesser General Public License +// along with Erigon. If not, see . + +package commitment + +import ( + "bytes" + "context" + "encoding/binary" + "fmt" + + keccak "github.com/erigontech/fastkeccak" + "github.com/holiman/uint256" + + "github.com/erigontech/erigon/common" +) + +// A decoded witness read as pre-state. pbinWitnessContext already serves it to +// the engine by bit path; this reads it the way a stateless verifier does, by +// address and slot, and drives the engine over it for the post-state root. +// +// Resolution is strict and not optional: a hash the witness carries no preimage +// for is an error on every path, never an empty read. Hex makes that a +// WITNESS_STRICT_VERIFY opt-in because an MPT witness can be legitimately +// incomplete; under bin an unresolved hash is unambiguous, so there is no mode +// where guessing is right. + +// PBinAccount is the account state the header leaves hold. A delegated account +// has no CODE_HASH leaf, and its CodeHash is the keccak of the indicator bytes +// its DELEGATION leaf carries. The binary tree commits no per-account storage +// root, so there is no field for one. +type PBinAccount struct { + Nonce uint64 + Balance uint256.Int + CodeSize uint64 + CodeHash common.Hash +} + +// PBinWitnessState is a decoded binary witness served as pre-state. +type PBinWitnessState struct { + tree *pbinWitnessTree + ctx *pbinWitnessContext + keys pbinDigestCache +} + +func PBinNewWitnessState(nodes [][]byte, root []byte) (*PBinWitnessState, error) { + tree, err := pbinDecodeWitness(nodes, root) + if err != nil { + return nil, err + } + return &PBinWitnessState{ + tree: tree, + ctx: pbinNewWitnessContext(tree), + keys: pbinDigestCache{sum: pbinSelectedSum}, + }, nil +} + +// SetCode supplies bytecode the witness cannot hold: code a block deploys has no +// pre-state chunk leaves. Everything else is read from the leaves. +func (s *PBinWitnessState) SetCode(addr, code []byte) { s.ctx.setCode(addr, code) } + +// Account resolves an address to the state its account leaves hold. ok is +// false when the witness proves the account absent. +func (s *PBinWitnessState) Account(addr []byte) (PBinAccount, bool, error) { + // An account holds exactly one of the CODE_HASH and DELEGATION leaves, and + // neither is ever zero, so whichever exists marks the account present — + // while BASIC_DATA is absent for an account whose nonce, balance and + // code_size are all zero. + var acc PBinAccount + basic, hasBasic, err := s.tree.leaf(s.keys.accountKey(addr, pbinBasicDataLeafKey)) + if err != nil { + return PBinAccount{}, false, err + } + if hasBasic { + acc.CodeSize = uint64(binary.BigEndian.Uint32(basic[pbinBasicDataCodeSizeOffset:])) + acc.Nonce = binary.BigEndian.Uint64(basic[pbinBasicDataNonceOffset:]) + acc.Balance.SetBytes(basic[pbinBasicDataBalanceOffset:]) + } + + codeHash, ok, err := s.tree.leaf(s.keys.accountKey(addr, pbinCodeHashLeafKey)) + if err != nil { + return PBinAccount{}, false, err + } + if ok { + acc.CodeHash = common.BytesToHash(codeHash) + return acc, true, nil + } + + indicator, err := s.ctx.delegationCode(addr, acc.CodeSize) + if err != nil { + return PBinAccount{}, false, err + } + if indicator == nil { + if hasBasic { + return PBinAccount{}, false, fmt.Errorf("%w: account %x has a BASIC_DATA leaf but neither a CODE_HASH nor a DELEGATION leaf", + errPBinWitnessNode, addr) + } + return PBinAccount{}, false, nil + } + // A delegated account commits no code hash; EXTCODEHASH defines its hash as + // the keccak of the indicator bytes. + acc.CodeHash = common.Hash(keccak.Sum256(indicator)) + return acc, true, nil +} + +// Storage resolves one slot. ok is false when the witness proves the slot +// absent, which the tree reads as zero. +func (s *PBinWitnessState) Storage(addr, slot []byte) (common.Hash, bool, error) { + value, ok, err := s.tree.leaf(s.keys.storageKey(addr, slot)) + if err != nil || !ok { + return common.Hash{}, false, err + } + return common.BytesToHash(value), true, nil +} + +// HasStorage reports whether the witness holds a non-zero storage slot of the +// account — EIP-7610's CREATE-collision predicate. The tree commits no +// per-account storage root, so the answer is read from the two key regions an +// account owns. The header slots resolve off the same proof path the account's +// own leaves sit on; the storage zone needs a key of its own walked into it, so +// a witness whose keys never enter the zone reads it as empty. +func (s *PBinWitnessState) HasStorage(addr []byte) bool { + // Sub-indices 64..127 are the header's storage slots, which is exactly the + // header stem extended by the two bits pbinHeaderStorageOffset leads with. + stem := s.keys.accountHeaderStem(addr) + header := pbinPathFromBits(append(stem, pbinHeaderStorageOffset), int16(8*len(stem)+2)) + if s.tree.hasSubtree(&header) { + return true + } + zone := pbinPathFromBytes(s.keys.accountStoragePrefix(addr)) + return s.tree.hasSubtree(&zone) +} + +// Code returns the account's bytecode: reassembled from the chunk leaves, or +// read from the DELEGATION leaf for a delegated account. ok is false when the +// witness proves the account absent. +func (s *PBinWitnessState) Code(addr []byte) ([]byte, bool, error) { + code, err := s.ctx.codeFromLeaves(addr) + if err != nil { + return nil, false, err + } + return code, code != nil, nil +} + +// Root applies the block's writes over the witness and returns the post-state +// root. The engine runs against the witness alone, so leaf splitting, branch +// creation, BASIC_DATA packing and code chunking are the ones the chain uses. +func (s *PBinWitnessState) Root(ctx context.Context, plainKeys [][]byte, updates []Update) ([]byte, error) { + if len(plainKeys) != len(updates) { + return nil, fmt.Errorf("pbin: %d plain keys for %d updates", len(plainKeys), len(updates)) + } + trie := NewPBinPatriciaHashed(s.ctx) + defer trie.Release() + + upd := NewUpdates(ModeUpdate, "", trie.setHashSuite(pbinSelectedSum)) + for i := range plainKeys { + upd.TouchPlainKeyDirect(string(plainKeys[i]), &updates[i]) + } + root, err := trie.Process(ctx, upd, "pbin-witness", nil, WarmupConfig{}) + if err != nil { + return nil, err + } + return bytes.Clone(root), nil +} + +// codeFromLeaves is the witness's own answer to "what code does this account +// run". The leaves are the single code source under bin: they are committed by +// the root, and the fold re-chunks every account it touches, so the pruned +// witness carries a chunk leaf wherever the post-state pass needs one. The +// reassembly is checked against the CODE_HASH leaf, so it cannot drift from the +// chunker. A nil result means the witness proves the account absent. +func (c *pbinWitnessContext) codeFromLeaves(addr []byte) ([]byte, error) { + // An account whose nonce, balance and code_size are all zero stores no + // BASIC_DATA leaf, so its absence is zeros rather than an absent account — + // the CODE_HASH or DELEGATION leaf is what marks the account present. + hashValue, hasCodeHash, err := c.tree.leaf(c.keys.accountKey(addr, pbinCodeHashLeafKey)) + if err != nil { + return nil, err + } + + var size uint64 + if basic, ok, err := c.tree.leaf(c.keys.accountKey(addr, pbinBasicDataLeafKey)); err != nil { + return nil, err + } else if ok { + size = uint64(binary.BigEndian.Uint32(basic[pbinBasicDataCodeSizeOffset:])) + } + + if !hasCodeHash { + return c.delegationCode(addr, size) + } + codeHash := common.BytesToHash(hashValue) + if size == 0 { + return []byte{}, nil + } + + code := make([]byte, 0, size) + for chunk := 0; chunk < pbinCodeChunkCount(size); chunk++ { + value, ok, err := c.tree.leaf(c.keys.codeChunkKey(codeHash, chunk)) + if err != nil { + return nil, err + } + if !ok { + // A chunk of 31 zero bytes is stored as no leaf at all, so an absent + // chunk is the zeros it stands for. code_size delimits the code, not + // which chunks are present. + var zero [pbinValueLength]byte + value = zero[:] + } + code = append(code, value[1:]...) + } + code = code[:size] + if got := common.Hash(keccak.Sum256(code)); got != codeHash { + return nil, fmt.Errorf("%w: code of account %x reassembles to %x, the CODE_HASH leaf says %x", + errPBinWitnessNode, addr, got, codeHash) + } + return code, nil +} + +// delegationCode reads a delegated account's code: the indicator its DELEGATION +// leaf carries. There is nothing to reassemble and no hash to check against — +// the root commits the leaf itself — so the leaf's fixed shape is the only thing +// that can be checked, and code_size has to agree with it. A nil result means the +// witness proves the account absent. +func (c *pbinWitnessContext) delegationCode(addr []byte, size uint64) ([]byte, error) { + value, ok, err := c.tree.leaf(c.keys.accountKey(addr, pbinDelegationLeafKey)) + if err != nil || !ok { + return nil, err + } + if size != pbinDelegationCodeLength || len(value) < pbinDelegationCodeLength { + return nil, fmt.Errorf("%w: delegation leaf of account %x holds %d bytes under code_size %d, want %d bytes of indicator", + errPBinWitnessNode, addr, len(value), size, pbinDelegationCodeLength) + } + return bytes.Clone(value[:pbinDelegationCodeLength]), nil +} + +func pbinCodeChunkCount(size uint64) int { + return int((size + pbinChunkDataLen - 1) / pbinChunkDataLen) +} + +// hasSubtree reports whether the witness proves a leaf exists under prefix. +// Unlike leaf, an unresolved hash is not an error here: a pruned witness proves +// only the regions its keys walked, so this answers what the node set can see +// and leaves the rest to read as empty. +func (w *pbinWitnessTree) hasSubtree(prefix *pbinBitpath) bool { + hash, pos := w.root, int16(0) + for { + if hash == pbinEmptyTreeHash { + return false + } + if pos >= prefix.bitLen { + return true + } + node, ok := w.nodes[hash] + if !ok { + return false + } + if node.isLeaf() { + key := pbinPathFromBytes(node.key) + return key.hasPrefix(prefix) + } + limit := min(prefix.bitLen-pos, node.prefix.bitLen) + if pbinCommonPrefixBitsAt(prefix, pos, &node.prefix) != limit { + return false + } + if prefix.bitLen-pos <= node.prefix.bitLen { + return true + } + end := pos + node.prefix.bitLen + hash, pos = node.children[prefix.bit(end)], end+1 + } +} + +// leaf resolves the value at a tree key. found is false when the walk reaches a +// node that proves the key absent — a leaf of another key, or a branch prefix +// the key diverges from. A hash the set carries no preimage for is an error, so +// an unresolved subtree is never read as an absent key. +func (w *pbinWitnessTree) leaf(key []byte) ([]byte, bool, error) { + path, err := pbinWitnessProvedPath(key) + if err != nil { + return nil, false, err + } + hash, pos := w.root, int16(0) + if hash == pbinEmptyTreeHash { + return nil, false, nil + } + for { + node, ok := w.nodes[hash] + if !ok { + return nil, false, fmt.Errorf("%w: no preimage for %x, reached at bit %d of key %x", + errPBinWitnessBlinded, hash, pos, key) + } + if node.isLeaf() { + if !bytes.Equal(node.key, key) { + return nil, false, nil + } + return node.value, true, nil + } + end := pos + node.prefix.bitLen + if end >= path.bitLen || pbinCommonPrefixBitsAt(&path, pos, &node.prefix) != node.prefix.bitLen { + return nil, false, nil + } + hash, pos = node.children[path.bit(end)], end+1 + } +} diff --git a/execution/commitment/pbin_witness_test.go b/execution/commitment/pbin_witness_test.go new file mode 100644 index 00000000000..09a2f4026a9 --- /dev/null +++ b/execution/commitment/pbin_witness_test.go @@ -0,0 +1,389 @@ +// Copyright 2026 The Erigon Authors +// This file is part of Erigon. +// +// Erigon is free software: you can redistribute it and/or modify +// it under the terms of the GNU Lesser General Public License as published by +// the Free Software Foundation, either version 3 of the License, or +// (at your option) any later version. +// +// Erigon is distributed in the hope that it will be useful, +// but WITHOUT ANY WARRANTY; without even the implied warranty of +// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +// GNU Lesser General Public License for more details. +// +// You should have received a copy of the GNU Lesser General Public License +// along with Erigon. If not, see . + +package commitment + +import ( + "bytes" + "context" + "maps" + "testing" + + keccak "github.com/erigontech/fastkeccak" + "github.com/stretchr/testify/require" + + "github.com/erigontech/erigon/common" + "github.com/erigontech/erigon/common/length" +) + +// pbinWitnessRecorder keeps every emission in arrival order, so a node hashed +// more than once stays visible instead of being folded away. +type pbinWitnessRecorder struct { + preimages [][]byte + hashes [][]byte +} + +func (r *pbinWitnessRecorder) onNode(preimage, hash []byte) { + r.preimages = append(r.preimages, bytes.Clone(preimage)) + r.hashes = append(r.hashes, bytes.Clone(hash)) +} + +// byHash folds the emissions into the node set a witness carries and checks the +// property that set relies on: one hash, one preimage. +func (r *pbinWitnessRecorder) byHash(t *testing.T) map[string][]byte { + t.Helper() + out := make(map[string][]byte, len(r.hashes)) + for i, hash := range r.hashes { + if prev, seen := out[string(hash)]; seen { + require.Equal(t, prev, r.preimages[i], "hash %x emitted with two preimages", hash) + continue + } + out[string(hash)] = r.preimages[i] + } + return out +} + +// pbinWitnessRejectingTracer fails the test on any emission; it stands in for a +// tracer that must have been detached. +type pbinWitnessRejectingTracer struct{ t *testing.T } + +func (r *pbinWitnessRejectingTracer) onNode(preimage, hash []byte) { + r.t.Helper() + r.t.Fatalf("detached tracer received node %x", hash) +} + +// pbinWitnessOracleNodes enumerates the reference tree's nodes as +// preimage-by-hash, derived from the corpus rather than from the engine. +func pbinWitnessOracleNodes(t *testing.T, entries []pbinOracleEntry) map[string][]byte { + t.Helper() + var tree pbinOracleTree + for _, e := range entries { + tree.insert(e.key, e.value) + } + out := make(map[string][]byte) + pbinWitnessCollectOracleNodes(t, tree.root, out) + return out +} + +func pbinWitnessCollectOracleNodes(t *testing.T, node pbinOracleNode, out map[string][]byte) []byte { + t.Helper() + if node == nil { + return make([]byte, length.Hash) + } + var preimage []byte + switch n := node.(type) { + case *pbinOracleLeaf: + preimage = append(preimage, pbinOracleLeafTag) + preimage = append(preimage, n.key...) + preimage = append(preimage, n.value...) + case *pbinOracleBranch: + left := pbinWitnessCollectOracleNodes(t, n.left, out) + right := pbinWitnessCollectOracleNodes(t, n.right, out) + preimage = append(preimage, pbinOracleBranchTag) + preimage = append(preimage, pbinOracleEncodeBitPrefix(n.prefix)...) + preimage = append(preimage, left...) + preimage = append(preimage, right...) + default: + t.Fatalf("unknown oracle node %T", node) + } + hash := pbinTestKeccak(t, preimage) + out[string(hash)] = preimage + return hash +} + +// pbinWitnessCorpus spans both zones and carries code, so the emitted set holds +// BASIC_DATA, CODE_HASH, code-chunk and storage leaves. +func pbinWitnessCorpus() *pbinTestCorpus { + c := new(pbinTestCorpus) + c.accountWithCodeBytes(pbinOracleAddr(21), 1, 500, bytes.Repeat([]byte{0x60}, 200)) + c.account(pbinOracleAddr(22), 2, 900, common.Hash{0x22}) + for _, slot := range []uint64{0, 63, 64, 256, 1 << 20} { + c.storage(pbinOracleAddr(21), pbinOracleSlot(slot), 0x11) + c.storage(pbinOracleAddr(22), pbinOracleSlot(slot), 0x22) + } + return c +} + +func pbinWitnessProcess(t *testing.T, corpus *pbinTestCorpus, tracer witnessTracer) ([]byte, *PBinPatriciaHashed) { + t.Helper() + pph, ms := pbinTestEngine(t) + corpus.applyTo(t, ms) + pph.setWitnessTracer(tracer) + return pbinTestProcess(t, pph, corpus.plainKeys, corpus.updates), pph +} + +// TestPBinWitnessTracerNilEmitsNothing: the tap is inert without a tracer, and +// detaching one really detaches it. +func TestPBinWitnessTracerNilEmitsNothing(t *testing.T) { + t.Parallel() + + corpus := pbinWitnessCorpus() + pph, ms := pbinTestEngine(t) + corpus.applyTo(t, ms) + + pph.setWitnessTracer(&pbinWitnessRejectingTracer{t: t}) + pph.setWitnessTracer(nil) + + root := pbinTestProcess(t, pph, corpus.plainKeys, corpus.updates) + require.Equal(t, corpus.oracleRoot(t), root) +} + +// TestPBinWitnessTracerEmitsEveryNode: a traced fold yields every node of the +// tree it builds, each one hashing to the hash it was emitted with, and the +// root is unchanged by the tracing. +func TestPBinWitnessTracerEmitsEveryNode(t *testing.T) { + t.Parallel() + + corpus := pbinWitnessCorpus() + untracedRoot, _ := pbinWitnessProcess(t, corpus, nil) + + rec := new(pbinWitnessRecorder) + root, _ := pbinWitnessProcess(t, corpus, rec) + require.Equal(t, untracedRoot, root) + require.Equal(t, corpus.oracleRoot(t), root) + + tags := map[byte]int{} + for i, preimage := range rec.preimages { + require.NotEmpty(t, preimage) + require.Equal(t, pbinTestKeccak(t, preimage), rec.hashes[i], "emission %d does not hash to its own preimage", i) + tags[preimage[0]]++ + } + require.Positive(t, tags[pbinLeafTag], "no leaf node emitted") + require.Positive(t, tags[pbinBranchTag], "no branch node emitted") + + emitted := rec.byHash(t) + require.Contains(t, emitted, string(root), "root node absent from the emitted set") + for hash, preimage := range pbinWitnessOracleNodes(t, corpus.entries(t)) { + got, ok := emitted[hash] + require.True(t, ok, "node %x of the reference tree was never emitted", hash) + require.Equal(t, preimage, got) + } +} + +// TestPBinWitnessTracerCoversRootLeaf: a one-key tree folds no row, so its only +// node is hashed by RootHash. A tap in foldBranch would emit nothing here. +func TestPBinWitnessTracerCoversRootLeaf(t *testing.T) { + t.Parallel() + + addr, slot := pbinOracleAddr(31), pbinOracleSlot(7000) + corpus := new(pbinTestCorpus).storage(addr, slot, 0x01, 0x02) + + rec := new(pbinWitnessRecorder) + root, pph := pbinWitnessProcess(t, corpus, rec) + require.Equal(t, pbinNodeLeaf, pph.grid.root.kind) + + emitted := rec.byHash(t) + require.Len(t, emitted, 1) + require.Contains(t, emitted, string(root)) + require.Equal(t, byte(pbinLeafTag), emitted[string(root)][0]) +} + +// TestPBinWitnessTracerCoversSiblingCells: the two leaves are hashed by +// hashRowCell during the branch fold, the root by RootHash. All three land in +// the emitted set and nothing else does. +func TestPBinWitnessTracerCoversSiblingCells(t *testing.T) { + t.Parallel() + + addr := pbinOracleAddr(32) + left, right := pbinOracleSlot(256), pbinOracleSlot(257) + corpus := new(pbinTestCorpus).storage(addr, left, 0xAA).storage(addr, right, 0xBB) + + rec := new(pbinWitnessRecorder) + root, pph := pbinWitnessProcess(t, corpus, rec) + require.Equal(t, pbinNodeBranch, pph.grid.root.kind) + + emitted := rec.byHash(t) + require.Contains(t, emitted, string(root)) + require.Equal(t, pbinWitnessOracleNodes(t, corpus.entries(t)), emitted) +} + +// TestPBinWitnessTracerDetachedOnReset keeps the tracer off the normal +// commitment path a reset engine goes back to serving. +func TestPBinWitnessTracerDetachedOnReset(t *testing.T) { + t.Parallel() + + corpus := pbinWitnessCorpus() + pph, ms := pbinTestEngine(t) + corpus.applyTo(t, ms) + + pph.setWitnessTracer(&pbinWitnessRejectingTracer{t: t}) + pph.Reset() + require.Nil(t, pph.hasher.tracer) + + require.Equal(t, corpus.oracleRoot(t), pbinTestProcess(t, pph, corpus.plainKeys, corpus.updates)) +} + +// pbinWitnessCommitted commits the corpus and hands back the state it left +// behind, so a later engine sees a stored tree rather than an empty one. +func pbinWitnessCommitted(t *testing.T, corpus *pbinTestCorpus) (*MockState, []byte) { + t.Helper() + pph, ms := pbinTestEngine(t) + corpus.applyTo(t, ms) + return ms, bytes.Clone(pbinTestProcess(t, pph, corpus.plainKeys, corpus.updates)) +} + +func pbinWitnessesOf(t *testing.T, ms *MockState, upd *Updates, produceExclusionProofs bool) (nodes, provedKeys [][]byte, root []byte) { + t.Helper() + nodes, provedKeys, root, err := NewPBinPatriciaHashed(ms).Witnesses(context.Background(), upd, produceExclusionProofs, "") + require.NoError(t, err) + return nodes, provedKeys, root +} + +// pbinWitnessPending is a corpus of updates against pbinWitnessCorpus that no +// state read can produce: applying them moves the root, so a witness pass that +// applied anything would be caught. +func pbinWitnessPending() *pbinTestCorpus { + c := new(pbinTestCorpus) + c.account(pbinOracleAddr(22), 77, 7777, common.Hash{0x99}) + c.account(pbinOracleAddr(23), 3, 300, common.Hash{0x23}) + c.storage(pbinOracleAddr(21), pbinOracleSlot(64), 0xEE) + c.storage(pbinOracleAddr(23), pbinOracleSlot(5), 0x55) + return c +} + +// TestPBinWitnessesReturnsParentRoot: the pass proves the tree as it stands. +// buildWitnessTrie checks the returned root against the parent block's, so an +// applied update would fail there. +func TestPBinWitnessesReturnsParentRoot(t *testing.T) { + t.Parallel() + + ms, parentRoot := pbinWitnessCommitted(t, pbinWitnessCorpus()) + pending := pbinWitnessPending() + + upd := WrapKeyUpdates(t, ModeUpdate, pbinKeyHasher(), pending.plainKeys, pending.updates) + nodes, _, root := pbinWitnessesOf(t, ms, upd, false) + require.Equal(t, parentRoot, root) + require.NotEmpty(t, nodes) + require.Equal(t, nodes[0], pbinWitnessNodeFor(t, nodes, root), "root node is not first") + + applied := WrapKeyUpdates(t, ModeUpdate, pbinKeyHasher(), pending.plainKeys, pending.updates) + postRoot, err := NewPBinPatriciaHashed(ms).Process(context.Background(), applied, "", nil, WarmupConfig{}) + require.NoError(t, err) + require.NotEqual(t, parentRoot, postRoot, "the pending updates do not move the root, so the test proves nothing") +} + +func pbinWitnessNodeFor(t *testing.T, nodes [][]byte, hash []byte) []byte { + t.Helper() + for _, node := range nodes { + if bytes.Equal(pbinTestKeccak(t, node), hash) { + return node + } + } + t.Fatalf("no captured node hashes to %x", hash) + return nil +} + +// TestPBinWitnessesLeavesStateUntouched: the fold writes each branch row back as +// it goes, and this pass folds rows it never modified. +func TestPBinWitnessesLeavesStateUntouched(t *testing.T) { + t.Parallel() + + ms, _ := pbinWitnessCommitted(t, pbinWitnessCorpus()) + before := maps.Clone(ms.cm) + + pending := pbinWitnessPending() + upd := WrapKeyUpdates(t, ModeUpdate, pbinKeyHasher(), pending.plainKeys, pending.updates) + pbinWitnessesOf(t, ms, upd, false) + + require.Equal(t, before, ms.cm) +} + +// TestPBinWitnessesProvesCodeLeaves: one account touch expands into leaves that +// never reach HashSort. Collecting the proved keys there instead of at the emit +// sink would drop every code leaf from the pruned witness. +func TestPBinWitnessesProvesCodeLeaves(t *testing.T) { + t.Parallel() + + addr := pbinOracleAddr(21) + code := bytes.Repeat([]byte{0x60}, 200) + corpus := pbinWitnessCorpus() + ms, _ := pbinWitnessCommitted(t, corpus) + + upd := WrapKeyUpdates(t, ModeDirect, pbinKeyHasher(), [][]byte{addr}, []Update{{}}) + _, provedKeys, _ := pbinWitnessesOf(t, ms, upd, false) + + proved := make(map[string]struct{}, len(provedKeys)) + for _, key := range provedKeys { + proved[string(key)] = struct{}{} + } + require.Contains(t, proved, string(pbinTreeKeyAccount(addr, pbinBasicDataLeafKey))) + require.Contains(t, proved, string(pbinTreeKeyAccount(addr, pbinCodeHashLeafKey))) + require.Contains(t, proved, string(pbinTreeKeyAccount(addr, pbinDelegationLeafKey)), + "the unconditional delegation-leaf removal walks its key, so the witness must prove it") + + chunks := pbinChunkifyCode(code) + require.Greater(t, len(chunks), 1) + for i := range chunks { + require.Contains(t, proved, string(pbinTreeKeyCodeChunk(keccak.Sum256(code), i)), "code chunk %d is not proved", i) + } + require.Len(t, provedKeys, 3+len(chunks)) +} + +// TestPBinWitnessesExclusionProofsIgnored: the flag materializes the branch an +// extension node hides, and EIP-8297 has none. Node order is the capture map's, +// so the sets are compared rather than the slices. +func TestPBinWitnessesExclusionProofsIgnored(t *testing.T) { + t.Parallel() + + corpus := pbinWitnessCorpus() + pending := pbinWitnessPending() + + msOff, _ := pbinWitnessCommitted(t, corpus) + off := WrapKeyUpdates(t, ModeUpdate, pbinKeyHasher(), pending.plainKeys, pending.updates) + offNodes, offKeys, offRoot := pbinWitnessesOf(t, msOff, off, false) + + msOn, _ := pbinWitnessCommitted(t, corpus) + on := WrapKeyUpdates(t, ModeUpdate, pbinKeyHasher(), pending.plainKeys, pending.updates) + onNodes, onKeys, onRoot := pbinWitnessesOf(t, msOn, on, true) + + require.Equal(t, offRoot, onRoot) + require.Equal(t, offKeys, onKeys) + require.Equal(t, offNodes[0], onNodes[0]) + require.ElementsMatch(t, offNodes, onNodes) +} + +// TestPBinWitnessesEmptyUpdates: nothing is proved, so nothing is captured, and +// the root still has to come back. +func TestPBinWitnessesEmptyUpdates(t *testing.T) { + t.Parallel() + + ms, parentRoot := pbinWitnessCommitted(t, pbinWitnessCorpus()) + + upd := WrapKeyUpdates(t, ModeDirect, pbinKeyHasher(), nil, nil) + nodes, provedKeys, root := pbinWitnessesOf(t, ms, upd, false) + require.Equal(t, parentRoot, root) + require.Empty(t, nodes) + require.Empty(t, provedKeys) +} + +// TestPBinWitnessTracerDetachedOnRelease: a pooled engine that kept its tracer +// would leak the next run's nodes into a finished witness. +func TestPBinWitnessTracerDetachedOnRelease(t *testing.T) { + t.Parallel() + + corpus := pbinWitnessCorpus() + pph, ms := pbinTestEngine(t) + corpus.applyTo(t, ms) + + rec := new(pbinWitnessRecorder) + pph.setWitnessTracer(rec) + pph.Release() + + reused := NewPBinPatriciaHashed(ms) + require.Nil(t, reused.hasher.tracer) + require.Equal(t, corpus.oracleRoot(t), pbinTestProcess(t, reused, corpus.plainKeys, corpus.updates)) + require.Empty(t, rec.hashes) +} diff --git a/execution/commitment/pbin_zerovalue_test.go b/execution/commitment/pbin_zerovalue_test.go index b279a6d69f3..c1e764afab8 100644 --- a/execution/commitment/pbin_zerovalue_test.go +++ b/execution/commitment/pbin_zerovalue_test.go @@ -18,24 +18,25 @@ package commitment import ( "bytes" - "context" + "fmt" + "sort" "testing" + keccak "github.com/erigontech/fastkeccak" "github.com/stretchr/testify/require" "github.com/erigontech/erigon/common" "github.com/erigontech/erigon/common/length" ) -// Zero-vs-absent. The domain encodes both as an absent read, while EIP-8297 has -// no removal and commits a zero value as a present leaf. The engine supplies the -// presence bit the domain lacks: a zeroed slot under a live leaf keeps the leaf -// and commits 32 zero bytes, an absent key with no leaf of its own contributes -// nothing, and an absent account over a live leaf stays refused. +// Zero-vs-absent. EIP-8297 makes them the same state: a leaf whose value is 32 +// zero bytes is not stored, and reads back as the zero it stood for. So the +// domain's shared encoding of the two needs no presence bit, and both a delete +// and a zero write remove the leaf. -// TestPBinStorageDeleteKeepsLeafAsPresentZero covers the update-stream side: the -// zeroed slot is touched, so its leaf is in the grid when the absent read lands. -func TestPBinStorageDeleteKeepsLeafAsPresentZero(t *testing.T) { +// TestPBinStorageZeroWriteRemovesLeaf covers the update-stream side: the zeroed +// slot is touched, so its leaf is in the grid when the absent read lands. +func TestPBinStorageZeroWriteRemovesLeaf(t *testing.T) { t.Parallel() for _, tc := range []struct { @@ -63,23 +64,21 @@ func TestPBinStorageDeleteKeepsLeafAsPresentZero(t *testing.T) { pph.Reset() root := pbinTestProcess(t, pph, zeroed.plainKeys, zeroed.updates) - want := new(pbinTestCorpus). - storage(addr, pbinOracleSlot(tc.gone)). - storage(addr, pbinOracleSlot(tc.kept), 0x02) - require.Equal(t, want.oracleRoot(t), root) - require.NotEqual(t, before, root) - survivorOnly := new(pbinTestCorpus).storage(addr, pbinOracleSlot(tc.kept), 0x02) - require.NotEqual(t, survivorOnly.oracleRoot(t), root, - "a zeroed slot keeps its leaf: dropping it is a different tree") + require.Equal(t, survivorOnly.oracleRoot(t), root, + "a zeroed slot leaves the tree it would have had without the slot") + require.NotEqual(t, before, root) }) } } -// TestPBinStorageDeleteOnUntouchedSiblingIsPresentZero is the same rule reached -// through the fold: the zeroed slot is never touched, so its leaf is rehydrated -// from the branch record and hashed with whatever the state read returns. -func TestPBinStorageDeleteOnUntouchedSiblingIsPresentZero(t *testing.T) { +// TestPBinStorageZeroOnUntouchedSiblingKeepsLeaf pins the fold path, where the +// rule does not yet hold: a slot zeroed without being in the update set is +// rehydrated from its branch record and committed as 32 zero bytes, which under +// the current spec is a state the tree cannot hold. Removal lives on the update +// path only. The domain always carries a zeroed slot in the same block's update +// set, so this is out of reach through ordinary execution. +func TestPBinStorageZeroOnUntouchedSiblingKeepsLeaf(t *testing.T) { t.Parallel() addr := pbinOracleAddr(42) @@ -100,10 +99,16 @@ func TestPBinStorageDeleteOnUntouchedSiblingIsPresentZero(t *testing.T) { pph.Reset() root := pbinTestProcess(t, pph, touched.plainKeys, touched.updates) - want := new(pbinTestCorpus). - storage(addr, pbinOracleSlot(256)). - storage(addr, pbinOracleSlot(257), 0x0B) - require.Equal(t, want.oracleRoot(t), root) + // The zero leaf is not a state entries() can express, since it filters zeros. + survivor := new(pbinTestCorpus).storage(addr, pbinOracleSlot(257), 0x0B) + withZeroLeaf := append(survivor.entries(t), pbinOracleEntry{ + key: pbinTreeKeyStorage(addr, pbinOracleSlot(256)), + value: make([]byte, pbinValueLength), + }) + want := pbinOracleRoot(withZeroLeaf) + require.Equal(t, want[:], root) + + require.NotEqual(t, survivor.oracleRoot(t), root, "the leaf survives as a zero") } func TestPBinLoadCellStateAbsentRead(t *testing.T) { @@ -138,33 +143,49 @@ func TestPBinLoadCellStateAbsentRead(t *testing.T) { }) } -// TestPBinAccountRemovalStillRefused keeps the refusal in place: turning an -// absent account into a zero-valued BASIC_DATA leaf would be consistent with -// eip:345-347, but it is not verified against the reference and would silently -// change the root. -func TestPBinAccountRemovalStillRefused(t *testing.T) { +// TestPBinAccountRemovalDropsBothSubtrees: an account owns its header stem and +// its storage prefix, and removing it removes those two subtrees whole — header +// storage slots included, and storage the fold was handed no list of. Its code +// chunks are content-addressed and shared, so they stay, and a bystander +// account must survive untouched. +func TestPBinAccountRemovalDropsBothSubtrees(t *testing.T) { t.Parallel() - addr := pbinOracleAddr(45) - stored := new(pbinTestCorpus).account(addr, 3, 7, common.Hash{0x45}) + addr, bystander := pbinOracleAddr(45), pbinOracleAddr(48) + code := bytes.Repeat([]byte{0x01}, 31*4) + stored := new(pbinTestCorpus). + accountWithCodeBytes(addr, 3, 7, code). + storage(addr, pbinOracleSlot(5), 0x01). // header window + storage(addr, pbinOracleSlot(256), 0x02). // storage zone + storage(addr, pbinOracleSlot(1<<20), 0x03). + account(bystander, 1, 2, common.Hash{0x48}) pph, ms := pbinTestEngine(t) - require.NoError(t, ms.applyPlainUpdates(stored.plainKeys, stored.updates)) + stored.applyTo(t, ms) pbinTestProcess(t, pph, stored.plainKeys, stored.updates) - require.NoError(t, ms.applyPlainUpdates(stored.plainKeys, []Update{{Flags: DeleteUpdate}})) + removal := new(pbinTestCorpus).account(addr, 0, 0, common.Hash{}) + require.NoError(t, ms.applyPlainUpdates(removal.plainKeys, []Update{{Flags: DeleteUpdate}})) + pph.Reset() - upd := WrapKeyUpdates(t, ModeDirect, pbinKeyHasher(), stored.plainKeys, stored.updates) - _, err := pph.Process(context.Background(), upd, "", nil, WarmupConfig{}) - require.ErrorIs(t, err, errPBinDeleteUnsupported) + root := pbinTestProcess(t, pph, removal.plainKeys, removal.updates) + + survivor := new(pbinTestCorpus).account(bystander, 1, 2, common.Hash{0x48}) + want := survivor.entries(t) + codeHash := keccak.Sum256(code) + for i, chunk := range pbinChunkifyCode(code) { + want = append(want, pbinOracleEntry{key: pbinTreeKeyCodeChunk(codeHash, i), value: chunk[:]}) + } + wantRoot := pbinOracleRoot(want) + require.Equal(t, wantRoot[:], root, + "nothing of the removed account's own subtrees may survive, and nothing of the other may go") } -// TestPBinFoldDeleteUnreachableFromProcess pins that foldDelete stays off the -// Process path, since it collapses nodes the reference leaves in place. Its only -// observable is the zero-length record it writes at a bit-path key — storeRoot -// makes the sole other zero-length write, and only at the root key — so a run -// that zeroes every leaf it stored must produce none. -func TestPBinFoldDeleteUnreachableFromProcess(t *testing.T) { +// TestPBinFoldDeleteRunsOnProcess: removing the last leaf of a subtree collapses +// it, and the collapse is observable as the zero-length record foldDelete writes +// at a bit-path key. storeRoot makes the sole other zero-length write, and only +// at the root key. +func TestPBinFoldDeleteRunsOnProcess(t *testing.T) { t.Parallel() addr := pbinOracleAddr(46) @@ -182,10 +203,9 @@ func TestPBinFoldDeleteUnreachableFromProcess(t *testing.T) { zeroed, want := new(pbinTestCorpus), new(pbinTestCorpus) for _, slot := range slots { zeroed.storage(addr, pbinOracleSlot(slot)) - want.storage(addr, pbinOracleSlot(slot)) } - // An absent key with no leaf of its own contributes nothing and leaves no - // empty row behind — the case a zero write must not be confused with. + // An absent key with no leaf of its own contributes nothing — the case a zero + // write over a live leaf must not be confused with. zeroed.storage(addr, pbinOracleSlot(1<<20)) want.account(pbinOracleAddr(47), 1, 2, common.Hash{0x47}) @@ -197,8 +217,59 @@ func TestPBinFoldDeleteUnreachableFromProcess(t *testing.T) { root := pbinTestProcess(t, pph, zeroed.plainKeys, zeroed.updates) require.Equal(t, want.oracleRoot(t), root) - require.NotEmpty(t, ctx.puts) + var collapsed int for _, put := range ctx.puts { - require.NotEmpty(t, put.data, "zero-length record at %x: foldDelete ran", put.prefix) + if len(put.data) == 0 { + collapsed++ + } + } + require.NotZero(t, collapsed, "every stored leaf was zeroed, so subtrees must collapse") +} + +// TestPBinCollapsedRowLeavesNoRecord: removing one of a branch's two children +// collapses the row into its survivor, and the record the row was unfolded from +// has to go with it — an incremental removal must store exactly the records a +// rebuild of the same state stores. +func TestPBinCollapsedRowLeavesNoRecord(t *testing.T) { + t.Parallel() + + addr, bystander := pbinOracleAddr(51), pbinOracleAddr(52) + stored := new(pbinTestCorpus). + account(bystander, 1, 2, common.Hash{0x52}). + storage(addr, pbinOracleSlot(256), 0x01). + storage(addr, pbinOracleSlot(257), 0x02) + + pph, ms := pbinTestEngine(t) + stored.applyTo(t, ms) + pbinTestProcess(t, pph, stored.plainKeys, stored.updates) + + zeroed := new(pbinTestCorpus).storage(addr, pbinOracleSlot(257)) + require.NoError(t, ms.applyPlainUpdates(zeroed.plainKeys, []Update{{Flags: DeleteUpdate}})) + pph.Reset() + root := pbinTestProcess(t, pph, zeroed.plainKeys, zeroed.updates) + + survivors := new(pbinTestCorpus). + account(bystander, 1, 2, common.Hash{0x52}). + storage(addr, pbinOracleSlot(256), 0x01) + require.Equal(t, survivors.oracleRoot(t), root) + + _, fresh := pbinTestEngine(t) + survivors.applyTo(t, fresh) + freshEngine := NewPBinPatriciaHashed(fresh) + defer freshEngine.Release() + pbinTestProcess(t, freshEngine, survivors.plainKeys, survivors.updates) + + require.Equal(t, pbinLiveRecordKeys(fresh), pbinLiveRecordKeys(ms), + "the collapsed row's record outlived the node it described") +} + +func pbinLiveRecordKeys(ms *MockState) []string { + keys := make([]string, 0, len(ms.cm)) + for prefix, data := range ms.cm { + if len(data) > 0 { + keys = append(keys, fmt.Sprintf("%x", prefix)) + } } + sort.Strings(keys) + return keys } diff --git a/execution/commitment/testdata/binary_trie_vectors.json b/execution/commitment/testdata/binary_trie_vectors.json new file mode 100644 index 00000000000..9450f944051 --- /dev/null +++ b/execution/commitment/testdata/binary_trie_vectors.json @@ -0,0 +1,777 @@ +{ + "source": "ethereum/execution-specs projects/binary-trie", + "source_commit": "58faeb09b95fd022200974c7bf8a6c3e84712c25", + "trie_roots": [ + { + "name": "empty", + "entries": [], + "root": "0x0000000000000000000000000000000000000000000000000000000000000000" + }, + { + "name": "single_leaf", + "entries": [ + { + "key": "0x00000000000000000000000000000000000000000000000000000000000000000000", + "value": "0x0101010101010101010101010101010101010101010101010101010101010101" + } + ], + "root": "0x4b60a28dce9f3529d103a26e00fadb98514cbd16ce03b7df752426addef9bbc7" + }, + { + "name": "single_leaf_one_byte_key", + "entries": [ + { + "key": "0xab", + "value": "0x0101010101010101010101010101010101010101010101010101010101010101" + } + ], + "root": "0x2ebeea9f8e2e4bbf6e4ff1b4cf8afbb641d4dfaf9a05469dea3787bbc35188d5" + }, + { + "name": "two_leaves_diverge_first_bit", + "entries": [ + { + "key": "0x00111111111111111111111111111111111111111111111111111111111111111111", + "value": "0x0101010101010101010101010101010101010101010101010101010101010101" + }, + { + "key": "0x80111111111111111111111111111111111111111111111111111111111111111111", + "value": "0x0202020202020202020202020202020202020202020202020202020202020202" + } + ], + "root": "0x57210f2156bafa91dc33b7528fcfdb50660b902494b80a00347b28949df72816" + }, + { + "name": "two_leaves_diverge_last_bit", + "entries": [ + { + "key": "0x22222222222222222222222222222222222222222222222222222222222222222200", + "value": "0x0101010101010101010101010101010101010101010101010101010101010101" + }, + { + "key": "0x22222222222222222222222222222222222222222222222222222222222222222201", + "value": "0x0202020202020202020202020202020202020202020202020202020202020202" + } + ], + "root": "0x606cacfcbf218928a25e67e40c8fa9cdf44d15cbc39be41da210cc86db128be9" + }, + { + "name": "three_leaves_shared_prefix", + "entries": [ + { + "key": "0xf0000000000000000000000000000000000000000000000000000000000000000000", + "value": "0x0101010101010101010101010101010101010101010101010101010101010101" + }, + { + "key": "0xf1000000000000000000000000000000000000000000000000000000000000000000", + "value": "0x0202020202020202020202020202020202020202020202020202020202020202" + }, + { + "key": "0x0f000000000000000000000000000000000000000000000000000000000000000000", + "value": "0x0303030303030303030303030303030303030303030303030303030303030303" + } + ], + "root": "0x50ca5b44506c7aeac67017eef1be8977c69d8d1074b3c870ce9fc6ef0aa18163" + }, + { + "name": "mixed_key_lengths_34_and_66", + "entries": [ + { + "key": "0x00aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa05", + "value": "0x0101010101010101010101010101010101010101010101010101010101010101" + }, + { + "key": "0xffbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbb07", + "value": "0x0202020202020202020202020202020202020202020202020202020202020202" + } + ], + "root": "0x117ecc342fcf3753397737026c26e522b9b23e61cfb4c8aafa0b1c98ca5d507b" + }, + { + "name": "overwrite_takes_last_value", + "entries": [ + { + "key": "0x42424242424242424242424242424242424242424242424242424242424242424242", + "value": "0x0101010101010101010101010101010101010101010101010101010101010101" + }, + { + "key": "0x42424242424242424242424242424242424242424242424242424242424242424242", + "value": "0x0202020202020202020202020202020202020202020202020202020202020202" + } + ], + "root": "0xe6817b5d8351669a295e51a0ae8459ace26faeb8904b8d9c7e3a00b9f343e0eb" + }, + { + "name": "random_50_keys_seed_8297", + "entries": [ + { + "key": "0x2aa6a8996ce6a78ab232d4ea1c1773f4216f5c6c16e580f784d1a03c7c4069f1b259", + "value": "0x602a3e5c20f394f60ad655f5a52a61487ce7121bc116b2d0036ad7e47298ab30" + }, + { + "key": "0xc44a5069709f82e5cfa1fdb523a09cbf72345d149135921a5ff4c22b590d5a7c6b32", + "value": "0x3fedde8f23715681af22f74b0d34825fb0cd1bb1a530e7e22b99c856937c3878" + }, + { + "key": "0x1d2c29c5a940de63446ffd493abe5469486948d20bcaf06d586dcd3e28507dbe4a4e", + "value": "0x557de7c40c7061a0e096cb67ee0d347c7e35b9d5fe896395ce2c79333c56e171" + }, + { + "key": "0xc62c92521ebf446fb986f84c4f8ea43e59ebf9744a30fccbea4ff32d0d2bd8c41568", + "value": "0x2cabc800545add40c21822901272b48ea6cccfa1574152725f54f504571b5bd3" + }, + { + "key": "0x4f0f6748864445c4f13e4c69d6b258bab15c7dde77da8d296c8c4de46cffb7518235", + "value": "0x11f71843a4e7dcc6fbe826921ed5bf7ff0db9a856f786c5e7662fcb016eb22ae" + }, + { + "key": "0xadbf5828b2759dea65fa9feef2e0580242769f423e60877107826d8de9fe19d6b77f", + "value": "0xecc05f6a68a2a05d955d25c1d2fa2d2c7d9b3948c70e4ba2117150041d162885" + }, + { + "key": "0x2a95680f5f67f0e0049573d9a08353549a058cc5fb50a87b2cadb92edd48f96b70a9", + "value": "0x52afae073e434b41f054a2d42d0783aaf89ab9cd5c1a96ba37d80fc6c222aa2a" + }, + { + "key": "0x7ca6b423f8f439ea5333e201ead1da2c3f3526aa0a4ea90c3467a4cbf3633c14a232", + "value": "0x2ea42ec85d724ce2cae8ff06532b670f94c7d0fffc6651d893d3dfaa605ba04c" + }, + { + "key": "0xd90768dee20dc8e1b17cb000c02d336c5e9115546fcf635a89e02aa72fcb4c35c079", + "value": "0x0ea636b852ecea23cbbf3600bed740a8a944adb3588d8d3f6d6cfe280b1d93a3" + }, + { + "key": "0x876a714a8fc09271610ec4abaf2ecc3ef7504527a6971bbcbd9cc0e0b1399fe933dc", + "value": "0x0fc1f11caa9f7ced770170dcf0c6006c014e5b8598c7e5c268a4203e207eeb9d" + }, + { + "key": "0x76b13611dabe295dc7e8df8402f1d43193a7891998570d7994a579186320cde1a7cf", + "value": "0xd4be299747047c39c7883b32836aed86acd8837f825a22bf260edf37cd998552" + }, + { + "key": "0xb537a077bf364c9e7e61842b3bdb47fc91e28e47954bc8cacb0a5eeeb0d0a32e09c7", + "value": "0x0d6c809686697d9ac4b0a6a4307459b6004e432933a5840e51aea8039deb341e" + }, + { + "key": "0x9581e69689729b644ddc033c7250173bde751225b7aae0bf279121fefe484499f225", + "value": "0xe0b5acd5c7b81bc11b2f923cb23b9725af51184dc9446950d59b3ccabad4336f" + }, + { + "key": "0x13af92c620778540042f70141701f7f364598d6ebb2822006938ca9559dcc9a5b0bd", + "value": "0x65665ad386602970d3d6194340f0e2dd673990c2eed2c11b5f7630cbf1ebef6e" + }, + { + "key": "0x30c89c776af10ea0a5d693cdc56e7cc26caef5a3d5dcc6a5e7f529004ef34608e2bb", + "value": "0xb10acf3de8bb5c127b7e898d2d31468eabf047610137763ef1327d56acf2c361" + }, + { + "key": "0x377ccaab099c7cfa7f22f0e82270e6bcb3e67c35f760c44d3f8d61cce78f37d3f5d6", + "value": "0xea6cd8bbfb9089722a00b8fe12de059493772ebda54edddf1163b16d90d118d8" + }, + { + "key": "0xe5c6b60b227487dabbcca806225c6a0ad42af15e465cb547595a8c185c87806741e1", + "value": "0xe127f236c860d50fc559abe9c734af3fd89099f4d9c3aea506237360ff72cbe3" + }, + { + "key": "0x952500b8e339aa344675e038772785efa67a9e820ff6c94f56f65c63056408e3351a", + "value": "0xa0b3499ec1f8120541ebaea047b0b6eec03967d9621b5136462c034aeb9fe483" + }, + { + "key": "0xedac6f4248259eab137c0d516cc36c9e43e8260444cc6d43200b4fc1a04e715897fc", + "value": "0xbf7049c0cdd86648fdfb8df979de2c5f28ce252ffb440132bad842de3d530aba" + }, + { + "key": "0xe97686bfc1d82e4195c44c37c63822b9a5790f13b85dc7fbad6e98e41ddb8da26fcf", + "value": "0xeed3aadd8b0587817d34d2de3c960c2c97d3f463e71d4b01e1e7672dbc615677" + }, + { + "key": "0x5e49a3cff08549e40d6bfa45cf4751e681323a0a90747af1fbf2a09029d4054c425d", + "value": "0x11ce34b22799c2e56c0fca48683d2befc6a3424a2c8808ed8f80f02f0bf74107" + }, + { + "key": "0xd49a56c989b8250924ace1b95496057c7e79a48be406ff0f033e9867114d5b28eefe", + "value": "0x4221245e55f174c502b407002d59f809841fae554c37b2c28bf5a56307ba4f4b" + }, + { + "key": "0xb8ec5602446a4bf77aa7f76e2d9080611acf14d59e868273574320059a7cce2fc56f", + "value": "0xc0ac6eebfbbb6650e31532413e0e2707a9724cf3b85a45b1e1a524079bfed6f6" + }, + { + "key": "0x63b5fad598b5ac70ac16669d0674b92588710e1f9eb4ecba96ffd3fca62bc81b9fd4", + "value": "0xdc23fe99d39f5d3d5c1fb403a1fa18f537565f634f9750296b07eed92ac295e9" + }, + { + "key": "0x41580a465c0b267026210bdaac106834115822dbf82975cb3187846c0750150b7da8", + "value": "0xc4350d9d1e38ea02d65518957de948ba86b3366e076fd7c2f701b50efa613601" + }, + { + "key": "0x40ee13ffd4d197e1661c65006cbb0dd5728e8236565299eb84063809b26a4e66c23c", + "value": "0xbaf01926523cff84ff29b32d4d19ff4e1fb222ea0187323675b04357c2bd659a" + }, + { + "key": "0xb2d9269956fe58d83f174b8d9fa80d6f7cf195b3e062ae55bd3cdb53d92308b4bd29", + "value": "0x939ed45a7cd62584b0ce4c549609bfdc80de4c33091f5a4d4062d07c09b3573e" + }, + { + "key": "0x5fcfe5231af181ca8711c3e3638ca8f9a6825431131840eccb2e00f8f341eb0e1f33", + "value": "0xe159bb37bea013607a9568c8c5c9c55129fca8aed6f619a2ffc14f29699dcc28" + }, + { + "key": "0x45dc75b619130f903c33151209e131665173d6da28f554a549a5563ad40bd8fa7a40", + "value": "0xeda30ecb83f08dc40f4c8b892b55ea3e8e30e11a67bf20a32c3015668b3797f2" + }, + { + "key": "0x4d2957cf1e0ab2d3bc27c3a7935b3390face7d07c5d73581a6082730c563265a3589", + "value": "0xf6c96dfd3b2ca036f2f025f1103ad5f23e653a5f0964b46355b892f3eab8facd" + }, + { + "key": "0x933625c1c382f8a83e243d584346e3b94e14b0ac5427cb84a0580b4568d2c91aa706", + "value": "0xe33ffa5d02f23d96e83344e80ccc48fca6036c87a4bac368a24ec28a26bb70c3" + }, + { + "key": "0x20455de6ec9ad2b33b187e1738be0e6b41c299ce96940837e49eea316ad1b6b825ee", + "value": "0x82dfb2bcce1e0f9d8b0230c432d05fbb6aaea5dcf13d7ab0709b89b1ceefb745" + }, + { + "key": "0xfda49bac1959069e75394a653a4e44575539f06deafaa084702605e71c00c9b8e81a", + "value": "0x0f626cda6806b58620931dad579dcb887592e8b46f94acf6831b7d3e0bf9076f" + }, + { + "key": "0xef257a2eb664173e42d4ae098eb635434a74c3c37fab969be2e758b1ca0c3a4d390d", + "value": "0x091b81cb0449892c17511cf1529e2f2666af2671b68d155488de18621f0ad1f8" + }, + { + "key": "0xfa3082b0bd39b1fb7011212ae0c3b8b78f6ce9450069eab37c7ffce74ec93d28e949", + "value": "0x300042c9c0d21bab7634484dfa54ae0dd4d579d23107c2c23f805cdcab1f9bc3" + }, + { + "key": "0x0f35b7fd43411a98bd2f77d1d75f9a44b6fd9ae514b9e12a9c94cf5cdefc3460d5d2", + "value": "0xa2d8179579efa9284a8c5d602825290471c426b06f648cdab864615d7ea33e37" + }, + { + "key": "0x5ae984fa36087b9eead78eafd1219a682441ee126a32522e5dd3f1d0a8555e9a3ca4", + "value": "0x2ccb3878ecf6016742eeb5cf1fa88186c65fad7f59e0519510311bc361d4dee4" + }, + { + "key": "0x62fcb1d41412ffc7c74768f39558646479095f648d3fd045ef2d857ea4740752e6e3", + "value": "0x09af31d454b6c1aa915dc28e5ab362396feb89ede60a5d617f14b3de0edc7012" + }, + { + "key": "0xacc4a501bfd10b9d890407484618059b0dd03ed7c759ca881847e201b4d7f326e233", + "value": "0xc5671cb9b0fb74823fd88bf2df0795602f8532b33ea7cb06a90bb6bc4d64013a" + }, + { + "key": "0x5d3305b1d38d84f22ab72f4cff4997f347e91801e2f01337cdae981e9224258a21ce", + "value": "0x8d578546affb0976f291db18f6ebe99e9dc47ec625003f7941861ac52dda72ff" + }, + { + "key": "0x9345d08e09992493c0f9d2f0b1181ed7a123879a02af0bd7c152f6ad75792efa6f96", + "value": "0xcc59af9043525c027d6c392a62f5f2c5d4626f6b70c7c155d5dbfbec73535f1c" + }, + { + "key": "0x334fbdf232ba304810bb66b0d99caf22fc6b9e35ccb1e470688157c5d767029b3add", + "value": "0x2f5470a09aa7e14347be86198b30ea13d6dfba5ebc2d406a8380141ca4993ecd" + }, + { + "key": "0x5f0fb320d16cf97ef07303475f28e0f72b93ae9689be3fe9c50d4bcd2e13e5456fa2", + "value": "0x31c23f0aa04789347e82acd04248dde92bd11674591f24e09d570cc6a782df11" + }, + { + "key": "0x8db965afa0f3685791b7747fac9b55fe6c9dc680654ceb7bd346be02037810e8cb4b", + "value": "0x8a9ed5de1a61fd15b89879d1f6e7d2dfe53238d9bdd9e0af088d05cb1427c966" + }, + { + "key": "0x3cbafb9218233c982e5c84fda1ded1146c2b208160213ea77ed397c125a55310204e", + "value": "0x9e4483e2abc56cbf79b1ea996ec0aacb171ab36f85643bf0f3121a6185964b72" + }, + { + "key": "0x353d69bbc4438bbae4f1006fb980dba7537ecb1614e8d3c123a678720ca9172144dc", + "value": "0x83af19ff2f7f5c89be64c268a9b01837c26e134a170969bad4bcea74f7ee2c1b" + }, + { + "key": "0xcd492f6376b541704fcbd979aaf13147eace07f547497470761e2c980b3fcd46ce44", + "value": "0x94355b1cbd394a0e65112e6eaa08c5293c0b82eb44b89b5eedbb433b74a8e9ed" + }, + { + "key": "0xec2be3bc6b4bdf9944757aa679fefc6f00a3e534ccf1f5045163d94a992d439c20d7", + "value": "0x4f2581fc51143abd11b62ad6de6fd4f3aaf1fe1a2f40a2b882c990c3db4b3c7e" + }, + { + "key": "0x770900a7fbc1f91e3bb2d425f8f65126f69b1cb601962360a44deb58208ab5c18b4d", + "value": "0x53fb7e26d5248117e143ba30c50481564cf4c02b3fa7ad41462423ca237cf851" + }, + { + "key": "0x11d51e7adf1f2771881bb2f625df7b201ab7719e6cbcf565e28b82bb899709d8581a", + "value": "0x9e428672e42d43194d4c9e84e2c9be48b1f0293bc983a847e38ee4743406631c" + } + ], + "root": "0xd966e4d5b3676b62c732a8c267753f375226322ec44b1c1d4f8f8c40de77e9be" + } + ], + "embedding": { + "address20": "0x00112233445566778899aabbccddeeff00112233", + "address32": "0x00000000000000000000000000112233445566778899aabbccddeeff00112233", + "basic_data_key": "0x00f4e42504054ae2ba2c9aab59b7cafad1e3df583c385d10fcb8ab0a0ab82e7a0800", + "code_hash_key": "0x00f4e42504054ae2ba2c9aab59b7cafad1e3df583c385d10fcb8ab0a0ab82e7a0801", + "delegation_key": "0x00f4e42504054ae2ba2c9aab59b7cafad1e3df583c385d10fcb8ab0a0ab82e7a0802", + "storage_slot_keys": { + "0": "0x00f4e42504054ae2ba2c9aab59b7cafad1e3df583c385d10fcb8ab0a0ab82e7a0840", + "1": "0x00f4e42504054ae2ba2c9aab59b7cafad1e3df583c385d10fcb8ab0a0ab82e7a0841", + "63": "0x00f4e42504054ae2ba2c9aab59b7cafad1e3df583c385d10fcb8ab0a0ab82e7a087f", + "64": "0xfff4e42504054ae2ba2c9aab59b7cafad1e3df583c385d10fcb8ab0a0ab82e7a08b7b7ba8d57e997347b504830cfb1837de0bb46da8c5c53654442588e0ca0bdbf40", + "255": "0xfff4e42504054ae2ba2c9aab59b7cafad1e3df583c385d10fcb8ab0a0ab82e7a08b7b7ba8d57e997347b504830cfb1837de0bb46da8c5c53654442588e0ca0bdbfff", + "256": "0xfff4e42504054ae2ba2c9aab59b7cafad1e3df583c385d10fcb8ab0a0ab82e7a085b15bdc9241d79c981f6bf4ae56cf1e77d1f9350cf73372e6d792c1c6eb13b3000", + "511": "0xfff4e42504054ae2ba2c9aab59b7cafad1e3df583c385d10fcb8ab0a0ab82e7a085b15bdc9241d79c981f6bf4ae56cf1e77d1f9350cf73372e6d792c1c6eb13b30ff", + "512": "0xfff4e42504054ae2ba2c9aab59b7cafad1e3df583c385d10fcb8ab0a0ab82e7a0808fc1122a8c0a65fbdf45fd999b8b9a4f1e09fd74bf5cc02c51d701972a861b000", + "1606938044258990275541962092341162602522202993782792835301376": "0xfff4e42504054ae2ba2c9aab59b7cafad1e3df583c385d10fcb8ab0a0ab82e7a08de40820dcd8994eedc4c374fd005321c147c12928ed5a191b1a0406bd81a0d7000" + }, + "code_chunk_keys": { + "0": "0x01348ace48ac0a7316c5dee2e4e5a680ba01413aa18e07403f3c00b4f82f2da55800", + "1": "0x01348ace48ac0a7316c5dee2e4e5a680ba01413aa18e07403f3c00b4f82f2da55801", + "255": "0x01348ace48ac0a7316c5dee2e4e5a680ba01413aa18e07403f3c00b4f82f2da558ff", + "256": "0x01d2482e6e552436a97975ef92aba4baec1ea2f25a2188a2aac744c6d86ac7e3de00", + "257": "0x01d2482e6e552436a97975ef92aba4baec1ea2f25a2188a2aac744c6d86ac7e3de01", + "511": "0x01d2482e6e552436a97975ef92aba4baec1ea2f25a2188a2aac744c6d86ac7e3deff", + "512": "0x015b73478e5bf9061bc84d75522ca707958946ef691b0a72194afbcb7089aa761d00", + "2114": "0x01ced5e67b5c39c6dd7dfdcf6c13447e7cbe9c2cfdb73ac5059517b389187134d942" + }, + "code_hash": "0xbcc90f2d6dada5b18e155c17a1c0a55920aae94f39857d39d0d8ed07ae8f228b" + }, + "chunkify_code": [ + { + "name": "empty", + "code": "0x", + "chunks": [] + }, + { + "name": "stop_padded", + "code": "0x00", + "chunks": [ + "0x0000000000000000000000000000000000000000000000000000000000000000" + ] + }, + { + "name": "eip_example_push4_boundary", + "code": "0x010101010101010101010101010101010101010101010101010101010163aabbccdd01010101010101010101", + "chunks": [ + "0x00010101010101010101010101010101010101010101010101010101010163aa", + "0x03bbccdd01010101010101010101000000000000000000000000000000000000" + ] + }, + { + "name": "push32_at_chunk_end_spills_31", + "code": "0x0101010101010101010101010101010101010101010101010101010101017f000102030405060708090a0b0c0d0e0f101112131415161718191a1b1c1d1e1f0101010101", + "chunks": [ + "0x000101010101010101010101010101010101010101010101010101010101017f", + "0x1f000102030405060708090a0b0c0d0e0f101112131415161718191a1b1c1d1e", + "0x011f010101010100000000000000000000000000000000000000000000000000" + ] + } + ], + "encode_basic_data": [ + { + "code_size": 0, + "nonce": 0, + "balance": "0x0", + "encoded": "0x0000000000000000000000000000000000000000000000000000000000000000" + }, + { + "code_size": 1234, + "nonce": 42, + "balance": "0xde0b6b3a7640000", + "encoded": "0x00000000000004d2000000000000002a00000000000000000de0b6b3a7640000" + }, + { + "code_size": 4294967295, + "nonce": 18446744073709551615, + "balance": "0xffffffffffffffffffffffffffffffff", + "encoded": "0x00000000ffffffffffffffffffffffffffffffffffffffffffffffffffffffff" + } + ], + "pbt_state": [ + { + "name": "empty_state", + "accounts": {}, + "root": "0x0000000000000000000000000000000000000000000000000000000000000000" + }, + { + "name": "single_eoa", + "accounts": { + "0xaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa": { + "nonce": 3, + "balance": "0xde0b6b3a7640000", + "code": "0x", + "code_hash": "0xc5d2460186f7233c927e7db2dcc703c0e500b653ca82273b7bfad8045d85a470", + "storage": {} + } + }, + "root": "0x61442bd142d34312a0b3e6216c0f08422f3c32a659bff2880bc74afa83fa880d" + }, + { + "name": "eoa_zero_nonce_and_balance", + "accounts": { + "0xaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa": { + "nonce": 0, + "balance": "0x0", + "code": "0x", + "code_hash": "0xc5d2460186f7233c927e7db2dcc703c0e500b653ca82273b7bfad8045d85a470", + "storage": {} + } + }, + "root": "0x6a065b1de86242ec9f94d244dc2d53cfd8f3739426b88c463748bb850f1351d8" + }, + { + "name": "code_with_push_data_spill", + "accounts": { + "0xaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa": { + "nonce": 1, + "balance": "0x10000000000000000", + "code": "0x0101010101010101010101010101010101010101010101010101010101017f000102030405060708090a0b0c0d0e0f101112131415161718191a1b1c1d1e1f0101010101", + "code_hash": "0x13c3c160f495b78b684f963ea72682524a2e1d1e24b612a40ff9f04a592cedf1", + "storage": {} + } + }, + "root": "0xd1b803b37f66213a264ef88057e472d66cf76fefaf05ad0b9d71045a1412bd84" + }, + { + "name": "code_and_boundary_storage", + "accounts": { + "0xaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa": { + "nonce": 1, + "balance": "0x0", + "code": "0x010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101", + "code_hash": "0x70c8f40cf22ebddfdd1c66a0e1891a29f4ae670f4ca055f379a106209e074165", + "storage": { + "63": "0x0000000000000000000000000000000000000000000000000000000000000001", + "64": "0x0000000000000000000000000000000000000000000000000000000000000002", + "256": "0x0000000000000000000000000000000000000000000000000000000000000003" + } + } + }, + "root": "0x84d204064e6f2d3f8862bf399d9c1d7eb46a47d041930beec3c1d1dd124e6bc8" + }, + { + "name": "code_across_the_group_boundary", + "accounts": { + "0xaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa": { + "nonce": 1, + "balance": "0x0", + "code": "0x0101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101", + "code_hash": "0x84a8136016d2be33610963c8193847a50c9a06787aa7ac7b2b0a1bf5069b4501", + "storage": {} + } + }, + "root": "0x2535d4a3b50552eb629d8ee2aa1479ad45eac1ee1dd035f036301a5b53a898d3" + }, + { + "name": "storage_across_the_header_boundary", + "accounts": { + "0xaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa": { + "nonce": 1, + "balance": "0x0", + "code": "0x", + "code_hash": "0xc5d2460186f7233c927e7db2dcc703c0e500b653ca82273b7bfad8045d85a470", + "storage": { + "0": "0x0000000000000000000000000000000000000000000000000000000000000001", + "1": "0x0000000000000000000000000000000000000000000000000000000000000002", + "63": "0x0000000000000000000000000000000000000000000000000000000000000003", + "64": "0x0000000000000000000000000000000000000000000000000000000000000004", + "255": "0x0000000000000000000000000000000000000000000000000000000000000005", + "256": "0x0000000000000000000000000000000000000000000000000000000000000006", + "115792089237316195423570985008687907853269984665640564039457584007913129639935": "0x0000000000000000000000000000000000000000000000000000000000000007" + } + } + }, + "root": "0xe35d57fc71e60a19fac7599ea972cee07c2102bdf4f5e23b8cffddd9e107f200" + }, + { + "name": "zero_storage_slot_is_absent", + "accounts": { + "0xaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa": { + "nonce": 1, + "balance": "0x0", + "code": "0x", + "code_hash": "0xc5d2460186f7233c927e7db2dcc703c0e500b653ca82273b7bfad8045d85a470", + "storage": { + "7": "0x0000000000000000000000000000000000000000000000000000000000000000", + "8": "0x0000000000000000000000000000000000000000000000000000000000000009", + "300": "0x0000000000000000000000000000000000000000000000000000000000000000" + } + } + }, + "root": "0x3b49e87b5b3c049828dd881ec4cbf355d0fab3813c58b785dd6519d314bdda96" + }, + { + "name": "full_header_occupancy", + "accounts": { + "0xaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa": { + "nonce": 1, + "balance": "0x0", + "code": "0x0101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101", + "code_hash": "0xe360036fb72811ee2775a32b4a10ea2b47029d6ed28783c5f7b151d7fda1a938", + "storage": { + "0": "0x0000000000000000000000000000000000000000000000000000000000000001", + "1": "0x0000000000000000000000000000000000000000000000000000000000000002", + "2": "0x0000000000000000000000000000000000000000000000000000000000000003", + "3": "0x0000000000000000000000000000000000000000000000000000000000000004", + "4": "0x0000000000000000000000000000000000000000000000000000000000000005", + "5": "0x0000000000000000000000000000000000000000000000000000000000000006", + "6": "0x0000000000000000000000000000000000000000000000000000000000000007", + "7": "0x0000000000000000000000000000000000000000000000000000000000000008", + "8": "0x0000000000000000000000000000000000000000000000000000000000000009", + "9": "0x000000000000000000000000000000000000000000000000000000000000000a", + "10": "0x000000000000000000000000000000000000000000000000000000000000000b", + "11": "0x000000000000000000000000000000000000000000000000000000000000000c", + "12": "0x000000000000000000000000000000000000000000000000000000000000000d", + "13": "0x000000000000000000000000000000000000000000000000000000000000000e", + "14": "0x000000000000000000000000000000000000000000000000000000000000000f", + "15": "0x0000000000000000000000000000000000000000000000000000000000000010", + "16": "0x0000000000000000000000000000000000000000000000000000000000000011", + "17": "0x0000000000000000000000000000000000000000000000000000000000000012", + "18": "0x0000000000000000000000000000000000000000000000000000000000000013", + "19": "0x0000000000000000000000000000000000000000000000000000000000000014", + "20": "0x0000000000000000000000000000000000000000000000000000000000000015", + "21": "0x0000000000000000000000000000000000000000000000000000000000000016", + "22": "0x0000000000000000000000000000000000000000000000000000000000000017", + "23": "0x0000000000000000000000000000000000000000000000000000000000000018", + "24": "0x0000000000000000000000000000000000000000000000000000000000000019", + "25": "0x000000000000000000000000000000000000000000000000000000000000001a", + "26": "0x000000000000000000000000000000000000000000000000000000000000001b", + "27": "0x000000000000000000000000000000000000000000000000000000000000001c", + "28": "0x000000000000000000000000000000000000000000000000000000000000001d", + "29": "0x000000000000000000000000000000000000000000000000000000000000001e", + "30": "0x000000000000000000000000000000000000000000000000000000000000001f", + "31": "0x0000000000000000000000000000000000000000000000000000000000000020", + "32": "0x0000000000000000000000000000000000000000000000000000000000000021", + "33": "0x0000000000000000000000000000000000000000000000000000000000000022", + "34": "0x0000000000000000000000000000000000000000000000000000000000000023", + "35": "0x0000000000000000000000000000000000000000000000000000000000000024", + "36": "0x0000000000000000000000000000000000000000000000000000000000000025", + "37": "0x0000000000000000000000000000000000000000000000000000000000000026", + "38": "0x0000000000000000000000000000000000000000000000000000000000000027", + "39": "0x0000000000000000000000000000000000000000000000000000000000000028", + "40": "0x0000000000000000000000000000000000000000000000000000000000000029", + "41": "0x000000000000000000000000000000000000000000000000000000000000002a", + "42": "0x000000000000000000000000000000000000000000000000000000000000002b", + "43": "0x000000000000000000000000000000000000000000000000000000000000002c", + "44": "0x000000000000000000000000000000000000000000000000000000000000002d", + "45": "0x000000000000000000000000000000000000000000000000000000000000002e", + "46": "0x000000000000000000000000000000000000000000000000000000000000002f", + "47": "0x0000000000000000000000000000000000000000000000000000000000000030", + "48": "0x0000000000000000000000000000000000000000000000000000000000000031", + "49": "0x0000000000000000000000000000000000000000000000000000000000000032", + "50": "0x0000000000000000000000000000000000000000000000000000000000000033", + "51": "0x0000000000000000000000000000000000000000000000000000000000000034", + "52": "0x0000000000000000000000000000000000000000000000000000000000000035", + "53": "0x0000000000000000000000000000000000000000000000000000000000000036", + "54": "0x0000000000000000000000000000000000000000000000000000000000000037", + "55": "0x0000000000000000000000000000000000000000000000000000000000000038", + "56": "0x0000000000000000000000000000000000000000000000000000000000000039", + "57": "0x000000000000000000000000000000000000000000000000000000000000003a", + "58": "0x000000000000000000000000000000000000000000000000000000000000003b", + "59": "0x000000000000000000000000000000000000000000000000000000000000003c", + "60": "0x000000000000000000000000000000000000000000000000000000000000003d", + "61": "0x000000000000000000000000000000000000000000000000000000000000003e", + "62": "0x000000000000000000000000000000000000000000000000000000000000003f", + "63": "0x0000000000000000000000000000000000000000000000000000000000000040" + } + } + }, + "root": "0xcc42106e8d0eeb48e1c5fe68dd8893908de01d318641a4270e64d178e401b78c" + }, + { + "name": "shared_bytecode_two_accounts", + "accounts": { + "0xaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa": { + "nonce": 1, + "balance": "0x1", + "code": "0x010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101", + "code_hash": "0x70c8f40cf22ebddfdd1c66a0e1891a29f4ae670f4ca055f379a106209e074165", + "storage": {} + }, + "0xbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbb": { + "nonce": 2, + "balance": "0x2", + "code": "0x010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101", + "code_hash": "0x70c8f40cf22ebddfdd1c66a0e1891a29f4ae670f4ca055f379a106209e074165", + "storage": {} + } + }, + "root": "0xf1b98ddd9b35b8444abf1c6f36d6c4ac3203c2927efa415e6803fa3a8151e719" + }, + { + "name": "short_shared_code_two_accounts", + "accounts": { + "0xaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa": { + "nonce": 1, + "balance": "0x1", + "code": "0xfefefefefefefefefefefefefefefefefefefefefefefefefefefefefefefefefefefefefefefefe", + "code_hash": "0xde4f73676a0de2b9bf587ccb2007d5c1c7f9dd6efa65b507eff39f44bd00fe89", + "storage": {} + }, + "0xbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbb": { + "nonce": 2, + "balance": "0x2", + "code": "0xfefefefefefefefefefefefefefefefefefefefefefefefefefefefefefefefefefefefefefefefe", + "code_hash": "0xde4f73676a0de2b9bf587ccb2007d5c1c7f9dd6efa65b507eff39f44bd00fe89", + "storage": {} + } + }, + "root": "0x12cc9c7f1890c044657ebf1f2e6ddaaf29ab8e6e972de60dc9d4f131fae2e4d2" + }, + { + "name": "delegation_designator", + "accounts": { + "0xaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa": { + "nonce": 1, + "balance": "0x0", + "code": "0xef0100cccccccccccccccccccccccccccccccccccccccc", + "code_hash": "0xf50ecf6d1bbb82ea6d6efb33e0cff6b48a6946a9dce2445b330537fa26d12e6d", + "storage": {} + } + }, + "root": "0x54f3446a8fb2084f179500f728221fed07d3d2f4ca46117fa6465ab91276908c" + }, + { + "name": "two_authorities_one_target", + "accounts": { + "0xaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa": { + "nonce": 1, + "balance": "0x1", + "code": "0xef0100cccccccccccccccccccccccccccccccccccccccc", + "code_hash": "0xf50ecf6d1bbb82ea6d6efb33e0cff6b48a6946a9dce2445b330537fa26d12e6d", + "storage": {} + }, + "0xbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbb": { + "nonce": 2, + "balance": "0x2", + "code": "0xef0100cccccccccccccccccccccccccccccccccccccccc", + "code_hash": "0xf50ecf6d1bbb82ea6d6efb33e0cff6b48a6946a9dce2445b330537fa26d12e6d", + "storage": {} + } + }, + "root": "0xf6a805ce1770a4549d4bbd4c55d55fd9f0f49e98161912c34ef4bdd3a44ee9cb" + }, + { + "name": "delegation_with_storage", + "accounts": { + "0xaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa": { + "nonce": 1, + "balance": "0x0", + "code": "0xef0100cccccccccccccccccccccccccccccccccccccccc", + "code_hash": "0xf50ecf6d1bbb82ea6d6efb33e0cff6b48a6946a9dce2445b330537fa26d12e6d", + "storage": { + "0": "0x0000000000000000000000000000000000000000000000000000000000000001", + "63": "0x0000000000000000000000000000000000000000000000000000000000000002", + "64": "0x0000000000000000000000000000000000000000000000000000000000000003" + } + } + }, + "root": "0x0eb6d553cb8eb7d227c98855e12bedfa83ff896c16fcfc125bc597d92a9aadf6" + }, + { + "name": "code_hash_starting_with_the_delegation_marker", + "accounts": { + "0xaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa": { + "nonce": 1, + "balance": "0x0", + "code": "0x0000000000000000000000000000000000000000637401", + "code_hash": "0xef0100f360bf074f90f948bcf767f30c5c3717d735b6af03fdf7efff4fcc2ecf", + "storage": {} + } + }, + "root": "0x3f1cf36a116be3dcdec395fc44fdb92ea71511471ec0bf7c447c7b40ef4a4396" + }, + { + "name": "code_chunks_of_zero_bytes", + "accounts": { + "0xaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa": { + "nonce": 1, + "balance": "0x0", + "code": "0x0000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000", + "code_hash": "0x2e795758918d9c804da815b3be88b798e63d21d668c624228fbd697bff25ea3b", + "storage": {} + } + }, + "root": "0x4001782ffd46a182d4a4beaec5ff32141776ed98f415651733252098a7dac362" + }, + { + "name": "max_basic_data_fields", + "accounts": { + "0xaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa": { + "nonce": 9007199254740991, + "balance": "0xffffffffffffffffffffffffffffffff", + "code": "0x", + "code_hash": "0xc5d2460186f7233c927e7db2dcc703c0e500b653ca82273b7bfad8045d85a470", + "storage": {} + } + }, + "root": "0x8331ded43e04d0fadfb2e42ef91add88b92e6b4ff1f5f5054f0e8a17e3638a52" + }, + { + "name": "random_6_accounts_seed_8297", + "accounts": { + "0x2aa6a8996ce6a78ab232d4ea1c1773f4216f5c6c": { + "nonce": 3864742092, + "balance": "0xd80b4d69597c60e1efb8a4ff0b5daa13", + "code": "0x", + "code_hash": "0xc5d2460186f7233c927e7db2dcc703c0e500b653ca82273b7bfad8045d85a470", + "storage": { + "23810349356214295913386548550719634316398909742729249082281881830725930739838": "0xebe10c5568a6acd14237bcf6d560d2cf7bf251d5400f68d4a0ac7615939ea857", + "96523555122766932062082486665246314608966373848871865152670980275761706763325": "0x1f54549ffb51a4a315716295306a7c53f0755d81fee7a7182c8b68ac9864d4a0", + "98591652884780845594215434683999800137086927586913902574991246821904145534181": "0xc481a4a2ad7cb9b16b591edaeabae84dc6f30fd205676b14a6dabb16b2231c1b" + } + }, + "0x036ad7e47298ab30c44a5069709f82e5cfa1fdb5": { + "nonce": 1556638775, + "balance": "0xe51b7a8af4367d872665b2247c239cd9", + "code": "0x23a09cbf72345d149135921a5ff4c22b590d5a7c6b323fedde8f23715681af22f74b0d34825fb0cd1bb1a530e7e22b99c856937c38781d2c29c5a940de63446ffd493abe5469486948d20bcaf06d586dcd3e28507dbe4a4e557de7c40c7061a0e096cb67", + "code_hash": "0x59ccc5659d64aafe203bfcb44b1a034853791063da1d6e2006286d4635e57749", + "storage": { + "22481324409851482507451361461257995159359285672655871475285301068545285417875": "0x8b4d0643d5e14a6fe484b99cbbd3281606937fd09393f867f17288dba672eb34", + "108540624023827917680660341181296584989878088019874691452633636111559395721206": "0xb6000e0f19c549363c9012401673c61df3acd2dac6c688efe9a8e051c5955969" + } + }, + "0x8ea43e59ebf9744a30fccbea4ff32d0d2bd8c415": { + "nonce": 735618664, + "balance": "0x2468d9b033c3fd72d099fa1ec7973061", + "code": "0x2c", + "code_hash": "0x3e7a35b97029f9e0cf6effd71c1a7958822e9a217d3a3aec886668a7dd8231cb", + "storage": { + "4171655909082161865571109005100230898954774310874913228405678276964421062338": "0xa4e1a791a94779f02d3fd4add43345f82a7a594f0041ee04a4632870646bea97", + "25836125176906852663030524785766215270304214778391188685675587848691358961258": "0xc357d7e167b39624e9c1edd366617e585379106388d2c44c4731dddbc5abb83f" + } + }, + "0x4445c4f13e4c69d6b258bab15c7dde77da8d296c": { + "nonce": 1185213493, + "balance": "0x7f63d0eddc50e2b1ffbcd4b981e866df", + "code": "0x4de46cffb751823511f71843a4e7dcc6fbe826921ed5bf7ff0db9a856f786c", + "code_hash": "0xdc1f5e259ca6e190357dd69f6640852a2d92ae6f45336d0863faed4b1a05f2e2", + "storage": { + "19942743671895068441030546052764156067014987410011317649363496461376689888165": "0x0b1d452e5838ae487e65db179f3ba1d8b82dbff33167b442d37f8e1d3b6632fd", + "58654395628803392503698366458431685516538035156756763504726343391399085975384": "0x99efd577b88168ffa98494f2b366690e4efde683aeb9330eb88ccfe33afc2613", + "1626051957501994272606654322465618868497675113599288733974508228147317687749": "0xa91d606cfbfa6980211ed20601433a4a2c7b428fd8c4284070701aedd7b03285" + } + }, + "0xd6b77fecc05f6a68a2a05d955d25c1d2fa2d2c7d": { + "nonce": 2452887031, + "balance": "0xf28ff04128411a867d9165b662ee783a", + "code": "0x3948c70e4ba2117150041d1628852a95680f5f67f0e0049573d9a08353549a", + "code_hash": "0xd64095e60edee2e39710bbd761df130eefaf2e3182a4a6b637a6281077b4bbc3", + "storage": {} + }, + "0x7b2cadb92edd48f96b70a952afae073e434b41f0": { + "nonce": 3485188628, + "balance": "0x5a1d9ca3fc2c8309535c7b76fbb29863", + "code": "0xa2", + "code_hash": "0x5817a817284a25996cf471299ba31908b9ff7bb9b4ec073d781021f971c8af66", + "storage": { + "106584524199836718886300765010566265940520737683892308674385881765558924295996": "0x41ca6dc003d765a9b51f1432fc4ec85616b6933eb6916ca0b2e73decd1fa5dfd", + "97637941765767869768932293193125868376845616129893957383796993607466809060766": "0x9bd755219717db6ddadc9518a376a0834b56ff750d0bdd4fe3cfab432e523ade" + } + } + }, + "root": "0xa5e4c911120339fa0de57ae06c90abbec39d87a7d753c9c10e7701473e60463f" + } + ] +} diff --git a/execution/commitment/testdata/eip8297_vectors.json b/execution/commitment/testdata/eip8297_vectors.json index aed6098cff0..f510ac859c4 100644 --- a/execution/commitment/testdata/eip8297_vectors.json +++ b/execution/commitment/testdata/eip8297_vectors.json @@ -1 +1,1858 @@ -{"meta":{"source":"execution-specs@ec412acfd (branch eip-8297-tests)","hasher":"blake3","generator":"export_vectors.py"},"empty_root":"0x0000000000000000000000000000000000000000000000000000000000000000","trie_vectors":[{"name":"empty","entries":[],"root":"0x0000000000000000000000000000000000000000000000000000000000000000"},{"name":"single_account_leaf","entries":[{"key":"0x0055bbfb5b05f80543bd662a7b7a74980a2992612798087fa24e1fc86b8e8508e400","value":"0x0000000000000000000000000000000000000000000000000000000000000007"}],"root":"0x22da077ee89a0e6e6259303169fb6c7a3133fafa3033515a355af9c4f9ed5ea0"},{"name":"one_header_stem_two_leaves","entries":[{"key":"0x0055bbfb5b05f80543bd662a7b7a74980a2992612798087fa24e1fc86b8e8508e400","value":"0x0000000000000000000000000000000000000000000000000000000000000007"},{"key":"0x0055bbfb5b05f80543bd662a7b7a74980a2992612798087fa24e1fc86b8e8508e401","value":"0x0000000000000000000000000000000000000000000000000000000000000009"}],"root":"0x3599d1dd23fef634f3845fd935375be8a42169ecc90906632aa8afa2fa380812"},{"name":"two_accounts","entries":[{"key":"0x0055bbfb5b05f80543bd662a7b7a74980a2992612798087fa24e1fc86b8e8508e400","value":"0x0000000000000000000000000000000000000000000000000000000000000007"},{"key":"0x0034bb4f3468340d5f72275da92420aed94db48bbc61823e92f29f14f51590373a00","value":"0x0000000000000000000000000000000000000000000000000000000000000008"}],"root":"0xbcca03773c89779e136f983eba516339660c732d5b0a1ccfc2c725dca29eefbc"},{"name":"cross_zone_small","entries":[{"key":"0x0055bbfb5b05f80543bd662a7b7a74980a2992612798087fa24e1fc86b8e8508e400","value":"0x0000000000000000000000000000000000000000000000000000000000000001"},{"key":"0x0055bbfb5b05f80543bd662a7b7a74980a2992612798087fa24e1fc86b8e8508e401","value":"0x0000000000000000000000000000000000000000000000000000000000000002"},{"key":"0xff55bbfb5b05f80543bd662a7b7a74980a2992612798087fa24e1fc86b8e8508e406f5e5117ba26652e3cbef5ea24cc46f42709eb47be3c46f3a923b68a67f44c264","value":"0x0000000000000000000000000000000000000000000000000000000000000003"},{"key":"0xff55bbfb5b05f80543bd662a7b7a74980a2992612798087fa24e1fc86b8e8508e4f1ce240e3efd0b0855a335ac6b58ea33be1b2afb193608d6d21fd91308dd74bc64","value":"0x0000000000000000000000000000000000000000000000000000000000000004"},{"key":"0x0034bb4f3468340d5f72275da92420aed94db48bbc61823e92f29f14f51590373a00","value":"0x0000000000000000000000000000000000000000000000000000000000000005"}],"root":"0x3fc0f35560e81b2b1bc349decef42eb48a614d8497646e401e7b85abc0b16a30"},{"name":"zero_value_present","entries":[{"key":"0x00a43fb2d7beb12b34786a837467f62b7d6c5dab4ab7eecc89c0b67db76a68292700","value":"0x0000000000000000000000000000000000000000000000000000000000000000"}],"root":"0x343a84978f71225f27f6dbdd2e0dd603a2ae3b83028a907ae0f8f4db262c9d13"},{"name":"full_header_stem","entries":[{"key":"0x004c3a8541d9ec369a9fd4bcdad885cc3b166da70ed6a83066924727d9ff147fce00","value":"0x0000000000000000000000000000000000000000000000000000000000000001"},{"key":"0x004c3a8541d9ec369a9fd4bcdad885cc3b166da70ed6a83066924727d9ff147fce01","value":"0x0000000000000000000000000000000000000000000000000000000000000002"},{"key":"0x004c3a8541d9ec369a9fd4bcdad885cc3b166da70ed6a83066924727d9ff147fce02","value":"0x0000000000000000000000000000000000000000000000000000000000000003"},{"key":"0x004c3a8541d9ec369a9fd4bcdad885cc3b166da70ed6a83066924727d9ff147fce03","value":"0x0000000000000000000000000000000000000000000000000000000000000004"},{"key":"0x004c3a8541d9ec369a9fd4bcdad885cc3b166da70ed6a83066924727d9ff147fce04","value":"0x0000000000000000000000000000000000000000000000000000000000000005"},{"key":"0x004c3a8541d9ec369a9fd4bcdad885cc3b166da70ed6a83066924727d9ff147fce05","value":"0x0000000000000000000000000000000000000000000000000000000000000006"},{"key":"0x004c3a8541d9ec369a9fd4bcdad885cc3b166da70ed6a83066924727d9ff147fce06","value":"0x0000000000000000000000000000000000000000000000000000000000000007"},{"key":"0x004c3a8541d9ec369a9fd4bcdad885cc3b166da70ed6a83066924727d9ff147fce07","value":"0x0000000000000000000000000000000000000000000000000000000000000008"},{"key":"0x004c3a8541d9ec369a9fd4bcdad885cc3b166da70ed6a83066924727d9ff147fce08","value":"0x0000000000000000000000000000000000000000000000000000000000000009"},{"key":"0x004c3a8541d9ec369a9fd4bcdad885cc3b166da70ed6a83066924727d9ff147fce09","value":"0x000000000000000000000000000000000000000000000000000000000000000a"},{"key":"0x004c3a8541d9ec369a9fd4bcdad885cc3b166da70ed6a83066924727d9ff147fce0a","value":"0x000000000000000000000000000000000000000000000000000000000000000b"},{"key":"0x004c3a8541d9ec369a9fd4bcdad885cc3b166da70ed6a83066924727d9ff147fce0b","value":"0x000000000000000000000000000000000000000000000000000000000000000c"},{"key":"0x004c3a8541d9ec369a9fd4bcdad885cc3b166da70ed6a83066924727d9ff147fce0c","value":"0x000000000000000000000000000000000000000000000000000000000000000d"},{"key":"0x004c3a8541d9ec369a9fd4bcdad885cc3b166da70ed6a83066924727d9ff147fce0d","value":"0x000000000000000000000000000000000000000000000000000000000000000e"},{"key":"0x004c3a8541d9ec369a9fd4bcdad885cc3b166da70ed6a83066924727d9ff147fce0e","value":"0x000000000000000000000000000000000000000000000000000000000000000f"},{"key":"0x004c3a8541d9ec369a9fd4bcdad885cc3b166da70ed6a83066924727d9ff147fce0f","value":"0x0000000000000000000000000000000000000000000000000000000000000010"},{"key":"0x004c3a8541d9ec369a9fd4bcdad885cc3b166da70ed6a83066924727d9ff147fce10","value":"0x0000000000000000000000000000000000000000000000000000000000000011"},{"key":"0x004c3a8541d9ec369a9fd4bcdad885cc3b166da70ed6a83066924727d9ff147fce11","value":"0x0000000000000000000000000000000000000000000000000000000000000012"},{"key":"0x004c3a8541d9ec369a9fd4bcdad885cc3b166da70ed6a83066924727d9ff147fce12","value":"0x0000000000000000000000000000000000000000000000000000000000000013"},{"key":"0x004c3a8541d9ec369a9fd4bcdad885cc3b166da70ed6a83066924727d9ff147fce13","value":"0x0000000000000000000000000000000000000000000000000000000000000014"},{"key":"0x004c3a8541d9ec369a9fd4bcdad885cc3b166da70ed6a83066924727d9ff147fce14","value":"0x0000000000000000000000000000000000000000000000000000000000000015"},{"key":"0x004c3a8541d9ec369a9fd4bcdad885cc3b166da70ed6a83066924727d9ff147fce15","value":"0x0000000000000000000000000000000000000000000000000000000000000016"},{"key":"0x004c3a8541d9ec369a9fd4bcdad885cc3b166da70ed6a83066924727d9ff147fce16","value":"0x0000000000000000000000000000000000000000000000000000000000000017"},{"key":"0x004c3a8541d9ec369a9fd4bcdad885cc3b166da70ed6a83066924727d9ff147fce17","value":"0x0000000000000000000000000000000000000000000000000000000000000018"},{"key":"0x004c3a8541d9ec369a9fd4bcdad885cc3b166da70ed6a83066924727d9ff147fce18","value":"0x0000000000000000000000000000000000000000000000000000000000000019"},{"key":"0x004c3a8541d9ec369a9fd4bcdad885cc3b166da70ed6a83066924727d9ff147fce19","value":"0x000000000000000000000000000000000000000000000000000000000000001a"},{"key":"0x004c3a8541d9ec369a9fd4bcdad885cc3b166da70ed6a83066924727d9ff147fce1a","value":"0x000000000000000000000000000000000000000000000000000000000000001b"},{"key":"0x004c3a8541d9ec369a9fd4bcdad885cc3b166da70ed6a83066924727d9ff147fce1b","value":"0x000000000000000000000000000000000000000000000000000000000000001c"},{"key":"0x004c3a8541d9ec369a9fd4bcdad885cc3b166da70ed6a83066924727d9ff147fce1c","value":"0x000000000000000000000000000000000000000000000000000000000000001d"},{"key":"0x004c3a8541d9ec369a9fd4bcdad885cc3b166da70ed6a83066924727d9ff147fce1d","value":"0x000000000000000000000000000000000000000000000000000000000000001e"},{"key":"0x004c3a8541d9ec369a9fd4bcdad885cc3b166da70ed6a83066924727d9ff147fce1e","value":"0x000000000000000000000000000000000000000000000000000000000000001f"},{"key":"0x004c3a8541d9ec369a9fd4bcdad885cc3b166da70ed6a83066924727d9ff147fce1f","value":"0x0000000000000000000000000000000000000000000000000000000000000020"},{"key":"0x004c3a8541d9ec369a9fd4bcdad885cc3b166da70ed6a83066924727d9ff147fce20","value":"0x0000000000000000000000000000000000000000000000000000000000000021"},{"key":"0x004c3a8541d9ec369a9fd4bcdad885cc3b166da70ed6a83066924727d9ff147fce21","value":"0x0000000000000000000000000000000000000000000000000000000000000022"},{"key":"0x004c3a8541d9ec369a9fd4bcdad885cc3b166da70ed6a83066924727d9ff147fce22","value":"0x0000000000000000000000000000000000000000000000000000000000000023"},{"key":"0x004c3a8541d9ec369a9fd4bcdad885cc3b166da70ed6a83066924727d9ff147fce23","value":"0x0000000000000000000000000000000000000000000000000000000000000024"},{"key":"0x004c3a8541d9ec369a9fd4bcdad885cc3b166da70ed6a83066924727d9ff147fce24","value":"0x0000000000000000000000000000000000000000000000000000000000000025"},{"key":"0x004c3a8541d9ec369a9fd4bcdad885cc3b166da70ed6a83066924727d9ff147fce25","value":"0x0000000000000000000000000000000000000000000000000000000000000026"},{"key":"0x004c3a8541d9ec369a9fd4bcdad885cc3b166da70ed6a83066924727d9ff147fce26","value":"0x0000000000000000000000000000000000000000000000000000000000000027"},{"key":"0x004c3a8541d9ec369a9fd4bcdad885cc3b166da70ed6a83066924727d9ff147fce27","value":"0x0000000000000000000000000000000000000000000000000000000000000028"},{"key":"0x004c3a8541d9ec369a9fd4bcdad885cc3b166da70ed6a83066924727d9ff147fce28","value":"0x0000000000000000000000000000000000000000000000000000000000000029"},{"key":"0x004c3a8541d9ec369a9fd4bcdad885cc3b166da70ed6a83066924727d9ff147fce29","value":"0x000000000000000000000000000000000000000000000000000000000000002a"},{"key":"0x004c3a8541d9ec369a9fd4bcdad885cc3b166da70ed6a83066924727d9ff147fce2a","value":"0x000000000000000000000000000000000000000000000000000000000000002b"},{"key":"0x004c3a8541d9ec369a9fd4bcdad885cc3b166da70ed6a83066924727d9ff147fce2b","value":"0x000000000000000000000000000000000000000000000000000000000000002c"},{"key":"0x004c3a8541d9ec369a9fd4bcdad885cc3b166da70ed6a83066924727d9ff147fce2c","value":"0x000000000000000000000000000000000000000000000000000000000000002d"},{"key":"0x004c3a8541d9ec369a9fd4bcdad885cc3b166da70ed6a83066924727d9ff147fce2d","value":"0x000000000000000000000000000000000000000000000000000000000000002e"},{"key":"0x004c3a8541d9ec369a9fd4bcdad885cc3b166da70ed6a83066924727d9ff147fce2e","value":"0x000000000000000000000000000000000000000000000000000000000000002f"},{"key":"0x004c3a8541d9ec369a9fd4bcdad885cc3b166da70ed6a83066924727d9ff147fce2f","value":"0x0000000000000000000000000000000000000000000000000000000000000030"},{"key":"0x004c3a8541d9ec369a9fd4bcdad885cc3b166da70ed6a83066924727d9ff147fce30","value":"0x0000000000000000000000000000000000000000000000000000000000000031"},{"key":"0x004c3a8541d9ec369a9fd4bcdad885cc3b166da70ed6a83066924727d9ff147fce31","value":"0x0000000000000000000000000000000000000000000000000000000000000032"},{"key":"0x004c3a8541d9ec369a9fd4bcdad885cc3b166da70ed6a83066924727d9ff147fce32","value":"0x0000000000000000000000000000000000000000000000000000000000000033"},{"key":"0x004c3a8541d9ec369a9fd4bcdad885cc3b166da70ed6a83066924727d9ff147fce33","value":"0x0000000000000000000000000000000000000000000000000000000000000034"},{"key":"0x004c3a8541d9ec369a9fd4bcdad885cc3b166da70ed6a83066924727d9ff147fce34","value":"0x0000000000000000000000000000000000000000000000000000000000000035"},{"key":"0x004c3a8541d9ec369a9fd4bcdad885cc3b166da70ed6a83066924727d9ff147fce35","value":"0x0000000000000000000000000000000000000000000000000000000000000036"},{"key":"0x004c3a8541d9ec369a9fd4bcdad885cc3b166da70ed6a83066924727d9ff147fce36","value":"0x0000000000000000000000000000000000000000000000000000000000000037"},{"key":"0x004c3a8541d9ec369a9fd4bcdad885cc3b166da70ed6a83066924727d9ff147fce37","value":"0x0000000000000000000000000000000000000000000000000000000000000038"},{"key":"0x004c3a8541d9ec369a9fd4bcdad885cc3b166da70ed6a83066924727d9ff147fce38","value":"0x0000000000000000000000000000000000000000000000000000000000000039"},{"key":"0x004c3a8541d9ec369a9fd4bcdad885cc3b166da70ed6a83066924727d9ff147fce39","value":"0x000000000000000000000000000000000000000000000000000000000000003a"},{"key":"0x004c3a8541d9ec369a9fd4bcdad885cc3b166da70ed6a83066924727d9ff147fce3a","value":"0x000000000000000000000000000000000000000000000000000000000000003b"},{"key":"0x004c3a8541d9ec369a9fd4bcdad885cc3b166da70ed6a83066924727d9ff147fce3b","value":"0x000000000000000000000000000000000000000000000000000000000000003c"},{"key":"0x004c3a8541d9ec369a9fd4bcdad885cc3b166da70ed6a83066924727d9ff147fce3c","value":"0x000000000000000000000000000000000000000000000000000000000000003d"},{"key":"0x004c3a8541d9ec369a9fd4bcdad885cc3b166da70ed6a83066924727d9ff147fce3d","value":"0x000000000000000000000000000000000000000000000000000000000000003e"},{"key":"0x004c3a8541d9ec369a9fd4bcdad885cc3b166da70ed6a83066924727d9ff147fce3e","value":"0x000000000000000000000000000000000000000000000000000000000000003f"},{"key":"0x004c3a8541d9ec369a9fd4bcdad885cc3b166da70ed6a83066924727d9ff147fce3f","value":"0x0000000000000000000000000000000000000000000000000000000000000040"},{"key":"0x004c3a8541d9ec369a9fd4bcdad885cc3b166da70ed6a83066924727d9ff147fce40","value":"0x0000000000000000000000000000000000000000000000000000000000000041"},{"key":"0x004c3a8541d9ec369a9fd4bcdad885cc3b166da70ed6a83066924727d9ff147fce41","value":"0x0000000000000000000000000000000000000000000000000000000000000042"},{"key":"0x004c3a8541d9ec369a9fd4bcdad885cc3b166da70ed6a83066924727d9ff147fce42","value":"0x0000000000000000000000000000000000000000000000000000000000000043"},{"key":"0x004c3a8541d9ec369a9fd4bcdad885cc3b166da70ed6a83066924727d9ff147fce43","value":"0x0000000000000000000000000000000000000000000000000000000000000044"},{"key":"0x004c3a8541d9ec369a9fd4bcdad885cc3b166da70ed6a83066924727d9ff147fce44","value":"0x0000000000000000000000000000000000000000000000000000000000000045"},{"key":"0x004c3a8541d9ec369a9fd4bcdad885cc3b166da70ed6a83066924727d9ff147fce45","value":"0x0000000000000000000000000000000000000000000000000000000000000046"},{"key":"0x004c3a8541d9ec369a9fd4bcdad885cc3b166da70ed6a83066924727d9ff147fce46","value":"0x0000000000000000000000000000000000000000000000000000000000000047"},{"key":"0x004c3a8541d9ec369a9fd4bcdad885cc3b166da70ed6a83066924727d9ff147fce47","value":"0x0000000000000000000000000000000000000000000000000000000000000048"},{"key":"0x004c3a8541d9ec369a9fd4bcdad885cc3b166da70ed6a83066924727d9ff147fce48","value":"0x0000000000000000000000000000000000000000000000000000000000000049"},{"key":"0x004c3a8541d9ec369a9fd4bcdad885cc3b166da70ed6a83066924727d9ff147fce49","value":"0x000000000000000000000000000000000000000000000000000000000000004a"},{"key":"0x004c3a8541d9ec369a9fd4bcdad885cc3b166da70ed6a83066924727d9ff147fce4a","value":"0x000000000000000000000000000000000000000000000000000000000000004b"},{"key":"0x004c3a8541d9ec369a9fd4bcdad885cc3b166da70ed6a83066924727d9ff147fce4b","value":"0x000000000000000000000000000000000000000000000000000000000000004c"},{"key":"0x004c3a8541d9ec369a9fd4bcdad885cc3b166da70ed6a83066924727d9ff147fce4c","value":"0x000000000000000000000000000000000000000000000000000000000000004d"},{"key":"0x004c3a8541d9ec369a9fd4bcdad885cc3b166da70ed6a83066924727d9ff147fce4d","value":"0x000000000000000000000000000000000000000000000000000000000000004e"},{"key":"0x004c3a8541d9ec369a9fd4bcdad885cc3b166da70ed6a83066924727d9ff147fce4e","value":"0x000000000000000000000000000000000000000000000000000000000000004f"},{"key":"0x004c3a8541d9ec369a9fd4bcdad885cc3b166da70ed6a83066924727d9ff147fce4f","value":"0x0000000000000000000000000000000000000000000000000000000000000050"},{"key":"0x004c3a8541d9ec369a9fd4bcdad885cc3b166da70ed6a83066924727d9ff147fce50","value":"0x0000000000000000000000000000000000000000000000000000000000000051"},{"key":"0x004c3a8541d9ec369a9fd4bcdad885cc3b166da70ed6a83066924727d9ff147fce51","value":"0x0000000000000000000000000000000000000000000000000000000000000052"},{"key":"0x004c3a8541d9ec369a9fd4bcdad885cc3b166da70ed6a83066924727d9ff147fce52","value":"0x0000000000000000000000000000000000000000000000000000000000000053"},{"key":"0x004c3a8541d9ec369a9fd4bcdad885cc3b166da70ed6a83066924727d9ff147fce53","value":"0x0000000000000000000000000000000000000000000000000000000000000054"},{"key":"0x004c3a8541d9ec369a9fd4bcdad885cc3b166da70ed6a83066924727d9ff147fce54","value":"0x0000000000000000000000000000000000000000000000000000000000000055"},{"key":"0x004c3a8541d9ec369a9fd4bcdad885cc3b166da70ed6a83066924727d9ff147fce55","value":"0x0000000000000000000000000000000000000000000000000000000000000056"},{"key":"0x004c3a8541d9ec369a9fd4bcdad885cc3b166da70ed6a83066924727d9ff147fce56","value":"0x0000000000000000000000000000000000000000000000000000000000000057"},{"key":"0x004c3a8541d9ec369a9fd4bcdad885cc3b166da70ed6a83066924727d9ff147fce57","value":"0x0000000000000000000000000000000000000000000000000000000000000058"},{"key":"0x004c3a8541d9ec369a9fd4bcdad885cc3b166da70ed6a83066924727d9ff147fce58","value":"0x0000000000000000000000000000000000000000000000000000000000000059"},{"key":"0x004c3a8541d9ec369a9fd4bcdad885cc3b166da70ed6a83066924727d9ff147fce59","value":"0x000000000000000000000000000000000000000000000000000000000000005a"},{"key":"0x004c3a8541d9ec369a9fd4bcdad885cc3b166da70ed6a83066924727d9ff147fce5a","value":"0x000000000000000000000000000000000000000000000000000000000000005b"},{"key":"0x004c3a8541d9ec369a9fd4bcdad885cc3b166da70ed6a83066924727d9ff147fce5b","value":"0x000000000000000000000000000000000000000000000000000000000000005c"},{"key":"0x004c3a8541d9ec369a9fd4bcdad885cc3b166da70ed6a83066924727d9ff147fce5c","value":"0x000000000000000000000000000000000000000000000000000000000000005d"},{"key":"0x004c3a8541d9ec369a9fd4bcdad885cc3b166da70ed6a83066924727d9ff147fce5d","value":"0x000000000000000000000000000000000000000000000000000000000000005e"},{"key":"0x004c3a8541d9ec369a9fd4bcdad885cc3b166da70ed6a83066924727d9ff147fce5e","value":"0x000000000000000000000000000000000000000000000000000000000000005f"},{"key":"0x004c3a8541d9ec369a9fd4bcdad885cc3b166da70ed6a83066924727d9ff147fce5f","value":"0x0000000000000000000000000000000000000000000000000000000000000060"},{"key":"0x004c3a8541d9ec369a9fd4bcdad885cc3b166da70ed6a83066924727d9ff147fce60","value":"0x0000000000000000000000000000000000000000000000000000000000000061"},{"key":"0x004c3a8541d9ec369a9fd4bcdad885cc3b166da70ed6a83066924727d9ff147fce61","value":"0x0000000000000000000000000000000000000000000000000000000000000062"},{"key":"0x004c3a8541d9ec369a9fd4bcdad885cc3b166da70ed6a83066924727d9ff147fce62","value":"0x0000000000000000000000000000000000000000000000000000000000000063"},{"key":"0x004c3a8541d9ec369a9fd4bcdad885cc3b166da70ed6a83066924727d9ff147fce63","value":"0x0000000000000000000000000000000000000000000000000000000000000064"},{"key":"0x004c3a8541d9ec369a9fd4bcdad885cc3b166da70ed6a83066924727d9ff147fce64","value":"0x0000000000000000000000000000000000000000000000000000000000000065"},{"key":"0x004c3a8541d9ec369a9fd4bcdad885cc3b166da70ed6a83066924727d9ff147fce65","value":"0x0000000000000000000000000000000000000000000000000000000000000066"},{"key":"0x004c3a8541d9ec369a9fd4bcdad885cc3b166da70ed6a83066924727d9ff147fce66","value":"0x0000000000000000000000000000000000000000000000000000000000000067"},{"key":"0x004c3a8541d9ec369a9fd4bcdad885cc3b166da70ed6a83066924727d9ff147fce67","value":"0x0000000000000000000000000000000000000000000000000000000000000068"},{"key":"0x004c3a8541d9ec369a9fd4bcdad885cc3b166da70ed6a83066924727d9ff147fce68","value":"0x0000000000000000000000000000000000000000000000000000000000000069"},{"key":"0x004c3a8541d9ec369a9fd4bcdad885cc3b166da70ed6a83066924727d9ff147fce69","value":"0x000000000000000000000000000000000000000000000000000000000000006a"},{"key":"0x004c3a8541d9ec369a9fd4bcdad885cc3b166da70ed6a83066924727d9ff147fce6a","value":"0x000000000000000000000000000000000000000000000000000000000000006b"},{"key":"0x004c3a8541d9ec369a9fd4bcdad885cc3b166da70ed6a83066924727d9ff147fce6b","value":"0x000000000000000000000000000000000000000000000000000000000000006c"},{"key":"0x004c3a8541d9ec369a9fd4bcdad885cc3b166da70ed6a83066924727d9ff147fce6c","value":"0x000000000000000000000000000000000000000000000000000000000000006d"},{"key":"0x004c3a8541d9ec369a9fd4bcdad885cc3b166da70ed6a83066924727d9ff147fce6d","value":"0x000000000000000000000000000000000000000000000000000000000000006e"},{"key":"0x004c3a8541d9ec369a9fd4bcdad885cc3b166da70ed6a83066924727d9ff147fce6e","value":"0x000000000000000000000000000000000000000000000000000000000000006f"},{"key":"0x004c3a8541d9ec369a9fd4bcdad885cc3b166da70ed6a83066924727d9ff147fce6f","value":"0x0000000000000000000000000000000000000000000000000000000000000070"},{"key":"0x004c3a8541d9ec369a9fd4bcdad885cc3b166da70ed6a83066924727d9ff147fce70","value":"0x0000000000000000000000000000000000000000000000000000000000000071"},{"key":"0x004c3a8541d9ec369a9fd4bcdad885cc3b166da70ed6a83066924727d9ff147fce71","value":"0x0000000000000000000000000000000000000000000000000000000000000072"},{"key":"0x004c3a8541d9ec369a9fd4bcdad885cc3b166da70ed6a83066924727d9ff147fce72","value":"0x0000000000000000000000000000000000000000000000000000000000000073"},{"key":"0x004c3a8541d9ec369a9fd4bcdad885cc3b166da70ed6a83066924727d9ff147fce73","value":"0x0000000000000000000000000000000000000000000000000000000000000074"},{"key":"0x004c3a8541d9ec369a9fd4bcdad885cc3b166da70ed6a83066924727d9ff147fce74","value":"0x0000000000000000000000000000000000000000000000000000000000000075"},{"key":"0x004c3a8541d9ec369a9fd4bcdad885cc3b166da70ed6a83066924727d9ff147fce75","value":"0x0000000000000000000000000000000000000000000000000000000000000076"},{"key":"0x004c3a8541d9ec369a9fd4bcdad885cc3b166da70ed6a83066924727d9ff147fce76","value":"0x0000000000000000000000000000000000000000000000000000000000000077"},{"key":"0x004c3a8541d9ec369a9fd4bcdad885cc3b166da70ed6a83066924727d9ff147fce77","value":"0x0000000000000000000000000000000000000000000000000000000000000078"},{"key":"0x004c3a8541d9ec369a9fd4bcdad885cc3b166da70ed6a83066924727d9ff147fce78","value":"0x0000000000000000000000000000000000000000000000000000000000000079"},{"key":"0x004c3a8541d9ec369a9fd4bcdad885cc3b166da70ed6a83066924727d9ff147fce79","value":"0x000000000000000000000000000000000000000000000000000000000000007a"},{"key":"0x004c3a8541d9ec369a9fd4bcdad885cc3b166da70ed6a83066924727d9ff147fce7a","value":"0x000000000000000000000000000000000000000000000000000000000000007b"},{"key":"0x004c3a8541d9ec369a9fd4bcdad885cc3b166da70ed6a83066924727d9ff147fce7b","value":"0x000000000000000000000000000000000000000000000000000000000000007c"},{"key":"0x004c3a8541d9ec369a9fd4bcdad885cc3b166da70ed6a83066924727d9ff147fce7c","value":"0x000000000000000000000000000000000000000000000000000000000000007d"},{"key":"0x004c3a8541d9ec369a9fd4bcdad885cc3b166da70ed6a83066924727d9ff147fce7d","value":"0x000000000000000000000000000000000000000000000000000000000000007e"},{"key":"0x004c3a8541d9ec369a9fd4bcdad885cc3b166da70ed6a83066924727d9ff147fce7e","value":"0x000000000000000000000000000000000000000000000000000000000000007f"},{"key":"0x004c3a8541d9ec369a9fd4bcdad885cc3b166da70ed6a83066924727d9ff147fce7f","value":"0x0000000000000000000000000000000000000000000000000000000000000080"},{"key":"0x004c3a8541d9ec369a9fd4bcdad885cc3b166da70ed6a83066924727d9ff147fce80","value":"0x0000000000000000000000000000000000000000000000000000000000000081"},{"key":"0x004c3a8541d9ec369a9fd4bcdad885cc3b166da70ed6a83066924727d9ff147fce81","value":"0x0000000000000000000000000000000000000000000000000000000000000082"},{"key":"0x004c3a8541d9ec369a9fd4bcdad885cc3b166da70ed6a83066924727d9ff147fce82","value":"0x0000000000000000000000000000000000000000000000000000000000000083"},{"key":"0x004c3a8541d9ec369a9fd4bcdad885cc3b166da70ed6a83066924727d9ff147fce83","value":"0x0000000000000000000000000000000000000000000000000000000000000084"},{"key":"0x004c3a8541d9ec369a9fd4bcdad885cc3b166da70ed6a83066924727d9ff147fce84","value":"0x0000000000000000000000000000000000000000000000000000000000000085"},{"key":"0x004c3a8541d9ec369a9fd4bcdad885cc3b166da70ed6a83066924727d9ff147fce85","value":"0x0000000000000000000000000000000000000000000000000000000000000086"},{"key":"0x004c3a8541d9ec369a9fd4bcdad885cc3b166da70ed6a83066924727d9ff147fce86","value":"0x0000000000000000000000000000000000000000000000000000000000000087"},{"key":"0x004c3a8541d9ec369a9fd4bcdad885cc3b166da70ed6a83066924727d9ff147fce87","value":"0x0000000000000000000000000000000000000000000000000000000000000088"},{"key":"0x004c3a8541d9ec369a9fd4bcdad885cc3b166da70ed6a83066924727d9ff147fce88","value":"0x0000000000000000000000000000000000000000000000000000000000000089"},{"key":"0x004c3a8541d9ec369a9fd4bcdad885cc3b166da70ed6a83066924727d9ff147fce89","value":"0x000000000000000000000000000000000000000000000000000000000000008a"},{"key":"0x004c3a8541d9ec369a9fd4bcdad885cc3b166da70ed6a83066924727d9ff147fce8a","value":"0x000000000000000000000000000000000000000000000000000000000000008b"},{"key":"0x004c3a8541d9ec369a9fd4bcdad885cc3b166da70ed6a83066924727d9ff147fce8b","value":"0x000000000000000000000000000000000000000000000000000000000000008c"},{"key":"0x004c3a8541d9ec369a9fd4bcdad885cc3b166da70ed6a83066924727d9ff147fce8c","value":"0x000000000000000000000000000000000000000000000000000000000000008d"},{"key":"0x004c3a8541d9ec369a9fd4bcdad885cc3b166da70ed6a83066924727d9ff147fce8d","value":"0x000000000000000000000000000000000000000000000000000000000000008e"},{"key":"0x004c3a8541d9ec369a9fd4bcdad885cc3b166da70ed6a83066924727d9ff147fce8e","value":"0x000000000000000000000000000000000000000000000000000000000000008f"},{"key":"0x004c3a8541d9ec369a9fd4bcdad885cc3b166da70ed6a83066924727d9ff147fce8f","value":"0x0000000000000000000000000000000000000000000000000000000000000090"},{"key":"0x004c3a8541d9ec369a9fd4bcdad885cc3b166da70ed6a83066924727d9ff147fce90","value":"0x0000000000000000000000000000000000000000000000000000000000000091"},{"key":"0x004c3a8541d9ec369a9fd4bcdad885cc3b166da70ed6a83066924727d9ff147fce91","value":"0x0000000000000000000000000000000000000000000000000000000000000092"},{"key":"0x004c3a8541d9ec369a9fd4bcdad885cc3b166da70ed6a83066924727d9ff147fce92","value":"0x0000000000000000000000000000000000000000000000000000000000000093"},{"key":"0x004c3a8541d9ec369a9fd4bcdad885cc3b166da70ed6a83066924727d9ff147fce93","value":"0x0000000000000000000000000000000000000000000000000000000000000094"},{"key":"0x004c3a8541d9ec369a9fd4bcdad885cc3b166da70ed6a83066924727d9ff147fce94","value":"0x0000000000000000000000000000000000000000000000000000000000000095"},{"key":"0x004c3a8541d9ec369a9fd4bcdad885cc3b166da70ed6a83066924727d9ff147fce95","value":"0x0000000000000000000000000000000000000000000000000000000000000096"},{"key":"0x004c3a8541d9ec369a9fd4bcdad885cc3b166da70ed6a83066924727d9ff147fce96","value":"0x0000000000000000000000000000000000000000000000000000000000000097"},{"key":"0x004c3a8541d9ec369a9fd4bcdad885cc3b166da70ed6a83066924727d9ff147fce97","value":"0x0000000000000000000000000000000000000000000000000000000000000098"},{"key":"0x004c3a8541d9ec369a9fd4bcdad885cc3b166da70ed6a83066924727d9ff147fce98","value":"0x0000000000000000000000000000000000000000000000000000000000000099"},{"key":"0x004c3a8541d9ec369a9fd4bcdad885cc3b166da70ed6a83066924727d9ff147fce99","value":"0x000000000000000000000000000000000000000000000000000000000000009a"},{"key":"0x004c3a8541d9ec369a9fd4bcdad885cc3b166da70ed6a83066924727d9ff147fce9a","value":"0x000000000000000000000000000000000000000000000000000000000000009b"},{"key":"0x004c3a8541d9ec369a9fd4bcdad885cc3b166da70ed6a83066924727d9ff147fce9b","value":"0x000000000000000000000000000000000000000000000000000000000000009c"},{"key":"0x004c3a8541d9ec369a9fd4bcdad885cc3b166da70ed6a83066924727d9ff147fce9c","value":"0x000000000000000000000000000000000000000000000000000000000000009d"},{"key":"0x004c3a8541d9ec369a9fd4bcdad885cc3b166da70ed6a83066924727d9ff147fce9d","value":"0x000000000000000000000000000000000000000000000000000000000000009e"},{"key":"0x004c3a8541d9ec369a9fd4bcdad885cc3b166da70ed6a83066924727d9ff147fce9e","value":"0x000000000000000000000000000000000000000000000000000000000000009f"},{"key":"0x004c3a8541d9ec369a9fd4bcdad885cc3b166da70ed6a83066924727d9ff147fce9f","value":"0x00000000000000000000000000000000000000000000000000000000000000a0"},{"key":"0x004c3a8541d9ec369a9fd4bcdad885cc3b166da70ed6a83066924727d9ff147fcea0","value":"0x00000000000000000000000000000000000000000000000000000000000000a1"},{"key":"0x004c3a8541d9ec369a9fd4bcdad885cc3b166da70ed6a83066924727d9ff147fcea1","value":"0x00000000000000000000000000000000000000000000000000000000000000a2"},{"key":"0x004c3a8541d9ec369a9fd4bcdad885cc3b166da70ed6a83066924727d9ff147fcea2","value":"0x00000000000000000000000000000000000000000000000000000000000000a3"},{"key":"0x004c3a8541d9ec369a9fd4bcdad885cc3b166da70ed6a83066924727d9ff147fcea3","value":"0x00000000000000000000000000000000000000000000000000000000000000a4"},{"key":"0x004c3a8541d9ec369a9fd4bcdad885cc3b166da70ed6a83066924727d9ff147fcea4","value":"0x00000000000000000000000000000000000000000000000000000000000000a5"},{"key":"0x004c3a8541d9ec369a9fd4bcdad885cc3b166da70ed6a83066924727d9ff147fcea5","value":"0x00000000000000000000000000000000000000000000000000000000000000a6"},{"key":"0x004c3a8541d9ec369a9fd4bcdad885cc3b166da70ed6a83066924727d9ff147fcea6","value":"0x00000000000000000000000000000000000000000000000000000000000000a7"},{"key":"0x004c3a8541d9ec369a9fd4bcdad885cc3b166da70ed6a83066924727d9ff147fcea7","value":"0x00000000000000000000000000000000000000000000000000000000000000a8"},{"key":"0x004c3a8541d9ec369a9fd4bcdad885cc3b166da70ed6a83066924727d9ff147fcea8","value":"0x00000000000000000000000000000000000000000000000000000000000000a9"},{"key":"0x004c3a8541d9ec369a9fd4bcdad885cc3b166da70ed6a83066924727d9ff147fcea9","value":"0x00000000000000000000000000000000000000000000000000000000000000aa"},{"key":"0x004c3a8541d9ec369a9fd4bcdad885cc3b166da70ed6a83066924727d9ff147fceaa","value":"0x00000000000000000000000000000000000000000000000000000000000000ab"},{"key":"0x004c3a8541d9ec369a9fd4bcdad885cc3b166da70ed6a83066924727d9ff147fceab","value":"0x00000000000000000000000000000000000000000000000000000000000000ac"},{"key":"0x004c3a8541d9ec369a9fd4bcdad885cc3b166da70ed6a83066924727d9ff147fceac","value":"0x00000000000000000000000000000000000000000000000000000000000000ad"},{"key":"0x004c3a8541d9ec369a9fd4bcdad885cc3b166da70ed6a83066924727d9ff147fcead","value":"0x00000000000000000000000000000000000000000000000000000000000000ae"},{"key":"0x004c3a8541d9ec369a9fd4bcdad885cc3b166da70ed6a83066924727d9ff147fceae","value":"0x00000000000000000000000000000000000000000000000000000000000000af"},{"key":"0x004c3a8541d9ec369a9fd4bcdad885cc3b166da70ed6a83066924727d9ff147fceaf","value":"0x00000000000000000000000000000000000000000000000000000000000000b0"},{"key":"0x004c3a8541d9ec369a9fd4bcdad885cc3b166da70ed6a83066924727d9ff147fceb0","value":"0x00000000000000000000000000000000000000000000000000000000000000b1"},{"key":"0x004c3a8541d9ec369a9fd4bcdad885cc3b166da70ed6a83066924727d9ff147fceb1","value":"0x00000000000000000000000000000000000000000000000000000000000000b2"},{"key":"0x004c3a8541d9ec369a9fd4bcdad885cc3b166da70ed6a83066924727d9ff147fceb2","value":"0x00000000000000000000000000000000000000000000000000000000000000b3"},{"key":"0x004c3a8541d9ec369a9fd4bcdad885cc3b166da70ed6a83066924727d9ff147fceb3","value":"0x00000000000000000000000000000000000000000000000000000000000000b4"},{"key":"0x004c3a8541d9ec369a9fd4bcdad885cc3b166da70ed6a83066924727d9ff147fceb4","value":"0x00000000000000000000000000000000000000000000000000000000000000b5"},{"key":"0x004c3a8541d9ec369a9fd4bcdad885cc3b166da70ed6a83066924727d9ff147fceb5","value":"0x00000000000000000000000000000000000000000000000000000000000000b6"},{"key":"0x004c3a8541d9ec369a9fd4bcdad885cc3b166da70ed6a83066924727d9ff147fceb6","value":"0x00000000000000000000000000000000000000000000000000000000000000b7"},{"key":"0x004c3a8541d9ec369a9fd4bcdad885cc3b166da70ed6a83066924727d9ff147fceb7","value":"0x00000000000000000000000000000000000000000000000000000000000000b8"},{"key":"0x004c3a8541d9ec369a9fd4bcdad885cc3b166da70ed6a83066924727d9ff147fceb8","value":"0x00000000000000000000000000000000000000000000000000000000000000b9"},{"key":"0x004c3a8541d9ec369a9fd4bcdad885cc3b166da70ed6a83066924727d9ff147fceb9","value":"0x00000000000000000000000000000000000000000000000000000000000000ba"},{"key":"0x004c3a8541d9ec369a9fd4bcdad885cc3b166da70ed6a83066924727d9ff147fceba","value":"0x00000000000000000000000000000000000000000000000000000000000000bb"},{"key":"0x004c3a8541d9ec369a9fd4bcdad885cc3b166da70ed6a83066924727d9ff147fcebb","value":"0x00000000000000000000000000000000000000000000000000000000000000bc"},{"key":"0x004c3a8541d9ec369a9fd4bcdad885cc3b166da70ed6a83066924727d9ff147fcebc","value":"0x00000000000000000000000000000000000000000000000000000000000000bd"},{"key":"0x004c3a8541d9ec369a9fd4bcdad885cc3b166da70ed6a83066924727d9ff147fcebd","value":"0x00000000000000000000000000000000000000000000000000000000000000be"},{"key":"0x004c3a8541d9ec369a9fd4bcdad885cc3b166da70ed6a83066924727d9ff147fcebe","value":"0x00000000000000000000000000000000000000000000000000000000000000bf"},{"key":"0x004c3a8541d9ec369a9fd4bcdad885cc3b166da70ed6a83066924727d9ff147fcebf","value":"0x00000000000000000000000000000000000000000000000000000000000000c0"},{"key":"0x004c3a8541d9ec369a9fd4bcdad885cc3b166da70ed6a83066924727d9ff147fcec0","value":"0x00000000000000000000000000000000000000000000000000000000000000c1"},{"key":"0x004c3a8541d9ec369a9fd4bcdad885cc3b166da70ed6a83066924727d9ff147fcec1","value":"0x00000000000000000000000000000000000000000000000000000000000000c2"},{"key":"0x004c3a8541d9ec369a9fd4bcdad885cc3b166da70ed6a83066924727d9ff147fcec2","value":"0x00000000000000000000000000000000000000000000000000000000000000c3"},{"key":"0x004c3a8541d9ec369a9fd4bcdad885cc3b166da70ed6a83066924727d9ff147fcec3","value":"0x00000000000000000000000000000000000000000000000000000000000000c4"},{"key":"0x004c3a8541d9ec369a9fd4bcdad885cc3b166da70ed6a83066924727d9ff147fcec4","value":"0x00000000000000000000000000000000000000000000000000000000000000c5"},{"key":"0x004c3a8541d9ec369a9fd4bcdad885cc3b166da70ed6a83066924727d9ff147fcec5","value":"0x00000000000000000000000000000000000000000000000000000000000000c6"},{"key":"0x004c3a8541d9ec369a9fd4bcdad885cc3b166da70ed6a83066924727d9ff147fcec6","value":"0x00000000000000000000000000000000000000000000000000000000000000c7"},{"key":"0x004c3a8541d9ec369a9fd4bcdad885cc3b166da70ed6a83066924727d9ff147fcec7","value":"0x00000000000000000000000000000000000000000000000000000000000000c8"},{"key":"0x004c3a8541d9ec369a9fd4bcdad885cc3b166da70ed6a83066924727d9ff147fcec8","value":"0x00000000000000000000000000000000000000000000000000000000000000c9"},{"key":"0x004c3a8541d9ec369a9fd4bcdad885cc3b166da70ed6a83066924727d9ff147fcec9","value":"0x00000000000000000000000000000000000000000000000000000000000000ca"},{"key":"0x004c3a8541d9ec369a9fd4bcdad885cc3b166da70ed6a83066924727d9ff147fceca","value":"0x00000000000000000000000000000000000000000000000000000000000000cb"},{"key":"0x004c3a8541d9ec369a9fd4bcdad885cc3b166da70ed6a83066924727d9ff147fcecb","value":"0x00000000000000000000000000000000000000000000000000000000000000cc"},{"key":"0x004c3a8541d9ec369a9fd4bcdad885cc3b166da70ed6a83066924727d9ff147fcecc","value":"0x00000000000000000000000000000000000000000000000000000000000000cd"},{"key":"0x004c3a8541d9ec369a9fd4bcdad885cc3b166da70ed6a83066924727d9ff147fcecd","value":"0x00000000000000000000000000000000000000000000000000000000000000ce"},{"key":"0x004c3a8541d9ec369a9fd4bcdad885cc3b166da70ed6a83066924727d9ff147fcece","value":"0x00000000000000000000000000000000000000000000000000000000000000cf"},{"key":"0x004c3a8541d9ec369a9fd4bcdad885cc3b166da70ed6a83066924727d9ff147fcecf","value":"0x00000000000000000000000000000000000000000000000000000000000000d0"},{"key":"0x004c3a8541d9ec369a9fd4bcdad885cc3b166da70ed6a83066924727d9ff147fced0","value":"0x00000000000000000000000000000000000000000000000000000000000000d1"},{"key":"0x004c3a8541d9ec369a9fd4bcdad885cc3b166da70ed6a83066924727d9ff147fced1","value":"0x00000000000000000000000000000000000000000000000000000000000000d2"},{"key":"0x004c3a8541d9ec369a9fd4bcdad885cc3b166da70ed6a83066924727d9ff147fced2","value":"0x00000000000000000000000000000000000000000000000000000000000000d3"},{"key":"0x004c3a8541d9ec369a9fd4bcdad885cc3b166da70ed6a83066924727d9ff147fced3","value":"0x00000000000000000000000000000000000000000000000000000000000000d4"},{"key":"0x004c3a8541d9ec369a9fd4bcdad885cc3b166da70ed6a83066924727d9ff147fced4","value":"0x00000000000000000000000000000000000000000000000000000000000000d5"},{"key":"0x004c3a8541d9ec369a9fd4bcdad885cc3b166da70ed6a83066924727d9ff147fced5","value":"0x00000000000000000000000000000000000000000000000000000000000000d6"},{"key":"0x004c3a8541d9ec369a9fd4bcdad885cc3b166da70ed6a83066924727d9ff147fced6","value":"0x00000000000000000000000000000000000000000000000000000000000000d7"},{"key":"0x004c3a8541d9ec369a9fd4bcdad885cc3b166da70ed6a83066924727d9ff147fced7","value":"0x00000000000000000000000000000000000000000000000000000000000000d8"},{"key":"0x004c3a8541d9ec369a9fd4bcdad885cc3b166da70ed6a83066924727d9ff147fced8","value":"0x00000000000000000000000000000000000000000000000000000000000000d9"},{"key":"0x004c3a8541d9ec369a9fd4bcdad885cc3b166da70ed6a83066924727d9ff147fced9","value":"0x00000000000000000000000000000000000000000000000000000000000000da"},{"key":"0x004c3a8541d9ec369a9fd4bcdad885cc3b166da70ed6a83066924727d9ff147fceda","value":"0x00000000000000000000000000000000000000000000000000000000000000db"},{"key":"0x004c3a8541d9ec369a9fd4bcdad885cc3b166da70ed6a83066924727d9ff147fcedb","value":"0x00000000000000000000000000000000000000000000000000000000000000dc"},{"key":"0x004c3a8541d9ec369a9fd4bcdad885cc3b166da70ed6a83066924727d9ff147fcedc","value":"0x00000000000000000000000000000000000000000000000000000000000000dd"},{"key":"0x004c3a8541d9ec369a9fd4bcdad885cc3b166da70ed6a83066924727d9ff147fcedd","value":"0x00000000000000000000000000000000000000000000000000000000000000de"},{"key":"0x004c3a8541d9ec369a9fd4bcdad885cc3b166da70ed6a83066924727d9ff147fcede","value":"0x00000000000000000000000000000000000000000000000000000000000000df"},{"key":"0x004c3a8541d9ec369a9fd4bcdad885cc3b166da70ed6a83066924727d9ff147fcedf","value":"0x00000000000000000000000000000000000000000000000000000000000000e0"},{"key":"0x004c3a8541d9ec369a9fd4bcdad885cc3b166da70ed6a83066924727d9ff147fcee0","value":"0x00000000000000000000000000000000000000000000000000000000000000e1"},{"key":"0x004c3a8541d9ec369a9fd4bcdad885cc3b166da70ed6a83066924727d9ff147fcee1","value":"0x00000000000000000000000000000000000000000000000000000000000000e2"},{"key":"0x004c3a8541d9ec369a9fd4bcdad885cc3b166da70ed6a83066924727d9ff147fcee2","value":"0x00000000000000000000000000000000000000000000000000000000000000e3"},{"key":"0x004c3a8541d9ec369a9fd4bcdad885cc3b166da70ed6a83066924727d9ff147fcee3","value":"0x00000000000000000000000000000000000000000000000000000000000000e4"},{"key":"0x004c3a8541d9ec369a9fd4bcdad885cc3b166da70ed6a83066924727d9ff147fcee4","value":"0x00000000000000000000000000000000000000000000000000000000000000e5"},{"key":"0x004c3a8541d9ec369a9fd4bcdad885cc3b166da70ed6a83066924727d9ff147fcee5","value":"0x00000000000000000000000000000000000000000000000000000000000000e6"},{"key":"0x004c3a8541d9ec369a9fd4bcdad885cc3b166da70ed6a83066924727d9ff147fcee6","value":"0x00000000000000000000000000000000000000000000000000000000000000e7"},{"key":"0x004c3a8541d9ec369a9fd4bcdad885cc3b166da70ed6a83066924727d9ff147fcee7","value":"0x00000000000000000000000000000000000000000000000000000000000000e8"},{"key":"0x004c3a8541d9ec369a9fd4bcdad885cc3b166da70ed6a83066924727d9ff147fcee8","value":"0x00000000000000000000000000000000000000000000000000000000000000e9"},{"key":"0x004c3a8541d9ec369a9fd4bcdad885cc3b166da70ed6a83066924727d9ff147fcee9","value":"0x00000000000000000000000000000000000000000000000000000000000000ea"},{"key":"0x004c3a8541d9ec369a9fd4bcdad885cc3b166da70ed6a83066924727d9ff147fceea","value":"0x00000000000000000000000000000000000000000000000000000000000000eb"},{"key":"0x004c3a8541d9ec369a9fd4bcdad885cc3b166da70ed6a83066924727d9ff147fceeb","value":"0x00000000000000000000000000000000000000000000000000000000000000ec"},{"key":"0x004c3a8541d9ec369a9fd4bcdad885cc3b166da70ed6a83066924727d9ff147fceec","value":"0x00000000000000000000000000000000000000000000000000000000000000ed"},{"key":"0x004c3a8541d9ec369a9fd4bcdad885cc3b166da70ed6a83066924727d9ff147fceed","value":"0x00000000000000000000000000000000000000000000000000000000000000ee"},{"key":"0x004c3a8541d9ec369a9fd4bcdad885cc3b166da70ed6a83066924727d9ff147fceee","value":"0x00000000000000000000000000000000000000000000000000000000000000ef"},{"key":"0x004c3a8541d9ec369a9fd4bcdad885cc3b166da70ed6a83066924727d9ff147fceef","value":"0x00000000000000000000000000000000000000000000000000000000000000f0"},{"key":"0x004c3a8541d9ec369a9fd4bcdad885cc3b166da70ed6a83066924727d9ff147fcef0","value":"0x00000000000000000000000000000000000000000000000000000000000000f1"},{"key":"0x004c3a8541d9ec369a9fd4bcdad885cc3b166da70ed6a83066924727d9ff147fcef1","value":"0x00000000000000000000000000000000000000000000000000000000000000f2"},{"key":"0x004c3a8541d9ec369a9fd4bcdad885cc3b166da70ed6a83066924727d9ff147fcef2","value":"0x00000000000000000000000000000000000000000000000000000000000000f3"},{"key":"0x004c3a8541d9ec369a9fd4bcdad885cc3b166da70ed6a83066924727d9ff147fcef3","value":"0x00000000000000000000000000000000000000000000000000000000000000f4"},{"key":"0x004c3a8541d9ec369a9fd4bcdad885cc3b166da70ed6a83066924727d9ff147fcef4","value":"0x00000000000000000000000000000000000000000000000000000000000000f5"},{"key":"0x004c3a8541d9ec369a9fd4bcdad885cc3b166da70ed6a83066924727d9ff147fcef5","value":"0x00000000000000000000000000000000000000000000000000000000000000f6"},{"key":"0x004c3a8541d9ec369a9fd4bcdad885cc3b166da70ed6a83066924727d9ff147fcef6","value":"0x00000000000000000000000000000000000000000000000000000000000000f7"},{"key":"0x004c3a8541d9ec369a9fd4bcdad885cc3b166da70ed6a83066924727d9ff147fcef7","value":"0x00000000000000000000000000000000000000000000000000000000000000f8"},{"key":"0x004c3a8541d9ec369a9fd4bcdad885cc3b166da70ed6a83066924727d9ff147fcef8","value":"0x00000000000000000000000000000000000000000000000000000000000000f9"},{"key":"0x004c3a8541d9ec369a9fd4bcdad885cc3b166da70ed6a83066924727d9ff147fcef9","value":"0x00000000000000000000000000000000000000000000000000000000000000fa"},{"key":"0x004c3a8541d9ec369a9fd4bcdad885cc3b166da70ed6a83066924727d9ff147fcefa","value":"0x00000000000000000000000000000000000000000000000000000000000000fb"},{"key":"0x004c3a8541d9ec369a9fd4bcdad885cc3b166da70ed6a83066924727d9ff147fcefb","value":"0x00000000000000000000000000000000000000000000000000000000000000fc"},{"key":"0x004c3a8541d9ec369a9fd4bcdad885cc3b166da70ed6a83066924727d9ff147fcefc","value":"0x00000000000000000000000000000000000000000000000000000000000000fd"},{"key":"0x004c3a8541d9ec369a9fd4bcdad885cc3b166da70ed6a83066924727d9ff147fcefd","value":"0x00000000000000000000000000000000000000000000000000000000000000fe"},{"key":"0x004c3a8541d9ec369a9fd4bcdad885cc3b166da70ed6a83066924727d9ff147fcefe","value":"0x00000000000000000000000000000000000000000000000000000000000000ff"},{"key":"0x004c3a8541d9ec369a9fd4bcdad885cc3b166da70ed6a83066924727d9ff147fceff","value":"0x0000000000000000000000000000000000000000000000000000000000000100"}],"root":"0x090ef773023b99997f3c8c72e3fa1fbd9b0b62833ffb662e457b60530b358721"}],"sequence_vectors":[{"seed":8297,"ops":[{"op":"set","key":"0x0015c4813bb5b1499e0a7e0d46daab8e10c70940c008c566d5c68557b1523d1f2999","value":"0x00000000000000000000000000000000000000000000000000000000362952bd"},{"op":"set","key":"0xff2eb90959edcd0b6a8a7922b8b8d37cd9664001c217b4c8e6d2df4b8ede329a7191c71427661efcc7f991940e68aae22bede52be877c5e2b9b4e0c22051e2c61706","value":"0x000000000000000000000000000000000000000000000000000000005912e971"},{"op":"delete","key":"0xff2eb90959edcd0b6a8a7922b8b8d37cd9664001c217b4c8e6d2df4b8ede329a7191c71427661efcc7f991940e68aae22bede52be877c5e2b9b4e0c22051e2c61706"},{"op":"set","key":"0xff9c847c439d6431d36ddeb326cb553da2e967778a24afbd1d23a62412186faf016d21858cec8b9c323663e513206217295d0693bb4eb042fd7e5e863612ec45fe5c","value":"0x000000000000000000000000000000000000000000000000000000009e92aea6"},{"op":"set","key":"0x00998f08d7054351e5fa3ccadda32ab8d6c0c86a825dd360d57c0fe6b46612297c21","value":"0x0000000000000000000000000000000000000000000000000000000037f3974d"},{"op":"set","key":"0x00e61994d31ceae39cae5feb02303eb90e0d4ae90f5b0755c3c26adc81f360ec8516","value":"0x0000000000000000000000000000000000000000000000000000000091546180"},{"op":"set","key":"0xffd1afd6427b13f2ce7beb82a2cd6bbfc58eac8b2d46bc87bfbc78fbf440e18a635bd91a1bb3fea09cd07c1f6f0b5b0928ec465c3d5409c3dc4d9f3b384fd276b0df","value":"0x00000000000000000000000000000000000000000000000000000000d560d2d0"},{"op":"set","key":"0x002eb90959edcd0b6a8a7922b8b8d37cd9664001c217b4c8e6d2df4b8ede329a717c","value":"0x00000000000000000000000000000000000000000000000000000000b7e649ff"},{"op":"delete","key":"0xff9c847c439d6431d36ddeb326cb553da2e967778a24afbd1d23a62412186faf016d21858cec8b9c323663e513206217295d0693bb4eb042fd7e5e863612ec45fe5c"},{"op":"set","key":"0xffe60d91d1d11837912311124089c6b73c9d726c99f4df797eb48c2b7fec6e5e2cedb6b011186812600d2c38950eb3aa1e9e39d11456e96ac38d3ec9cb37e4816783","value":"0x0000000000000000000000000000000000000000000000000000000015716296"},{"op":"set","key":"0x0078a2812688114caf4452a5611322977e4b1000e59ef408d4c8e2c90028370bcbf3","value":"0x00000000000000000000000000000000000000000000000000000000d566656c"},{"op":"set","key":"0x000861030791246737d706c0d0017de01a26278bbe4ce8a134f5e0cd6f7eb69bcf6b","value":"0x00000000000000000000000000000000000000000000000000000000c6f30fd3"},{"op":"set","key":"0x0068e867f5f899fc01d641a00cea2c42d58f3cc061797135198a85301d1e2903062a","value":"0x00000000000000000000000000000000000000000000000000000000308a8072"},{"op":"set","key":"0xff46aeb3243f905d4e0eac7f60ee8962848c87e79dfbd019ece14fe45bd720ea6bc7d37b238b059b3f39fb34f03f189fc8c596ad3bc45f7231d1a8e723510074014d","value":"0x00000000000000000000000000000000000000000000000000000000fd6c27f9"},{"op":"set","key":"0x00a43fb2d7beb12b34786a837467f62b7d6c5dab4ab7eecc89c0b67db76a682927c1","value":"0x000000000000000000000000000000000000000000000000000000000b5daa14"},{"op":"set","key":"0x00543907253acb230f5dc96bbafd8ea0f37dd63b30e8a4a3675bb007673a31fbc5d7","value":"0x00000000000000000000000000000000000000000000000000000000ef86c437"},{"op":"set","key":"0xff0fb23a930b05e3bdfbdd0e6694fa9df9c5059ceabc60618bc40931ddb379f1f64c1111b166c73d061477381d77db7818b678c5a5595df51af307fccfda674238c3","value":"0x000000000000000000000000000000000000000000000000000000008687ece2"},{"op":"set","key":"0x0046aeb3243f905d4e0eac7f60ee8962848c87e79dfbd019ece14fe45bd720ea6b69","value":"0x000000000000000000000000000000000000000000000000000000008d81d15d"},{"op":"set","key":"0xff998f08d7054351e5fa3ccadda32ab8d6c0c86a825dd360d57c0fe6b46612297c216fd3a73b07a45409a19b5e6d545779e55b6900fddc4443dcb06e3d97e25e1908","value":"0x000000000000000000000000000000000000000000000000000000008f91e546"},{"op":"set","key":"0x002cebacb183e2f51a3b6cb9f7fe2bd11c399bf1bdb8ce95ed457d3e46190197a0fd","value":"0x00000000000000000000000000000000000000000000000000000000b638fa76"}],"roots_after":["0xcf6ca0dad8d69c45120da214b9052e614649a640e52560d0f47487073325e93d","0xa7247b2d9c8c6e49a18add897d235a5e72c292a5144008676972983ebd5e624e","0xcf6ca0dad8d69c45120da214b9052e614649a640e52560d0f47487073325e93d","0x9b659884c441cf90291fe8e2336c65d34cb0da60709da8a6165486c1a00829e4","0x1b5adc55d232b1c9b93c8bae8c1e736a2e596efabd3cc466de195d38ecbe9746","0xa604a44382e79cbca1ae6140d150d84980510447609e2a2356dce852861b3217","0x67994f2a47248a4677f1691706ec0ad76d479894fb7015f06bda6d4af4d46a55","0x99b2ce17b9b779b3f63f68d218182a59b928c80c1ac8d8bcb716cd3c202a20b3","0xda86ba5028d29db7d6006f7cf340f6af5203506cbcc76e72ba27302933a4943f","0xaa845405281cd62dc569bf03f8a6c1bd35609cd2c1860db56a9478254f8cb25e","0x1708bbd96a838a1f0816667f74a2d2a1a01b8c5499bcb533be4a45cf684b8bef","0xf8850e79c7adbebb051eb0ea11e3ca9866c11f660cb2b7d2c3892beb20246f26","0xc6c221bffc3947d15ad0e994e150fa0e654897bde5039e88bcda287cf24dc827","0xf1036ba9ebb2780554ab2596581122a999fe301b33446a3bf13c623dd8796f96","0x61cd8daf48607b812e063019741d2ae0d77bf9193b2b5f5996b756a22a488b1f","0x73aaf9a0a5b4f179bacf1f1c2fbcb630163797d02cdc5115977d26e8ea9f48ff","0xff83d59bd08841abbfc5cf10620dd334ec2fbb9bd0e579aec30905b4e40ccab9","0x44b55ce7592e1ba9aa3ea9c465cdadee3f7e09abccbec49aec6290eab112d0a4","0xe1d9ef033ebdf35275483848038f858fa43063805c1cc1b5ea96654745cc3b94","0xcff0d627850d4d4ea368e66ff2349c0caf454129e387d23df3c8dcf71534768f"]},{"seed":11832,"ops":[{"op":"set","key":"0xff1df77729bb76b139342cfc86aff011590bf6bb810f47e69a6babac30af96f9f5ffa1e869c4566231d09357daec90e71aa93821a7347b29175ef4a1bc02a0b38120","value":"0x0000000000000000000000000000000000000000000000000000000079b57838"},{"op":"set","key":"0xff3cbfd03e3f083b0987f841c4372031faf655a63472518a3613e2e30ecb542b51ef2c19e3ed459de7a67968b7004609b54e8e34fef08d7f9ed3c6eedcc980488255","value":"0x00000000000000000000000000000000000000000000000000000000449c8b5d"},{"op":"set","key":"0x007b1eefd69ea014244937aaeaea4e8d4d296c04edd75ae47897e5612ef5a1a25289","value":"0x00000000000000000000000000000000000000000000000000000000b5b13d29"},{"op":"set","key":"0xffa43fb2d7beb12b34786a837467f62b7d6c5dab4ab7eecc89c0b67db76a6829272094bf023796a3d4bdc16643cddf5395ab048ba346a4f50f1cc64da78ad7dd5266","value":"0x000000000000000000000000000000000000000000000000000000008cc69019"},{"op":"set","key":"0xff5963897639cd11d686b199366a1249d76bc89e0f7161c9eca48d9a199435952d4a28b184a4e022d38844c7967da6fbdf879ffafc6cfabf88c929ec9c9ff90040d1","value":"0x00000000000000000000000000000000000000000000000000000000af9bbd7d"},{"op":"set","key":"0x00e61994d31ceae39cae5feb02303eb90e0d4ae90f5b0755c3c26adc81f360ec85bf","value":"0x000000000000000000000000000000000000000000000000000000005dde837c"},{"op":"delete","key":"0xffa43fb2d7beb12b34786a837467f62b7d6c5dab4ab7eecc89c0b67db76a6829272094bf023796a3d4bdc16643cddf5395ab048ba346a4f50f1cc64da78ad7dd5266"},{"op":"delete","key":"0xff1df77729bb76b139342cfc86aff011590bf6bb810f47e69a6babac30af96f9f5ffa1e869c4566231d09357daec90e71aa93821a7347b29175ef4a1bc02a0b38120"},{"op":"delete","key":"0xff3cbfd03e3f083b0987f841c4372031faf655a63472518a3613e2e30ecb542b51ef2c19e3ed459de7a67968b7004609b54e8e34fef08d7f9ed3c6eedcc980488255"},{"op":"delete","key":"0x00e61994d31ceae39cae5feb02303eb90e0d4ae90f5b0755c3c26adc81f360ec85bf"},{"op":"set","key":"0xff0fb23a930b05e3bdfbdd0e6694fa9df9c5059ceabc60618bc40931ddb379f1f6065b90a89bb317ff71b1f933493a08b55f152abe9f5ae5b58400664d141c512119","value":"0x000000000000000000000000000000000000000000000000000000000a082d85"},{"op":"set","key":"0xff998f08d7054351e5fa3ccadda32ab8d6c0c86a825dd360d57c0fe6b46612297c725184c1ce99f155151a19aa14a427e1a784778319e2b193ac66bd6374b328cdff","value":"0x00000000000000000000000000000000000000000000000000000000a3ea3eb4"},{"op":"set","key":"0xff78a2812688114caf4452a5611322977e4b1000e59ef408d4c8e2c90028370bcb71e2e06bd4204550cd67cae560e42f8afcdc75e454a14f00ed2a8ec7c885869e40","value":"0x000000000000000000000000000000000000000000000000000000007435a9e4"},{"op":"delete","key":"0xff0fb23a930b05e3bdfbdd0e6694fa9df9c5059ceabc60618bc40931ddb379f1f6065b90a89bb317ff71b1f933493a08b55f152abe9f5ae5b58400664d141c512119"},{"op":"set","key":"0x000fb23a930b05e3bdfbdd0e6694fa9df9c5059ceabc60618bc40931ddb379f1f668","value":"0x000000000000000000000000000000000000000000000000000000000275abc8"},{"op":"delete","key":"0xff5963897639cd11d686b199366a1249d76bc89e0f7161c9eca48d9a199435952d4a28b184a4e022d38844c7967da6fbdf879ffafc6cfabf88c929ec9c9ff90040d1"},{"op":"set","key":"0x007293ce391f5cd652216ca8ea31069a06a82f393028dfc8786aa5efd7be734b2cbe","value":"0x0000000000000000000000000000000000000000000000000000000094f87f55"},{"op":"set","key":"0xff4c3a8541d9ec369a9fd4bcdad885cc3b166da70ed6a83066924727d9ff147fce963472d01342d91a0a268bdfee2ecf01a6d30f9021151faf2e2e6719120bf40f0a","value":"0x00000000000000000000000000000000000000000000000000000000fdac9fff"},{"op":"set","key":"0xff1df77729bb76b139342cfc86aff011590bf6bb810f47e69a6babac30af96f9f5ffa1e869c4566231d09357daec90e71aa93821a7347b29175ef4a1bc02a0b38181","value":"0x00000000000000000000000000000000000000000000000000000000e4d876b8"},{"op":"set","key":"0x00e2f41518226671332da4adddab1fdcb9903c7b895c7943b669067fe96b0baacfd5","value":"0x0000000000000000000000000000000000000000000000000000000019be8821"}],"roots_after":["0x5cdba549646b7612059692efa58d33f90059dfa9df06433f4438510c1b32e049","0x6ae6cd7fe0f7f9dfbff03d1d6b88c55f8c9f247584c70427a2a53612cdf202e6","0xcbe8e97d34728b819654a46096d4fe13abcdeed3e31cf088be21de915051259a","0x155969c29e159077b8a9997325ac9962fee077d64c9467f1ccd68618c9abbcdb","0x2439229c4decda037eb0a692540821cdd6c77df8ba51d478291d905e5de4f5e4","0xc252ede6498166ca351dc0346fdd1ee5df6694e3acf7f5714f2e80db687ccc79","0x7fcfd5973d297879b65209bb9cf1ca74c30ee717e13350d2c0447675165cb80b","0xdbb4e48dda8ae1f9287abccd62637085bb171f5897ad42e5019500209e89202d","0x14712da3d0ce63185cae268f29bf2593295476a83a8e95b3b4daf01a83274d6f","0x51098829074dc7618bdca7c079f0ac0577e0a08a0df5c92dcd104bf6cad43ea6","0x2643869090c9ca916157631cac68996dd01de67c66639eeb443f9b93ebe7df1c","0x9793adae9d275d569398dc631e09da40464320a0dc0c644d36d79f97d34b7288","0xef2bd1092bc3404855dd94e9452f97cebeb425d8027e7a56dc1c5fc0e93c38f6","0x85170f6f2bed9f200e8835f9d2c9bd13cb9267d77e834dcd73d43d1f897cc346","0x71c46fa0b2804adc2012db0d399c477429071b47f13fc31ddc125bc599e8fafb","0xeba1b27a56ff60fd186ede1c5ee445c59b2733f527c6ccc30b765b6917491720","0x2d0dcb278bc7892ad37afe2c523b73d18a60e76b1dc41d04469896c3b355d490","0x5aadbab832910092a62a49e78c5d76b10ffe26f3452a0bd47fd049e36eb31d63","0x53b8d1dab39520c882f5666b092ab47f59f4f3084c9471eefbb15d6a0c58ad5d","0x52ff67405b74b808b1649b64764de113df17e70ea5aebf5489bc1db7aad52422"]},{"seed":3102,"ops":[{"op":"set","key":"0xff2cebacb183e2f51a3b6cb9f7fe2bd11c399bf1bdb8ce95ed457d3e46190197a02a6a55293936c1e1b6894fbb3b2873c993bd580ec835889bf0f211410e368fc199","value":"0x000000000000000000000000000000000000000000000000000000002e422f9a"},{"op":"delete","key":"0xff2cebacb183e2f51a3b6cb9f7fe2bd11c399bf1bdb8ce95ed457d3e46190197a02a6a55293936c1e1b6894fbb3b2873c993bd580ec835889bf0f211410e368fc199"},{"op":"set","key":"0xff29e4e6bbd168fe5b46b7d34c34b628f95d54c4e75f9335a7e96282d1173b07dfd0f9925799d37553e0f34b2d7a49a135fc34a90b6fb99bfe70f2e43c28e758854d","value":"0x000000000000000000000000000000000000000000000000000000002ecaa733"},{"op":"set","key":"0x00d1afd6427b13f2ce7beb82a2cd6bbfc58eac8b2d46bc87bfbc78fbf440e18a635f","value":"0x0000000000000000000000000000000000000000000000000000000076fe3750"},{"op":"set","key":"0x000861030791246737d706c0d0017de01a26278bbe4ce8a134f5e0cd6f7eb69bcff9","value":"0x0000000000000000000000000000000000000000000000000000000035fd5ae2"},{"op":"set","key":"0xff998f08d7054351e5fa3ccadda32ab8d6c0c86a825dd360d57c0fe6b46612297cc290a8b5ac7a453c9cec5724a8a85ad4002fb6dc9c73e86aedfa05133317c0402e","value":"0x00000000000000000000000000000000000000000000000000000000be9e2390"},{"op":"set","key":"0x005963897639cd11d686b199366a1249d76bc89e0f7161c9eca48d9a199435952d33","value":"0x00000000000000000000000000000000000000000000000000000000b3e90b26"},{"op":"set","key":"0xffe57860ba0adcdd1294caf74da8b990a80a8a6861bc01b30dfe07450d25bab38f20483a0da833c6de846c07a8214bee6ee90bd1da5d9b9553f74784602fdd83dfb9","value":"0x0000000000000000000000000000000000000000000000000000000051dcd3af"},{"op":"set","key":"0x00e6b6d8d98be54d4c00cee378bedf964f755b5257bf1eabf52cf39aa987686fa743","value":"0x0000000000000000000000000000000000000000000000000000000083a3dad3"},{"op":"set","key":"0xff0fb23a930b05e3bdfbdd0e6694fa9df9c5059ceabc60618bc40931ddb379f1f6065b90a89bb317ff71b1f933493a08b55f152abe9f5ae5b58400664d141c512161","value":"0x00000000000000000000000000000000000000000000000000000000939e31a5"},{"op":"set","key":"0xff2cebacb183e2f51a3b6cb9f7fe2bd11c399bf1bdb8ce95ed457d3e46190197a018eb952465e3dd252f35c719bb6d7d14f62bf363e0afa74100d3046f81539e08b9","value":"0x000000000000000000000000000000000000000000000000000000002da16542"},{"op":"set","key":"0xff4b83f061efb2f4708114e46b5ef50a255528c28c80bbe5fc6ad88e892bd4c4ea37491f84eb77d352d7d8fca0484b856ceab7236e2f773d157b3fbd6d0586de7f48","value":"0x000000000000000000000000000000000000000000000000000000003b1510f6"},{"op":"set","key":"0x00c901cfbb39e551189323bea0af6cf9e81008c90dce65cbc2ff4672e263d7bb87f3","value":"0x0000000000000000000000000000000000000000000000000000000087d0f3c4"},{"op":"set","key":"0x0055a6923f7414ba482ae8a64b407f386a0bb0491b742bd4b22b5b4b44fca84e848f","value":"0x000000000000000000000000000000000000000000000000000000008cfbc63e"},{"op":"set","key":"0xff7b1eefd69ea014244937aaeaea4e8d4d296c04edd75ae47897e5612ef5a1a25254d51e5b726c0f3cfd1b03f07f25907b18767b99fef9eb727475858c458e1c7087","value":"0x00000000000000000000000000000000000000000000000000000000af70ae1b"},{"op":"set","key":"0xff29e4e6bbd168fe5b46b7d34c34b628f95d54c4e75f9335a7e96282d1173b07df754b0f807e1dc46306d4cb12e85e18d008678fa80cd9e6fa118c34e179bd412a87","value":"0x00000000000000000000000000000000000000000000000000000000d15c3b16"},{"op":"set","key":"0xff29e4e6bbd168fe5b46b7d34c34b628f95d54c4e75f9335a7e96282d1173b07df754b0f807e1dc46306d4cb12e85e18d008678fa80cd9e6fa118c34e179bd412a57","value":"0x000000000000000000000000000000000000000000000000000000003e5f6e17"},{"op":"set","key":"0x0055bbfb5b05f80543bd662a7b7a74980a2992612798087fa24e1fc86b8e8508e4b6","value":"0x000000000000000000000000000000000000000000000000000000002a25f39d"},{"op":"delete","key":"0xff0fb23a930b05e3bdfbdd0e6694fa9df9c5059ceabc60618bc40931ddb379f1f6065b90a89bb317ff71b1f933493a08b55f152abe9f5ae5b58400664d141c512161"},{"op":"set","key":"0x00e61994d31ceae39cae5feb02303eb90e0d4ae90f5b0755c3c26adc81f360ec85de","value":"0x00000000000000000000000000000000000000000000000000000000fd3f724c"}],"roots_after":["0xdd16f2b8e1c012d8ed988df1cd9e31fc06bceee33304ec90fdee4c9e0d6f2522","0x0000000000000000000000000000000000000000000000000000000000000000","0xe7c887160ebfbf178105a04157a5b67d669ac6899fbf632526152fc5cb82cd80","0xd77cc71cfc52b7a272f47bb93cf6e3b5ae4a2d7fc991e4e2b76586018620570b","0xfb180e479837451381e4ebbf7a21be6937dbab9b830eeb798cf1854b48bc82ad","0x663475566fe0b6afa6fb71c73802d98b37948ccc16237229d8722a66b31c48f0","0xe26dac98c3f044695b7bda1e7bab4e9e0422d41b367b3fb873d50734d15f8de8","0x90263ca13708230b2ae2d6413b76995a7c177ed239310a9628fe4aac1e2d51fe","0xd7893e542d99ae8c18bd7a495190a58e2e1e25252d4fa9378ef2694db0e751ee","0x4df5042f26d44caf9d655bda9bb46284498628761474d302392daff333e5b610","0x1113ff21db5505e243f390d5df0fc11549690813ff7a25e8bc60b5209d39ed72","0xe14995d380c1552d2901cacc440b51fed58732d5eb6b6adb202e891f3f53b6d6","0x5d3fd815f8282caf95aab8105212fb98b6f5516beb9fba2ed4f4ee502b85ed74","0x09b380d8286220693a4a9e4b85e074ea94cf0dadbccacd05d6f8dc5470438087","0x957d9940952d058f2523c52603f8cc27537ee0e40b8c452d9691e28ff0f9d782","0x7cc8b1cbe418de642947381e3fd9251730dff6165bb7d48de4388cce893d8aea","0x5ea579760671e854abc2adfcb037bd6f1470c26e5f4ba6e9392d05e1af830b6d","0xb4281adb36825d1f7dd6b7f0536a81ad9950fb727124828f0a94a79c32885052","0xa90bdbe39f8833c02f914c2ca37a5d5b91be83ba6a4721923b1ffe8899c40d9e","0x8c3fff51ed5749c6147c9bb35b5ac0f20f0f0ff92604b0d7cd0f83bcaecf52b4"]},{"seed":90210,"ops":[{"op":"set","key":"0xffd1afd6427b13f2ce7beb82a2cd6bbfc58eac8b2d46bc87bfbc78fbf440e18a63aeb0aca39c8f6ba440835a8e371b8b2719dec5b24a57f1da0e7519c3cd74beff52","value":"0x00000000000000000000000000000000000000000000000000000000cec06895"},{"op":"set","key":"0x00e61994d31ceae39cae5feb02303eb90e0d4ae90f5b0755c3c26adc81f360ec8587","value":"0x0000000000000000000000000000000000000000000000000000000026a125de"},{"op":"delete","key":"0x00e61994d31ceae39cae5feb02303eb90e0d4ae90f5b0755c3c26adc81f360ec8587"},{"op":"delete","key":"0xffd1afd6427b13f2ce7beb82a2cd6bbfc58eac8b2d46bc87bfbc78fbf440e18a63aeb0aca39c8f6ba440835a8e371b8b2719dec5b24a57f1da0e7519c3cd74beff52"},{"op":"set","key":"0x00c901cfbb39e551189323bea0af6cf9e81008c90dce65cbc2ff4672e263d7bb871a","value":"0x0000000000000000000000000000000000000000000000000000000038f9aacc"},{"op":"set","key":"0x005963897639cd11d686b199366a1249d76bc89e0f7161c9eca48d9a199435952d0f","value":"0x0000000000000000000000000000000000000000000000000000000053b3bca6"},{"op":"set","key":"0x0068e867f5f899fc01d641a00cea2c42d58f3cc061797135198a85301d1e290306c3","value":"0x0000000000000000000000000000000000000000000000000000000058e273d9"},{"op":"set","key":"0x00e61994d31ceae39cae5feb02303eb90e0d4ae90f5b0755c3c26adc81f360ec85b3","value":"0x000000000000000000000000000000000000000000000000000000008debe84f"},{"op":"delete","key":"0x00c901cfbb39e551189323bea0af6cf9e81008c90dce65cbc2ff4672e263d7bb871a"},{"op":"set","key":"0x0055bbfb5b05f80543bd662a7b7a74980a2992612798087fa24e1fc86b8e8508e4b2","value":"0x0000000000000000000000000000000000000000000000000000000052fbeee9"},{"op":"set","key":"0xff68e867f5f899fc01d641a00cea2c42d58f3cc061797135198a85301d1e290306efdc84373704529ffcbc3d4ed318de0ec2cdf5f61dd800c143f44a741af2f838a0","value":"0x0000000000000000000000000000000000000000000000000000000012acb6e5"},{"op":"set","key":"0xffe60d91d1d11837912311124089c6b73c9d726c99f4df797eb48c2b7fec6e5e2c3b9034b692f2597d8c9c9ad6921ecd112d2f69cc50a6e065034e2ace540e8928ab","value":"0x0000000000000000000000000000000000000000000000000000000088a67fe9"},{"op":"set","key":"0xffe60d91d1d11837912311124089c6b73c9d726c99f4df797eb48c2b7fec6e5e2c9c921b846417391ff7e9525580b53876b30e3f6cf563e5849525f6fa88bf230f1a","value":"0x0000000000000000000000000000000000000000000000000000000075b67af1"},{"op":"set","key":"0xff78a2812688114caf4452a5611322977e4b1000e59ef408d4c8e2c90028370bcb279b454b0f4a6026943322996543a3cbb6c41d7c467067d9bc010242feef3c2c4b","value":"0x00000000000000000000000000000000000000000000000000000000fd6d065d"},{"op":"set","key":"0x00bded9122e6171a5dad9ce2aaebd933abf4718e9bf12448cf113064c9fd8e6ad0a4","value":"0x000000000000000000000000000000000000000000000000000000001c92d573"},{"op":"delete","key":"0xffe60d91d1d11837912311124089c6b73c9d726c99f4df797eb48c2b7fec6e5e2c9c921b846417391ff7e9525580b53876b30e3f6cf563e5849525f6fa88bf230f1a"},{"op":"set","key":"0x003cbfd03e3f083b0987f841c4372031faf655a63472518a3613e2e30ecb542b514a","value":"0x00000000000000000000000000000000000000000000000000000000617ad32c"},{"op":"set","key":"0xffec64b2de1fc1d53b795f49a1c84ca4172bc3292472c3e244e4f8c225fa786f56b7bce92ed9ae70f9ca2c10d21fe857f18cd2b64d54c70193d8a0dd772967c2580d","value":"0x00000000000000000000000000000000000000000000000000000000565e29f9"},{"op":"delete","key":"0xff78a2812688114caf4452a5611322977e4b1000e59ef408d4c8e2c90028370bcb279b454b0f4a6026943322996543a3cbb6c41d7c467067d9bc010242feef3c2c4b"},{"op":"set","key":"0xffd1afd6427b13f2ce7beb82a2cd6bbfc58eac8b2d46bc87bfbc78fbf440e18a635bd91a1bb3fea09cd07c1f6f0b5b0928ec465c3d5409c3dc4d9f3b384fd276b0e3","value":"0x00000000000000000000000000000000000000000000000000000000f03eb650"}],"roots_after":["0xe4a4cc81bad5ec263e1470b6766f0e48c44cb6de9edfafe42ce6a742e7c0de35","0x628d4752f5a37f73d798da0fd84216c8c96d376e81ca8c011a1ef0e5b293037d","0xe4a4cc81bad5ec263e1470b6766f0e48c44cb6de9edfafe42ce6a742e7c0de35","0x0000000000000000000000000000000000000000000000000000000000000000","0x36d0ac2cbaa8daac84e3abb0711753ab2803d39fb7def6e34b95e607b5177c92","0xcdd2d3edb22a14874aa1b8adc01bad8e80dd4f6f0c603788bd28705981409ba7","0x0438e8ad308f977fe36353cde64fef7f1210dd3820b5275c3fffe712e342f125","0xe8514dc5fbf6794122aad2a058ad95df0835731e49061dfc5238fe8673b7cb57","0xfa1bed5d0c8187d176cdabee2a0b6d96ec72791326c1d9d00555e2752fa4ae06","0x388f561f8cd9b274f5c9cd3154ab49c7586c9cae7b28c0b7471be9be98cba3be","0x36fca86e1497e245753b5662e45cac10ce68278494adef2b1219b534d63411e2","0x98ebe09efe3ff35ee2f8b20732a3f30408119a25e620107c33f503cd39e55143","0xcd53e141a34d3c96464d66bc027b4cc0731739f1e645aced12c51072463f8fc4","0xbba122989fb7ec24899f02a1aa37fcf967d84f6c5207d843ac9a754b1eaa7ba3","0xb219a14dd80c8cc69152edbcec566fe773892d5e615e310b063e99def0f6aa79","0x3531deb75570f3a160ba9b471a10be04750c59ed54984186a46d4521e5058145","0xd3e835c31b1c00c8f864d7da97f035c6147452c3762a7f69b174751b815737a8","0xd74280c2db61bd2bd9d739ae4c38053ce6192f683907b95c6c0fed0ddf6b1b66","0x24a2abd1e5f37cbb1e104406be5ee8b6c4989c27bb6ec117ae4e18c0b66dfc9c","0x3a7c4099580d9b766538946c88846c95d7822fd4d6121a20e57e1791026600f5"]},{"seed":20260727,"ops":[{"op":"set","key":"0x002eb90959edcd0b6a8a7922b8b8d37cd9664001c217b4c8e6d2df4b8ede329a7170","value":"0x0000000000000000000000000000000000000000000000000000000068535e9a"},{"op":"set","key":"0xff3cbfd03e3f083b0987f841c4372031faf655a63472518a3613e2e30ecb542b5111589c57f4d8ad55d2c4ece4c6b63961c45be7479bfd08d04fab2d26d0acb7a091","value":"0x0000000000000000000000000000000000000000000000000000000056756dfe"},{"op":"set","key":"0x007b1eefd69ea014244937aaeaea4e8d4d296c04edd75ae47897e5612ef5a1a2521a","value":"0x000000000000000000000000000000000000000000000000000000005959a793"},{"op":"delete","key":"0x002eb90959edcd0b6a8a7922b8b8d37cd9664001c217b4c8e6d2df4b8ede329a7170"},{"op":"delete","key":"0x007b1eefd69ea014244937aaeaea4e8d4d296c04edd75ae47897e5612ef5a1a2521a"},{"op":"set","key":"0x0055a6923f7414ba482ae8a64b407f386a0bb0491b742bd4b22b5b4b44fca84e8468","value":"0x000000000000000000000000000000000000000000000000000000003c2b7202"},{"op":"delete","key":"0xff3cbfd03e3f083b0987f841c4372031faf655a63472518a3613e2e30ecb542b5111589c57f4d8ad55d2c4ece4c6b63961c45be7479bfd08d04fab2d26d0acb7a091"},{"op":"delete","key":"0x0055a6923f7414ba482ae8a64b407f386a0bb0491b742bd4b22b5b4b44fca84e8468"},{"op":"set","key":"0xffe60d91d1d11837912311124089c6b73c9d726c99f4df797eb48c2b7fec6e5e2c6066e8c722a574a900a59551ed956d32ca2b36e48771ed8b373da6b223bbbe9c8f","value":"0x000000000000000000000000000000000000000000000000000000009bb7df73"},{"op":"set","key":"0x009c847c439d6431d36ddeb326cb553da2e967778a24afbd1d23a62412186faf0130","value":"0x00000000000000000000000000000000000000000000000000000000def11b80"},{"op":"set","key":"0x007b1eefd69ea014244937aaeaea4e8d4d296c04edd75ae47897e5612ef5a1a2524d","value":"0x00000000000000000000000000000000000000000000000000000000f05708e7"},{"op":"set","key":"0x00998f08d7054351e5fa3ccadda32ab8d6c0c86a825dd360d57c0fe6b46612297cba","value":"0x00000000000000000000000000000000000000000000000000000000c433224b"},{"op":"set","key":"0xff5ac76a8f23131e4ca988c8afcc313130cc95e521d2691635fc26196d63e66a1f33b1e44125772918b9d44a12766eb0dad773e863d120780173c394789aa668e925","value":"0x00000000000000000000000000000000000000000000000000000000abbc594e"},{"op":"set","key":"0xff998f08d7054351e5fa3ccadda32ab8d6c0c86a825dd360d57c0fe6b46612297c36c8cce2938e709d43205827ddebecc780d7e631bb1acb24046af6e5b23e445aa2","value":"0x00000000000000000000000000000000000000000000000000000000219ea23a"},{"op":"set","key":"0x00d1d141c1bed8bb4f809292e9340a0209cd167484a24a5ca993fcac4b97086d89af","value":"0x00000000000000000000000000000000000000000000000000000000e015951e"},{"op":"delete","key":"0x00d1d141c1bed8bb4f809292e9340a0209cd167484a24a5ca993fcac4b97086d89af"},{"op":"set","key":"0xffc28c21982a56a6cecf595bfeb78297c7432abfb487cc75c09db2856a118f441df2a84c02b0cbaafcabc4cbb5f988d916f3287032f07e3f8bf51588b4311ed04c72","value":"0x00000000000000000000000000000000000000000000000000000000973ab40a"},{"op":"set","key":"0x00e2f41518226671332da4adddab1fdcb9903c7b895c7943b669067fe96b0baacfdb","value":"0x000000000000000000000000000000000000000000000000000000000c8a8e64"},{"op":"delete","key":"0x007b1eefd69ea014244937aaeaea4e8d4d296c04edd75ae47897e5612ef5a1a2524d"},{"op":"set","key":"0xff15c4813bb5b1499e0a7e0d46daab8e10c70940c008c566d5c68557b1523d1f29cac055e3c06fef0ce191be15524a246c11d1103158d4f8b49d31c629e552005366","value":"0x000000000000000000000000000000000000000000000000000000001a5e6148"}],"roots_after":["0x8a7b32f0179f8591d8d29bbc5ce478d2d1cca5ceebddb5c6880e6682fd1a7458","0x63e670849972d58347a67cb04d30b369145ea59ac06fd411c231b46c05b534c8","0xeedfc71a50202763dc37c74f994b52783a5e3ac19ff7c3d818c9bd2c95b562f7","0x1cfbe1c8b87bb9ecafd506ee003ddaeef688eaa24a564cd7147270130425709e","0xf8daed5ef73e34987561ea33d980ed96600f48700aa75ea5706c74f79babdb3d","0xcf348977fb91b672c061404e5bc6f063a444d19478b4aba7450531d48ae5b8f8","0xfa4116de24b5dce1e1ebb9736c78a00ec1eb957aaee7e9f9af3b74f68e43a73e","0x0000000000000000000000000000000000000000000000000000000000000000","0x5f2fc0f08e034ab42e6b4b8eec8038618707b8f2ae9722e14a6048d1935a438c","0xbc0812ac0dbf193721b4e90a5f23d702399df82d1e1da571c9e2388155b5409f","0xe105f7395ae6845ec524e9dcdf9a33600d7eece3b9dacaf7fee43deca2ad0957","0x5f30927f1062975cf9ad72910c99386ffc609b4dac5e1633a0a7a051ff221ca1","0x6f8469d0d9572b492c6c170c43f734aea5714da7d2e47ac9c10a01938439a1cb","0x7a75f5784c851040e436de70bafabe4abddc0baab0db46583879fdc1a7bcbd01","0xaa03aca82638a0c50233cb98152c122b0c21c4efb2a47035a690b5a65a2e54ed","0x7a75f5784c851040e436de70bafabe4abddc0baab0db46583879fdc1a7bcbd01","0xf6d984c0ed4b562ad15789d556f759f72b44627fb197780e88d36f450c60cd4e","0x52ae36bcfdb7559ffaf993e3d23b7727ada4f1403f12a9f16ba0324610852a8e","0x7d48f61dc2656ca3fc52efbb16bf80b4806cf43aaef56397c9d526d739ab2ad4","0x867a92986423254fd9f362a682695d3eb986bd5106cadabbd72dd0faf70c0cf9"]}],"embedding_vectors":{"address":"0xaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa","basic_data_key":"0x00d9ae2d236f8713a5bf808cda488167a56cc97e4b83006f42b1c06c0c3f053bbf00","code_hash_key":"0x00d9ae2d236f8713a5bf808cda488167a56cc97e4b83006f42b1c06c0c3f053bbf01","slots":[{"slot":0,"key":"0x00d9ae2d236f8713a5bf808cda488167a56cc97e4b83006f42b1c06c0c3f053bbf40"},{"slot":5,"key":"0x00d9ae2d236f8713a5bf808cda488167a56cc97e4b83006f42b1c06c0c3f053bbf45"},{"slot":63,"key":"0x00d9ae2d236f8713a5bf808cda488167a56cc97e4b83006f42b1c06c0c3f053bbf7f"},{"slot":64,"key":"0xffd9ae2d236f8713a5bf808cda488167a56cc97e4b83006f42b1c06c0c3f053bbf181495ef75f8d2005a4a5bd3c5139cf6b5cdd1a02574832253cc9932687b332a40"},{"slot":255,"key":"0xffd9ae2d236f8713a5bf808cda488167a56cc97e4b83006f42b1c06c0c3f053bbf181495ef75f8d2005a4a5bd3c5139cf6b5cdd1a02574832253cc9932687b332aff"},{"slot":256,"key":"0xffd9ae2d236f8713a5bf808cda488167a56cc97e4b83006f42b1c06c0c3f053bbfe717382bfeaa9f4c014431fb7bbc3c6946dd510a82d49c925d9df7735c26b16f00"},{"slot":1000,"key":"0xffd9ae2d236f8713a5bf808cda488167a56cc97e4b83006f42b1c06c0c3f053bbf2b1a6c18962d993b9da5ed300ce5696bdcad6e09f08b9c0735fa749d417650f9e8"},{"slot":57896044618658097711785492504343953926634992332820282019728792003956564819968,"key":"0xffd9ae2d236f8713a5bf808cda488167a56cc97e4b83006f42b1c06c0c3f053bbf23d37150867b80a99fea951c62d43f974e7d2f53089c5f5683b4fc1d4387286c00"}],"chunks":[{"chunk":0,"key":"0x00d9ae2d236f8713a5bf808cda488167a56cc97e4b83006f42b1c06c0c3f053bbf80"},{"chunk":5,"key":"0x00d9ae2d236f8713a5bf808cda488167a56cc97e4b83006f42b1c06c0c3f053bbf85"},{"chunk":127,"key":"0x00d9ae2d236f8713a5bf808cda488167a56cc97e4b83006f42b1c06c0c3f053bbfff"},{"chunk":128,"key":"0x01465f911ccfe7707c51a877fc621cffa8e700a5828cf92df27b142de919c0f6c800"},{"chunk":300,"key":"0x01465f911ccfe7707c51a877fc621cffa8e700a5828cf92df27b142de919c0f6c8ac"},{"chunk":383,"key":"0x01465f911ccfe7707c51a877fc621cffa8e700a5828cf92df27b142de919c0f6c8ff"},{"chunk":384,"key":"0x0171ad9e407a8d200ab2858bcf828d9016a0b4bc54e6e39dd3e31847b6220a63ce00"}]},"basic_data_vectors":[{"code_size":0,"nonce":0,"balance":"0","value":"0x0000000000000000000000000000000000000000000000000000000000000000"},{"code_size":0,"nonce":1,"balance":"1000000000000000000","value":"0x0000000000000000000000000000000100000000000000000de0b6b3a7640000"},{"code_size":287454020,"nonce":6153737369425722316,"balance":"1512366075204170929049582354406559215","value":"0x00000000112233445566778899aabbcc0123456789abcdef0123456789abcdef"},{"code_size":24576,"nonce":1,"balance":"1","value":"0x0000000000006000000000000000000100000000000000000000000000000001"}],"chunkify_vectors":[{"name":"empty","code":"0x","chunks":[]},{"name":"short","code":"0x6001","chunks":["0x0060010000000000000000000000000000000000000000000000000000000000"]},{"name":"push_boundary","code":"0x6060606060606060606060606060606060606060606060606060606060606060606060606060606060606060606060606060606060606060606060606060","chunks":["0x0060606060606060606060606060606060606060606060606060606060606060","0x0160606060606060606060606060606060606060606060606060606060606060"]},{"name":"push32_tail","code":"0x7feeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeee","chunks":["0x007feeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeee","0x02eeeeeeeeeeeeeeeeeeee000000000000000000000000000000000000000000"]},{"name":"zeros62","code":"0x0000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000","chunks":["0x0000000000000000000000000000000000000000000000000000000000000000","0x0000000000000000000000000000000000000000000000000000000000000000"]}]} +{ + "meta": { + "source": "execution-specs@ec412acfd (branch eip-8297-tests)", + "hasher": "blake3", + "generator": "export_vectors.py" + }, + "empty_root": "0x0000000000000000000000000000000000000000000000000000000000000000", + "trie_vectors": [ + { + "name": "empty", + "entries": [], + "root": "0x0000000000000000000000000000000000000000000000000000000000000000" + }, + { + "name": "single_account_leaf", + "entries": [ + { + "key": "0x0055bbfb5b05f80543bd662a7b7a74980a2992612798087fa24e1fc86b8e8508e400", + "value": "0x0000000000000000000000000000000000000000000000000000000000000007" + } + ], + "root": "0x22da077ee89a0e6e6259303169fb6c7a3133fafa3033515a355af9c4f9ed5ea0" + }, + { + "name": "one_header_stem_two_leaves", + "entries": [ + { + "key": "0x0055bbfb5b05f80543bd662a7b7a74980a2992612798087fa24e1fc86b8e8508e400", + "value": "0x0000000000000000000000000000000000000000000000000000000000000007" + }, + { + "key": "0x0055bbfb5b05f80543bd662a7b7a74980a2992612798087fa24e1fc86b8e8508e401", + "value": "0x0000000000000000000000000000000000000000000000000000000000000009" + } + ], + "root": "0x3599d1dd23fef634f3845fd935375be8a42169ecc90906632aa8afa2fa380812" + }, + { + "name": "two_accounts", + "entries": [ + { + "key": "0x0055bbfb5b05f80543bd662a7b7a74980a2992612798087fa24e1fc86b8e8508e400", + "value": "0x0000000000000000000000000000000000000000000000000000000000000007" + }, + { + "key": "0x0034bb4f3468340d5f72275da92420aed94db48bbc61823e92f29f14f51590373a00", + "value": "0x0000000000000000000000000000000000000000000000000000000000000008" + } + ], + "root": "0xbcca03773c89779e136f983eba516339660c732d5b0a1ccfc2c725dca29eefbc" + }, + { + "name": "cross_zone_small", + "entries": [ + { + "key": "0x0055bbfb5b05f80543bd662a7b7a74980a2992612798087fa24e1fc86b8e8508e400", + "value": "0x0000000000000000000000000000000000000000000000000000000000000001" + }, + { + "key": "0x0055bbfb5b05f80543bd662a7b7a74980a2992612798087fa24e1fc86b8e8508e401", + "value": "0x0000000000000000000000000000000000000000000000000000000000000002" + }, + { + "key": "0xff55bbfb5b05f80543bd662a7b7a74980a2992612798087fa24e1fc86b8e8508e406f5e5117ba26652e3cbef5ea24cc46f42709eb47be3c46f3a923b68a67f44c264", + "value": "0x0000000000000000000000000000000000000000000000000000000000000003" + }, + { + "key": "0xff55bbfb5b05f80543bd662a7b7a74980a2992612798087fa24e1fc86b8e8508e4f1ce240e3efd0b0855a335ac6b58ea33be1b2afb193608d6d21fd91308dd74bc64", + "value": "0x0000000000000000000000000000000000000000000000000000000000000004" + }, + { + "key": "0x0034bb4f3468340d5f72275da92420aed94db48bbc61823e92f29f14f51590373a00", + "value": "0x0000000000000000000000000000000000000000000000000000000000000005" + } + ], + "root": "0x3fc0f35560e81b2b1bc349decef42eb48a614d8497646e401e7b85abc0b16a30" + }, + { + "name": "full_header_stem", + "entries": [ + { + "key": "0x004c3a8541d9ec369a9fd4bcdad885cc3b166da70ed6a83066924727d9ff147fce00", + "value": "0x0000000000000000000000000000000000000000000000000000000000000001" + }, + { + "key": "0x004c3a8541d9ec369a9fd4bcdad885cc3b166da70ed6a83066924727d9ff147fce01", + "value": "0x0000000000000000000000000000000000000000000000000000000000000002" + }, + { + "key": "0x004c3a8541d9ec369a9fd4bcdad885cc3b166da70ed6a83066924727d9ff147fce02", + "value": "0x0000000000000000000000000000000000000000000000000000000000000003" + }, + { + "key": "0x004c3a8541d9ec369a9fd4bcdad885cc3b166da70ed6a83066924727d9ff147fce03", + "value": "0x0000000000000000000000000000000000000000000000000000000000000004" + }, + { + "key": "0x004c3a8541d9ec369a9fd4bcdad885cc3b166da70ed6a83066924727d9ff147fce04", + "value": "0x0000000000000000000000000000000000000000000000000000000000000005" + }, + { + "key": "0x004c3a8541d9ec369a9fd4bcdad885cc3b166da70ed6a83066924727d9ff147fce05", + "value": "0x0000000000000000000000000000000000000000000000000000000000000006" + }, + { + "key": "0x004c3a8541d9ec369a9fd4bcdad885cc3b166da70ed6a83066924727d9ff147fce06", + "value": "0x0000000000000000000000000000000000000000000000000000000000000007" + }, + { + "key": "0x004c3a8541d9ec369a9fd4bcdad885cc3b166da70ed6a83066924727d9ff147fce07", + "value": "0x0000000000000000000000000000000000000000000000000000000000000008" + }, + { + "key": "0x004c3a8541d9ec369a9fd4bcdad885cc3b166da70ed6a83066924727d9ff147fce08", + "value": "0x0000000000000000000000000000000000000000000000000000000000000009" + }, + { + "key": "0x004c3a8541d9ec369a9fd4bcdad885cc3b166da70ed6a83066924727d9ff147fce09", + "value": "0x000000000000000000000000000000000000000000000000000000000000000a" + }, + { + "key": "0x004c3a8541d9ec369a9fd4bcdad885cc3b166da70ed6a83066924727d9ff147fce0a", + "value": "0x000000000000000000000000000000000000000000000000000000000000000b" + }, + { + "key": "0x004c3a8541d9ec369a9fd4bcdad885cc3b166da70ed6a83066924727d9ff147fce0b", + "value": "0x000000000000000000000000000000000000000000000000000000000000000c" + }, + { + "key": "0x004c3a8541d9ec369a9fd4bcdad885cc3b166da70ed6a83066924727d9ff147fce0c", + "value": "0x000000000000000000000000000000000000000000000000000000000000000d" + }, + { + "key": "0x004c3a8541d9ec369a9fd4bcdad885cc3b166da70ed6a83066924727d9ff147fce0d", + "value": "0x000000000000000000000000000000000000000000000000000000000000000e" + }, + { + "key": "0x004c3a8541d9ec369a9fd4bcdad885cc3b166da70ed6a83066924727d9ff147fce0e", + "value": "0x000000000000000000000000000000000000000000000000000000000000000f" + }, + { + "key": "0x004c3a8541d9ec369a9fd4bcdad885cc3b166da70ed6a83066924727d9ff147fce0f", + "value": "0x0000000000000000000000000000000000000000000000000000000000000010" + }, + { + "key": "0x004c3a8541d9ec369a9fd4bcdad885cc3b166da70ed6a83066924727d9ff147fce10", + "value": "0x0000000000000000000000000000000000000000000000000000000000000011" + }, + { + "key": "0x004c3a8541d9ec369a9fd4bcdad885cc3b166da70ed6a83066924727d9ff147fce11", + "value": "0x0000000000000000000000000000000000000000000000000000000000000012" + }, + { + "key": "0x004c3a8541d9ec369a9fd4bcdad885cc3b166da70ed6a83066924727d9ff147fce12", + "value": "0x0000000000000000000000000000000000000000000000000000000000000013" + }, + { + "key": "0x004c3a8541d9ec369a9fd4bcdad885cc3b166da70ed6a83066924727d9ff147fce13", + "value": "0x0000000000000000000000000000000000000000000000000000000000000014" + }, + { + "key": "0x004c3a8541d9ec369a9fd4bcdad885cc3b166da70ed6a83066924727d9ff147fce14", + "value": "0x0000000000000000000000000000000000000000000000000000000000000015" + }, + { + "key": "0x004c3a8541d9ec369a9fd4bcdad885cc3b166da70ed6a83066924727d9ff147fce15", + "value": "0x0000000000000000000000000000000000000000000000000000000000000016" + }, + { + "key": "0x004c3a8541d9ec369a9fd4bcdad885cc3b166da70ed6a83066924727d9ff147fce16", + "value": "0x0000000000000000000000000000000000000000000000000000000000000017" + }, + { + "key": "0x004c3a8541d9ec369a9fd4bcdad885cc3b166da70ed6a83066924727d9ff147fce17", + "value": "0x0000000000000000000000000000000000000000000000000000000000000018" + }, + { + "key": "0x004c3a8541d9ec369a9fd4bcdad885cc3b166da70ed6a83066924727d9ff147fce18", + "value": "0x0000000000000000000000000000000000000000000000000000000000000019" + }, + { + "key": "0x004c3a8541d9ec369a9fd4bcdad885cc3b166da70ed6a83066924727d9ff147fce19", + "value": "0x000000000000000000000000000000000000000000000000000000000000001a" + }, + { + "key": "0x004c3a8541d9ec369a9fd4bcdad885cc3b166da70ed6a83066924727d9ff147fce1a", + "value": "0x000000000000000000000000000000000000000000000000000000000000001b" + }, + { + "key": "0x004c3a8541d9ec369a9fd4bcdad885cc3b166da70ed6a83066924727d9ff147fce1b", + "value": "0x000000000000000000000000000000000000000000000000000000000000001c" + }, + { + "key": "0x004c3a8541d9ec369a9fd4bcdad885cc3b166da70ed6a83066924727d9ff147fce1c", + "value": "0x000000000000000000000000000000000000000000000000000000000000001d" + }, + { + "key": "0x004c3a8541d9ec369a9fd4bcdad885cc3b166da70ed6a83066924727d9ff147fce1d", + "value": "0x000000000000000000000000000000000000000000000000000000000000001e" + }, + { + "key": "0x004c3a8541d9ec369a9fd4bcdad885cc3b166da70ed6a83066924727d9ff147fce1e", + "value": "0x000000000000000000000000000000000000000000000000000000000000001f" + }, + { + "key": "0x004c3a8541d9ec369a9fd4bcdad885cc3b166da70ed6a83066924727d9ff147fce1f", + "value": "0x0000000000000000000000000000000000000000000000000000000000000020" + }, + { + "key": "0x004c3a8541d9ec369a9fd4bcdad885cc3b166da70ed6a83066924727d9ff147fce20", + "value": "0x0000000000000000000000000000000000000000000000000000000000000021" + }, + { + "key": "0x004c3a8541d9ec369a9fd4bcdad885cc3b166da70ed6a83066924727d9ff147fce21", + "value": "0x0000000000000000000000000000000000000000000000000000000000000022" + }, + { + "key": "0x004c3a8541d9ec369a9fd4bcdad885cc3b166da70ed6a83066924727d9ff147fce22", + "value": "0x0000000000000000000000000000000000000000000000000000000000000023" + }, + { + "key": "0x004c3a8541d9ec369a9fd4bcdad885cc3b166da70ed6a83066924727d9ff147fce23", + "value": "0x0000000000000000000000000000000000000000000000000000000000000024" + }, + { + "key": "0x004c3a8541d9ec369a9fd4bcdad885cc3b166da70ed6a83066924727d9ff147fce24", + "value": "0x0000000000000000000000000000000000000000000000000000000000000025" + }, + { + "key": "0x004c3a8541d9ec369a9fd4bcdad885cc3b166da70ed6a83066924727d9ff147fce25", + "value": "0x0000000000000000000000000000000000000000000000000000000000000026" + }, + { + "key": "0x004c3a8541d9ec369a9fd4bcdad885cc3b166da70ed6a83066924727d9ff147fce26", + "value": "0x0000000000000000000000000000000000000000000000000000000000000027" + }, + { + "key": "0x004c3a8541d9ec369a9fd4bcdad885cc3b166da70ed6a83066924727d9ff147fce27", + "value": "0x0000000000000000000000000000000000000000000000000000000000000028" + }, + { + "key": "0x004c3a8541d9ec369a9fd4bcdad885cc3b166da70ed6a83066924727d9ff147fce28", + "value": "0x0000000000000000000000000000000000000000000000000000000000000029" + }, + { + "key": "0x004c3a8541d9ec369a9fd4bcdad885cc3b166da70ed6a83066924727d9ff147fce29", + "value": "0x000000000000000000000000000000000000000000000000000000000000002a" + }, + { + "key": "0x004c3a8541d9ec369a9fd4bcdad885cc3b166da70ed6a83066924727d9ff147fce2a", + "value": "0x000000000000000000000000000000000000000000000000000000000000002b" + }, + { + "key": "0x004c3a8541d9ec369a9fd4bcdad885cc3b166da70ed6a83066924727d9ff147fce2b", + "value": "0x000000000000000000000000000000000000000000000000000000000000002c" + }, + { + "key": "0x004c3a8541d9ec369a9fd4bcdad885cc3b166da70ed6a83066924727d9ff147fce2c", + "value": "0x000000000000000000000000000000000000000000000000000000000000002d" + }, + { + "key": "0x004c3a8541d9ec369a9fd4bcdad885cc3b166da70ed6a83066924727d9ff147fce2d", + "value": "0x000000000000000000000000000000000000000000000000000000000000002e" + }, + { + "key": "0x004c3a8541d9ec369a9fd4bcdad885cc3b166da70ed6a83066924727d9ff147fce2e", + "value": "0x000000000000000000000000000000000000000000000000000000000000002f" + }, + { + "key": "0x004c3a8541d9ec369a9fd4bcdad885cc3b166da70ed6a83066924727d9ff147fce2f", + "value": "0x0000000000000000000000000000000000000000000000000000000000000030" + }, + { + "key": "0x004c3a8541d9ec369a9fd4bcdad885cc3b166da70ed6a83066924727d9ff147fce30", + "value": "0x0000000000000000000000000000000000000000000000000000000000000031" + }, + { + "key": "0x004c3a8541d9ec369a9fd4bcdad885cc3b166da70ed6a83066924727d9ff147fce31", + "value": "0x0000000000000000000000000000000000000000000000000000000000000032" + }, + { + "key": "0x004c3a8541d9ec369a9fd4bcdad885cc3b166da70ed6a83066924727d9ff147fce32", + "value": "0x0000000000000000000000000000000000000000000000000000000000000033" + }, + { + "key": "0x004c3a8541d9ec369a9fd4bcdad885cc3b166da70ed6a83066924727d9ff147fce33", + "value": "0x0000000000000000000000000000000000000000000000000000000000000034" + }, + { + "key": "0x004c3a8541d9ec369a9fd4bcdad885cc3b166da70ed6a83066924727d9ff147fce34", + "value": "0x0000000000000000000000000000000000000000000000000000000000000035" + }, + { + "key": "0x004c3a8541d9ec369a9fd4bcdad885cc3b166da70ed6a83066924727d9ff147fce35", + "value": "0x0000000000000000000000000000000000000000000000000000000000000036" + }, + { + "key": "0x004c3a8541d9ec369a9fd4bcdad885cc3b166da70ed6a83066924727d9ff147fce36", + "value": "0x0000000000000000000000000000000000000000000000000000000000000037" + }, + { + "key": "0x004c3a8541d9ec369a9fd4bcdad885cc3b166da70ed6a83066924727d9ff147fce37", + "value": "0x0000000000000000000000000000000000000000000000000000000000000038" + }, + { + "key": "0x004c3a8541d9ec369a9fd4bcdad885cc3b166da70ed6a83066924727d9ff147fce38", + "value": "0x0000000000000000000000000000000000000000000000000000000000000039" + }, + { + "key": "0x004c3a8541d9ec369a9fd4bcdad885cc3b166da70ed6a83066924727d9ff147fce39", + "value": "0x000000000000000000000000000000000000000000000000000000000000003a" + }, + { + "key": "0x004c3a8541d9ec369a9fd4bcdad885cc3b166da70ed6a83066924727d9ff147fce3a", + "value": "0x000000000000000000000000000000000000000000000000000000000000003b" + }, + { + "key": "0x004c3a8541d9ec369a9fd4bcdad885cc3b166da70ed6a83066924727d9ff147fce3b", + "value": "0x000000000000000000000000000000000000000000000000000000000000003c" + }, + { + "key": "0x004c3a8541d9ec369a9fd4bcdad885cc3b166da70ed6a83066924727d9ff147fce3c", + "value": "0x000000000000000000000000000000000000000000000000000000000000003d" + }, + { + "key": "0x004c3a8541d9ec369a9fd4bcdad885cc3b166da70ed6a83066924727d9ff147fce3d", + "value": "0x000000000000000000000000000000000000000000000000000000000000003e" + }, + { + "key": "0x004c3a8541d9ec369a9fd4bcdad885cc3b166da70ed6a83066924727d9ff147fce3e", + "value": "0x000000000000000000000000000000000000000000000000000000000000003f" + }, + { + "key": "0x004c3a8541d9ec369a9fd4bcdad885cc3b166da70ed6a83066924727d9ff147fce3f", + "value": "0x0000000000000000000000000000000000000000000000000000000000000040" + }, + { + "key": "0x004c3a8541d9ec369a9fd4bcdad885cc3b166da70ed6a83066924727d9ff147fce40", + "value": "0x0000000000000000000000000000000000000000000000000000000000000041" + }, + { + "key": "0x004c3a8541d9ec369a9fd4bcdad885cc3b166da70ed6a83066924727d9ff147fce41", + "value": "0x0000000000000000000000000000000000000000000000000000000000000042" + }, + { + "key": "0x004c3a8541d9ec369a9fd4bcdad885cc3b166da70ed6a83066924727d9ff147fce42", + "value": "0x0000000000000000000000000000000000000000000000000000000000000043" + }, + { + "key": "0x004c3a8541d9ec369a9fd4bcdad885cc3b166da70ed6a83066924727d9ff147fce43", + "value": "0x0000000000000000000000000000000000000000000000000000000000000044" + }, + { + "key": "0x004c3a8541d9ec369a9fd4bcdad885cc3b166da70ed6a83066924727d9ff147fce44", + "value": "0x0000000000000000000000000000000000000000000000000000000000000045" + }, + { + "key": "0x004c3a8541d9ec369a9fd4bcdad885cc3b166da70ed6a83066924727d9ff147fce45", + "value": "0x0000000000000000000000000000000000000000000000000000000000000046" + }, + { + "key": "0x004c3a8541d9ec369a9fd4bcdad885cc3b166da70ed6a83066924727d9ff147fce46", + "value": "0x0000000000000000000000000000000000000000000000000000000000000047" + }, + { + "key": "0x004c3a8541d9ec369a9fd4bcdad885cc3b166da70ed6a83066924727d9ff147fce47", + "value": "0x0000000000000000000000000000000000000000000000000000000000000048" + }, + { + "key": "0x004c3a8541d9ec369a9fd4bcdad885cc3b166da70ed6a83066924727d9ff147fce48", + "value": "0x0000000000000000000000000000000000000000000000000000000000000049" + }, + { + "key": "0x004c3a8541d9ec369a9fd4bcdad885cc3b166da70ed6a83066924727d9ff147fce49", + "value": "0x000000000000000000000000000000000000000000000000000000000000004a" + }, + { + "key": "0x004c3a8541d9ec369a9fd4bcdad885cc3b166da70ed6a83066924727d9ff147fce4a", + "value": "0x000000000000000000000000000000000000000000000000000000000000004b" + }, + { + "key": "0x004c3a8541d9ec369a9fd4bcdad885cc3b166da70ed6a83066924727d9ff147fce4b", + "value": "0x000000000000000000000000000000000000000000000000000000000000004c" + }, + { + "key": "0x004c3a8541d9ec369a9fd4bcdad885cc3b166da70ed6a83066924727d9ff147fce4c", + "value": "0x000000000000000000000000000000000000000000000000000000000000004d" + }, + { + "key": "0x004c3a8541d9ec369a9fd4bcdad885cc3b166da70ed6a83066924727d9ff147fce4d", + "value": "0x000000000000000000000000000000000000000000000000000000000000004e" + }, + { + "key": "0x004c3a8541d9ec369a9fd4bcdad885cc3b166da70ed6a83066924727d9ff147fce4e", + "value": "0x000000000000000000000000000000000000000000000000000000000000004f" + }, + { + "key": "0x004c3a8541d9ec369a9fd4bcdad885cc3b166da70ed6a83066924727d9ff147fce4f", + "value": "0x0000000000000000000000000000000000000000000000000000000000000050" + }, + { + "key": "0x004c3a8541d9ec369a9fd4bcdad885cc3b166da70ed6a83066924727d9ff147fce50", + "value": "0x0000000000000000000000000000000000000000000000000000000000000051" + }, + { + "key": "0x004c3a8541d9ec369a9fd4bcdad885cc3b166da70ed6a83066924727d9ff147fce51", + "value": "0x0000000000000000000000000000000000000000000000000000000000000052" + }, + { + "key": "0x004c3a8541d9ec369a9fd4bcdad885cc3b166da70ed6a83066924727d9ff147fce52", + "value": "0x0000000000000000000000000000000000000000000000000000000000000053" + }, + { + "key": "0x004c3a8541d9ec369a9fd4bcdad885cc3b166da70ed6a83066924727d9ff147fce53", + "value": "0x0000000000000000000000000000000000000000000000000000000000000054" + }, + { + "key": "0x004c3a8541d9ec369a9fd4bcdad885cc3b166da70ed6a83066924727d9ff147fce54", + "value": "0x0000000000000000000000000000000000000000000000000000000000000055" + }, + { + "key": "0x004c3a8541d9ec369a9fd4bcdad885cc3b166da70ed6a83066924727d9ff147fce55", + "value": "0x0000000000000000000000000000000000000000000000000000000000000056" + }, + { + "key": "0x004c3a8541d9ec369a9fd4bcdad885cc3b166da70ed6a83066924727d9ff147fce56", + "value": "0x0000000000000000000000000000000000000000000000000000000000000057" + }, + { + "key": "0x004c3a8541d9ec369a9fd4bcdad885cc3b166da70ed6a83066924727d9ff147fce57", + "value": "0x0000000000000000000000000000000000000000000000000000000000000058" + }, + { + "key": "0x004c3a8541d9ec369a9fd4bcdad885cc3b166da70ed6a83066924727d9ff147fce58", + "value": "0x0000000000000000000000000000000000000000000000000000000000000059" + }, + { + "key": "0x004c3a8541d9ec369a9fd4bcdad885cc3b166da70ed6a83066924727d9ff147fce59", + "value": "0x000000000000000000000000000000000000000000000000000000000000005a" + }, + { + "key": "0x004c3a8541d9ec369a9fd4bcdad885cc3b166da70ed6a83066924727d9ff147fce5a", + "value": "0x000000000000000000000000000000000000000000000000000000000000005b" + }, + { + "key": "0x004c3a8541d9ec369a9fd4bcdad885cc3b166da70ed6a83066924727d9ff147fce5b", + "value": "0x000000000000000000000000000000000000000000000000000000000000005c" + }, + { + "key": "0x004c3a8541d9ec369a9fd4bcdad885cc3b166da70ed6a83066924727d9ff147fce5c", + "value": "0x000000000000000000000000000000000000000000000000000000000000005d" + }, + { + "key": "0x004c3a8541d9ec369a9fd4bcdad885cc3b166da70ed6a83066924727d9ff147fce5d", + "value": "0x000000000000000000000000000000000000000000000000000000000000005e" + }, + { + "key": "0x004c3a8541d9ec369a9fd4bcdad885cc3b166da70ed6a83066924727d9ff147fce5e", + "value": "0x000000000000000000000000000000000000000000000000000000000000005f" + }, + { + "key": "0x004c3a8541d9ec369a9fd4bcdad885cc3b166da70ed6a83066924727d9ff147fce5f", + "value": "0x0000000000000000000000000000000000000000000000000000000000000060" + }, + { + "key": "0x004c3a8541d9ec369a9fd4bcdad885cc3b166da70ed6a83066924727d9ff147fce60", + "value": "0x0000000000000000000000000000000000000000000000000000000000000061" + }, + { + "key": "0x004c3a8541d9ec369a9fd4bcdad885cc3b166da70ed6a83066924727d9ff147fce61", + "value": "0x0000000000000000000000000000000000000000000000000000000000000062" + }, + { + "key": "0x004c3a8541d9ec369a9fd4bcdad885cc3b166da70ed6a83066924727d9ff147fce62", + "value": "0x0000000000000000000000000000000000000000000000000000000000000063" + }, + { + "key": "0x004c3a8541d9ec369a9fd4bcdad885cc3b166da70ed6a83066924727d9ff147fce63", + "value": "0x0000000000000000000000000000000000000000000000000000000000000064" + }, + { + "key": "0x004c3a8541d9ec369a9fd4bcdad885cc3b166da70ed6a83066924727d9ff147fce64", + "value": "0x0000000000000000000000000000000000000000000000000000000000000065" + }, + { + "key": "0x004c3a8541d9ec369a9fd4bcdad885cc3b166da70ed6a83066924727d9ff147fce65", + "value": "0x0000000000000000000000000000000000000000000000000000000000000066" + }, + { + "key": "0x004c3a8541d9ec369a9fd4bcdad885cc3b166da70ed6a83066924727d9ff147fce66", + "value": "0x0000000000000000000000000000000000000000000000000000000000000067" + }, + { + "key": "0x004c3a8541d9ec369a9fd4bcdad885cc3b166da70ed6a83066924727d9ff147fce67", + "value": "0x0000000000000000000000000000000000000000000000000000000000000068" + }, + { + "key": "0x004c3a8541d9ec369a9fd4bcdad885cc3b166da70ed6a83066924727d9ff147fce68", + "value": "0x0000000000000000000000000000000000000000000000000000000000000069" + }, + { + "key": "0x004c3a8541d9ec369a9fd4bcdad885cc3b166da70ed6a83066924727d9ff147fce69", + "value": "0x000000000000000000000000000000000000000000000000000000000000006a" + }, + { + "key": "0x004c3a8541d9ec369a9fd4bcdad885cc3b166da70ed6a83066924727d9ff147fce6a", + "value": "0x000000000000000000000000000000000000000000000000000000000000006b" + }, + { + "key": "0x004c3a8541d9ec369a9fd4bcdad885cc3b166da70ed6a83066924727d9ff147fce6b", + "value": "0x000000000000000000000000000000000000000000000000000000000000006c" + }, + { + "key": "0x004c3a8541d9ec369a9fd4bcdad885cc3b166da70ed6a83066924727d9ff147fce6c", + "value": "0x000000000000000000000000000000000000000000000000000000000000006d" + }, + { + "key": "0x004c3a8541d9ec369a9fd4bcdad885cc3b166da70ed6a83066924727d9ff147fce6d", + "value": "0x000000000000000000000000000000000000000000000000000000000000006e" + }, + { + "key": "0x004c3a8541d9ec369a9fd4bcdad885cc3b166da70ed6a83066924727d9ff147fce6e", + "value": "0x000000000000000000000000000000000000000000000000000000000000006f" + }, + { + "key": "0x004c3a8541d9ec369a9fd4bcdad885cc3b166da70ed6a83066924727d9ff147fce6f", + "value": "0x0000000000000000000000000000000000000000000000000000000000000070" + }, + { + "key": "0x004c3a8541d9ec369a9fd4bcdad885cc3b166da70ed6a83066924727d9ff147fce70", + "value": "0x0000000000000000000000000000000000000000000000000000000000000071" + }, + { + "key": "0x004c3a8541d9ec369a9fd4bcdad885cc3b166da70ed6a83066924727d9ff147fce71", + "value": "0x0000000000000000000000000000000000000000000000000000000000000072" + }, + { + "key": "0x004c3a8541d9ec369a9fd4bcdad885cc3b166da70ed6a83066924727d9ff147fce72", + "value": "0x0000000000000000000000000000000000000000000000000000000000000073" + }, + { + "key": "0x004c3a8541d9ec369a9fd4bcdad885cc3b166da70ed6a83066924727d9ff147fce73", + "value": "0x0000000000000000000000000000000000000000000000000000000000000074" + }, + { + "key": "0x004c3a8541d9ec369a9fd4bcdad885cc3b166da70ed6a83066924727d9ff147fce74", + "value": "0x0000000000000000000000000000000000000000000000000000000000000075" + }, + { + "key": "0x004c3a8541d9ec369a9fd4bcdad885cc3b166da70ed6a83066924727d9ff147fce75", + "value": "0x0000000000000000000000000000000000000000000000000000000000000076" + }, + { + "key": "0x004c3a8541d9ec369a9fd4bcdad885cc3b166da70ed6a83066924727d9ff147fce76", + "value": "0x0000000000000000000000000000000000000000000000000000000000000077" + }, + { + "key": "0x004c3a8541d9ec369a9fd4bcdad885cc3b166da70ed6a83066924727d9ff147fce77", + "value": "0x0000000000000000000000000000000000000000000000000000000000000078" + }, + { + "key": "0x004c3a8541d9ec369a9fd4bcdad885cc3b166da70ed6a83066924727d9ff147fce78", + "value": "0x0000000000000000000000000000000000000000000000000000000000000079" + }, + { + "key": "0x004c3a8541d9ec369a9fd4bcdad885cc3b166da70ed6a83066924727d9ff147fce79", + "value": "0x000000000000000000000000000000000000000000000000000000000000007a" + }, + { + "key": "0x004c3a8541d9ec369a9fd4bcdad885cc3b166da70ed6a83066924727d9ff147fce7a", + "value": "0x000000000000000000000000000000000000000000000000000000000000007b" + }, + { + "key": "0x004c3a8541d9ec369a9fd4bcdad885cc3b166da70ed6a83066924727d9ff147fce7b", + "value": "0x000000000000000000000000000000000000000000000000000000000000007c" + }, + { + "key": "0x004c3a8541d9ec369a9fd4bcdad885cc3b166da70ed6a83066924727d9ff147fce7c", + "value": "0x000000000000000000000000000000000000000000000000000000000000007d" + }, + { + "key": "0x004c3a8541d9ec369a9fd4bcdad885cc3b166da70ed6a83066924727d9ff147fce7d", + "value": "0x000000000000000000000000000000000000000000000000000000000000007e" + }, + { + "key": "0x004c3a8541d9ec369a9fd4bcdad885cc3b166da70ed6a83066924727d9ff147fce7e", + "value": "0x000000000000000000000000000000000000000000000000000000000000007f" + }, + { + "key": "0x004c3a8541d9ec369a9fd4bcdad885cc3b166da70ed6a83066924727d9ff147fce7f", + "value": "0x0000000000000000000000000000000000000000000000000000000000000080" + }, + { + "key": "0x004c3a8541d9ec369a9fd4bcdad885cc3b166da70ed6a83066924727d9ff147fce80", + "value": "0x0000000000000000000000000000000000000000000000000000000000000081" + }, + { + "key": "0x004c3a8541d9ec369a9fd4bcdad885cc3b166da70ed6a83066924727d9ff147fce81", + "value": "0x0000000000000000000000000000000000000000000000000000000000000082" + }, + { + "key": "0x004c3a8541d9ec369a9fd4bcdad885cc3b166da70ed6a83066924727d9ff147fce82", + "value": "0x0000000000000000000000000000000000000000000000000000000000000083" + }, + { + "key": "0x004c3a8541d9ec369a9fd4bcdad885cc3b166da70ed6a83066924727d9ff147fce83", + "value": "0x0000000000000000000000000000000000000000000000000000000000000084" + }, + { + "key": "0x004c3a8541d9ec369a9fd4bcdad885cc3b166da70ed6a83066924727d9ff147fce84", + "value": "0x0000000000000000000000000000000000000000000000000000000000000085" + }, + { + "key": "0x004c3a8541d9ec369a9fd4bcdad885cc3b166da70ed6a83066924727d9ff147fce85", + "value": "0x0000000000000000000000000000000000000000000000000000000000000086" + }, + { + "key": "0x004c3a8541d9ec369a9fd4bcdad885cc3b166da70ed6a83066924727d9ff147fce86", + "value": "0x0000000000000000000000000000000000000000000000000000000000000087" + }, + { + "key": "0x004c3a8541d9ec369a9fd4bcdad885cc3b166da70ed6a83066924727d9ff147fce87", + "value": "0x0000000000000000000000000000000000000000000000000000000000000088" + }, + { + "key": "0x004c3a8541d9ec369a9fd4bcdad885cc3b166da70ed6a83066924727d9ff147fce88", + "value": "0x0000000000000000000000000000000000000000000000000000000000000089" + }, + { + "key": "0x004c3a8541d9ec369a9fd4bcdad885cc3b166da70ed6a83066924727d9ff147fce89", + "value": "0x000000000000000000000000000000000000000000000000000000000000008a" + }, + { + "key": "0x004c3a8541d9ec369a9fd4bcdad885cc3b166da70ed6a83066924727d9ff147fce8a", + "value": "0x000000000000000000000000000000000000000000000000000000000000008b" + }, + { + "key": "0x004c3a8541d9ec369a9fd4bcdad885cc3b166da70ed6a83066924727d9ff147fce8b", + "value": "0x000000000000000000000000000000000000000000000000000000000000008c" + }, + { + "key": "0x004c3a8541d9ec369a9fd4bcdad885cc3b166da70ed6a83066924727d9ff147fce8c", + "value": "0x000000000000000000000000000000000000000000000000000000000000008d" + }, + { + "key": "0x004c3a8541d9ec369a9fd4bcdad885cc3b166da70ed6a83066924727d9ff147fce8d", + "value": "0x000000000000000000000000000000000000000000000000000000000000008e" + }, + { + "key": "0x004c3a8541d9ec369a9fd4bcdad885cc3b166da70ed6a83066924727d9ff147fce8e", + "value": "0x000000000000000000000000000000000000000000000000000000000000008f" + }, + { + "key": "0x004c3a8541d9ec369a9fd4bcdad885cc3b166da70ed6a83066924727d9ff147fce8f", + "value": "0x0000000000000000000000000000000000000000000000000000000000000090" + }, + { + "key": "0x004c3a8541d9ec369a9fd4bcdad885cc3b166da70ed6a83066924727d9ff147fce90", + "value": "0x0000000000000000000000000000000000000000000000000000000000000091" + }, + { + "key": "0x004c3a8541d9ec369a9fd4bcdad885cc3b166da70ed6a83066924727d9ff147fce91", + "value": "0x0000000000000000000000000000000000000000000000000000000000000092" + }, + { + "key": "0x004c3a8541d9ec369a9fd4bcdad885cc3b166da70ed6a83066924727d9ff147fce92", + "value": "0x0000000000000000000000000000000000000000000000000000000000000093" + }, + { + "key": "0x004c3a8541d9ec369a9fd4bcdad885cc3b166da70ed6a83066924727d9ff147fce93", + "value": "0x0000000000000000000000000000000000000000000000000000000000000094" + }, + { + "key": "0x004c3a8541d9ec369a9fd4bcdad885cc3b166da70ed6a83066924727d9ff147fce94", + "value": "0x0000000000000000000000000000000000000000000000000000000000000095" + }, + { + "key": "0x004c3a8541d9ec369a9fd4bcdad885cc3b166da70ed6a83066924727d9ff147fce95", + "value": "0x0000000000000000000000000000000000000000000000000000000000000096" + }, + { + "key": "0x004c3a8541d9ec369a9fd4bcdad885cc3b166da70ed6a83066924727d9ff147fce96", + "value": "0x0000000000000000000000000000000000000000000000000000000000000097" + }, + { + "key": "0x004c3a8541d9ec369a9fd4bcdad885cc3b166da70ed6a83066924727d9ff147fce97", + "value": "0x0000000000000000000000000000000000000000000000000000000000000098" + }, + { + "key": "0x004c3a8541d9ec369a9fd4bcdad885cc3b166da70ed6a83066924727d9ff147fce98", + "value": "0x0000000000000000000000000000000000000000000000000000000000000099" + }, + { + "key": "0x004c3a8541d9ec369a9fd4bcdad885cc3b166da70ed6a83066924727d9ff147fce99", + "value": "0x000000000000000000000000000000000000000000000000000000000000009a" + }, + { + "key": "0x004c3a8541d9ec369a9fd4bcdad885cc3b166da70ed6a83066924727d9ff147fce9a", + "value": "0x000000000000000000000000000000000000000000000000000000000000009b" + }, + { + "key": "0x004c3a8541d9ec369a9fd4bcdad885cc3b166da70ed6a83066924727d9ff147fce9b", + "value": "0x000000000000000000000000000000000000000000000000000000000000009c" + }, + { + "key": "0x004c3a8541d9ec369a9fd4bcdad885cc3b166da70ed6a83066924727d9ff147fce9c", + "value": "0x000000000000000000000000000000000000000000000000000000000000009d" + }, + { + "key": "0x004c3a8541d9ec369a9fd4bcdad885cc3b166da70ed6a83066924727d9ff147fce9d", + "value": "0x000000000000000000000000000000000000000000000000000000000000009e" + }, + { + "key": "0x004c3a8541d9ec369a9fd4bcdad885cc3b166da70ed6a83066924727d9ff147fce9e", + "value": "0x000000000000000000000000000000000000000000000000000000000000009f" + }, + { + "key": "0x004c3a8541d9ec369a9fd4bcdad885cc3b166da70ed6a83066924727d9ff147fce9f", + "value": "0x00000000000000000000000000000000000000000000000000000000000000a0" + }, + { + "key": "0x004c3a8541d9ec369a9fd4bcdad885cc3b166da70ed6a83066924727d9ff147fcea0", + "value": "0x00000000000000000000000000000000000000000000000000000000000000a1" + }, + { + "key": "0x004c3a8541d9ec369a9fd4bcdad885cc3b166da70ed6a83066924727d9ff147fcea1", + "value": "0x00000000000000000000000000000000000000000000000000000000000000a2" + }, + { + "key": "0x004c3a8541d9ec369a9fd4bcdad885cc3b166da70ed6a83066924727d9ff147fcea2", + "value": "0x00000000000000000000000000000000000000000000000000000000000000a3" + }, + { + "key": "0x004c3a8541d9ec369a9fd4bcdad885cc3b166da70ed6a83066924727d9ff147fcea3", + "value": "0x00000000000000000000000000000000000000000000000000000000000000a4" + }, + { + "key": "0x004c3a8541d9ec369a9fd4bcdad885cc3b166da70ed6a83066924727d9ff147fcea4", + "value": "0x00000000000000000000000000000000000000000000000000000000000000a5" + }, + { + "key": "0x004c3a8541d9ec369a9fd4bcdad885cc3b166da70ed6a83066924727d9ff147fcea5", + "value": "0x00000000000000000000000000000000000000000000000000000000000000a6" + }, + { + "key": "0x004c3a8541d9ec369a9fd4bcdad885cc3b166da70ed6a83066924727d9ff147fcea6", + "value": "0x00000000000000000000000000000000000000000000000000000000000000a7" + }, + { + "key": "0x004c3a8541d9ec369a9fd4bcdad885cc3b166da70ed6a83066924727d9ff147fcea7", + "value": "0x00000000000000000000000000000000000000000000000000000000000000a8" + }, + { + "key": "0x004c3a8541d9ec369a9fd4bcdad885cc3b166da70ed6a83066924727d9ff147fcea8", + "value": "0x00000000000000000000000000000000000000000000000000000000000000a9" + }, + { + "key": "0x004c3a8541d9ec369a9fd4bcdad885cc3b166da70ed6a83066924727d9ff147fcea9", + "value": "0x00000000000000000000000000000000000000000000000000000000000000aa" + }, + { + "key": "0x004c3a8541d9ec369a9fd4bcdad885cc3b166da70ed6a83066924727d9ff147fceaa", + "value": "0x00000000000000000000000000000000000000000000000000000000000000ab" + }, + { + "key": "0x004c3a8541d9ec369a9fd4bcdad885cc3b166da70ed6a83066924727d9ff147fceab", + "value": "0x00000000000000000000000000000000000000000000000000000000000000ac" + }, + { + "key": "0x004c3a8541d9ec369a9fd4bcdad885cc3b166da70ed6a83066924727d9ff147fceac", + "value": "0x00000000000000000000000000000000000000000000000000000000000000ad" + }, + { + "key": "0x004c3a8541d9ec369a9fd4bcdad885cc3b166da70ed6a83066924727d9ff147fcead", + "value": "0x00000000000000000000000000000000000000000000000000000000000000ae" + }, + { + "key": "0x004c3a8541d9ec369a9fd4bcdad885cc3b166da70ed6a83066924727d9ff147fceae", + "value": "0x00000000000000000000000000000000000000000000000000000000000000af" + }, + { + "key": "0x004c3a8541d9ec369a9fd4bcdad885cc3b166da70ed6a83066924727d9ff147fceaf", + "value": "0x00000000000000000000000000000000000000000000000000000000000000b0" + }, + { + "key": "0x004c3a8541d9ec369a9fd4bcdad885cc3b166da70ed6a83066924727d9ff147fceb0", + "value": "0x00000000000000000000000000000000000000000000000000000000000000b1" + }, + { + "key": "0x004c3a8541d9ec369a9fd4bcdad885cc3b166da70ed6a83066924727d9ff147fceb1", + "value": "0x00000000000000000000000000000000000000000000000000000000000000b2" + }, + { + "key": "0x004c3a8541d9ec369a9fd4bcdad885cc3b166da70ed6a83066924727d9ff147fceb2", + "value": "0x00000000000000000000000000000000000000000000000000000000000000b3" + }, + { + "key": "0x004c3a8541d9ec369a9fd4bcdad885cc3b166da70ed6a83066924727d9ff147fceb3", + "value": "0x00000000000000000000000000000000000000000000000000000000000000b4" + }, + { + "key": "0x004c3a8541d9ec369a9fd4bcdad885cc3b166da70ed6a83066924727d9ff147fceb4", + "value": "0x00000000000000000000000000000000000000000000000000000000000000b5" + }, + { + "key": "0x004c3a8541d9ec369a9fd4bcdad885cc3b166da70ed6a83066924727d9ff147fceb5", + "value": "0x00000000000000000000000000000000000000000000000000000000000000b6" + }, + { + "key": "0x004c3a8541d9ec369a9fd4bcdad885cc3b166da70ed6a83066924727d9ff147fceb6", + "value": "0x00000000000000000000000000000000000000000000000000000000000000b7" + }, + { + "key": "0x004c3a8541d9ec369a9fd4bcdad885cc3b166da70ed6a83066924727d9ff147fceb7", + "value": "0x00000000000000000000000000000000000000000000000000000000000000b8" + }, + { + "key": "0x004c3a8541d9ec369a9fd4bcdad885cc3b166da70ed6a83066924727d9ff147fceb8", + "value": "0x00000000000000000000000000000000000000000000000000000000000000b9" + }, + { + "key": "0x004c3a8541d9ec369a9fd4bcdad885cc3b166da70ed6a83066924727d9ff147fceb9", + "value": "0x00000000000000000000000000000000000000000000000000000000000000ba" + }, + { + "key": "0x004c3a8541d9ec369a9fd4bcdad885cc3b166da70ed6a83066924727d9ff147fceba", + "value": "0x00000000000000000000000000000000000000000000000000000000000000bb" + }, + { + "key": "0x004c3a8541d9ec369a9fd4bcdad885cc3b166da70ed6a83066924727d9ff147fcebb", + "value": "0x00000000000000000000000000000000000000000000000000000000000000bc" + }, + { + "key": "0x004c3a8541d9ec369a9fd4bcdad885cc3b166da70ed6a83066924727d9ff147fcebc", + "value": "0x00000000000000000000000000000000000000000000000000000000000000bd" + }, + { + "key": "0x004c3a8541d9ec369a9fd4bcdad885cc3b166da70ed6a83066924727d9ff147fcebd", + "value": "0x00000000000000000000000000000000000000000000000000000000000000be" + }, + { + "key": "0x004c3a8541d9ec369a9fd4bcdad885cc3b166da70ed6a83066924727d9ff147fcebe", + "value": "0x00000000000000000000000000000000000000000000000000000000000000bf" + }, + { + "key": "0x004c3a8541d9ec369a9fd4bcdad885cc3b166da70ed6a83066924727d9ff147fcebf", + "value": "0x00000000000000000000000000000000000000000000000000000000000000c0" + }, + { + "key": "0x004c3a8541d9ec369a9fd4bcdad885cc3b166da70ed6a83066924727d9ff147fcec0", + "value": "0x00000000000000000000000000000000000000000000000000000000000000c1" + }, + { + "key": "0x004c3a8541d9ec369a9fd4bcdad885cc3b166da70ed6a83066924727d9ff147fcec1", + "value": "0x00000000000000000000000000000000000000000000000000000000000000c2" + }, + { + "key": "0x004c3a8541d9ec369a9fd4bcdad885cc3b166da70ed6a83066924727d9ff147fcec2", + "value": "0x00000000000000000000000000000000000000000000000000000000000000c3" + }, + { + "key": "0x004c3a8541d9ec369a9fd4bcdad885cc3b166da70ed6a83066924727d9ff147fcec3", + "value": "0x00000000000000000000000000000000000000000000000000000000000000c4" + }, + { + "key": "0x004c3a8541d9ec369a9fd4bcdad885cc3b166da70ed6a83066924727d9ff147fcec4", + "value": "0x00000000000000000000000000000000000000000000000000000000000000c5" + }, + { + "key": "0x004c3a8541d9ec369a9fd4bcdad885cc3b166da70ed6a83066924727d9ff147fcec5", + "value": "0x00000000000000000000000000000000000000000000000000000000000000c6" + }, + { + "key": "0x004c3a8541d9ec369a9fd4bcdad885cc3b166da70ed6a83066924727d9ff147fcec6", + "value": "0x00000000000000000000000000000000000000000000000000000000000000c7" + }, + { + "key": "0x004c3a8541d9ec369a9fd4bcdad885cc3b166da70ed6a83066924727d9ff147fcec7", + "value": "0x00000000000000000000000000000000000000000000000000000000000000c8" + }, + { + "key": "0x004c3a8541d9ec369a9fd4bcdad885cc3b166da70ed6a83066924727d9ff147fcec8", + "value": "0x00000000000000000000000000000000000000000000000000000000000000c9" + }, + { + "key": "0x004c3a8541d9ec369a9fd4bcdad885cc3b166da70ed6a83066924727d9ff147fcec9", + "value": "0x00000000000000000000000000000000000000000000000000000000000000ca" + }, + { + "key": "0x004c3a8541d9ec369a9fd4bcdad885cc3b166da70ed6a83066924727d9ff147fceca", + "value": "0x00000000000000000000000000000000000000000000000000000000000000cb" + }, + { + "key": "0x004c3a8541d9ec369a9fd4bcdad885cc3b166da70ed6a83066924727d9ff147fcecb", + "value": "0x00000000000000000000000000000000000000000000000000000000000000cc" + }, + { + "key": "0x004c3a8541d9ec369a9fd4bcdad885cc3b166da70ed6a83066924727d9ff147fcecc", + "value": "0x00000000000000000000000000000000000000000000000000000000000000cd" + }, + { + "key": "0x004c3a8541d9ec369a9fd4bcdad885cc3b166da70ed6a83066924727d9ff147fcecd", + "value": "0x00000000000000000000000000000000000000000000000000000000000000ce" + }, + { + "key": "0x004c3a8541d9ec369a9fd4bcdad885cc3b166da70ed6a83066924727d9ff147fcece", + "value": "0x00000000000000000000000000000000000000000000000000000000000000cf" + }, + { + "key": "0x004c3a8541d9ec369a9fd4bcdad885cc3b166da70ed6a83066924727d9ff147fcecf", + "value": "0x00000000000000000000000000000000000000000000000000000000000000d0" + }, + { + "key": "0x004c3a8541d9ec369a9fd4bcdad885cc3b166da70ed6a83066924727d9ff147fced0", + "value": "0x00000000000000000000000000000000000000000000000000000000000000d1" + }, + { + "key": "0x004c3a8541d9ec369a9fd4bcdad885cc3b166da70ed6a83066924727d9ff147fced1", + "value": "0x00000000000000000000000000000000000000000000000000000000000000d2" + }, + { + "key": "0x004c3a8541d9ec369a9fd4bcdad885cc3b166da70ed6a83066924727d9ff147fced2", + "value": "0x00000000000000000000000000000000000000000000000000000000000000d3" + }, + { + "key": "0x004c3a8541d9ec369a9fd4bcdad885cc3b166da70ed6a83066924727d9ff147fced3", + "value": "0x00000000000000000000000000000000000000000000000000000000000000d4" + }, + { + "key": "0x004c3a8541d9ec369a9fd4bcdad885cc3b166da70ed6a83066924727d9ff147fced4", + "value": "0x00000000000000000000000000000000000000000000000000000000000000d5" + }, + { + "key": "0x004c3a8541d9ec369a9fd4bcdad885cc3b166da70ed6a83066924727d9ff147fced5", + "value": "0x00000000000000000000000000000000000000000000000000000000000000d6" + }, + { + "key": "0x004c3a8541d9ec369a9fd4bcdad885cc3b166da70ed6a83066924727d9ff147fced6", + "value": "0x00000000000000000000000000000000000000000000000000000000000000d7" + }, + { + "key": "0x004c3a8541d9ec369a9fd4bcdad885cc3b166da70ed6a83066924727d9ff147fced7", + "value": "0x00000000000000000000000000000000000000000000000000000000000000d8" + }, + { + "key": "0x004c3a8541d9ec369a9fd4bcdad885cc3b166da70ed6a83066924727d9ff147fced8", + "value": "0x00000000000000000000000000000000000000000000000000000000000000d9" + }, + { + "key": "0x004c3a8541d9ec369a9fd4bcdad885cc3b166da70ed6a83066924727d9ff147fced9", + "value": "0x00000000000000000000000000000000000000000000000000000000000000da" + }, + { + "key": "0x004c3a8541d9ec369a9fd4bcdad885cc3b166da70ed6a83066924727d9ff147fceda", + "value": "0x00000000000000000000000000000000000000000000000000000000000000db" + }, + { + "key": "0x004c3a8541d9ec369a9fd4bcdad885cc3b166da70ed6a83066924727d9ff147fcedb", + "value": "0x00000000000000000000000000000000000000000000000000000000000000dc" + }, + { + "key": "0x004c3a8541d9ec369a9fd4bcdad885cc3b166da70ed6a83066924727d9ff147fcedc", + "value": "0x00000000000000000000000000000000000000000000000000000000000000dd" + }, + { + "key": "0x004c3a8541d9ec369a9fd4bcdad885cc3b166da70ed6a83066924727d9ff147fcedd", + "value": "0x00000000000000000000000000000000000000000000000000000000000000de" + }, + { + "key": "0x004c3a8541d9ec369a9fd4bcdad885cc3b166da70ed6a83066924727d9ff147fcede", + "value": "0x00000000000000000000000000000000000000000000000000000000000000df" + }, + { + "key": "0x004c3a8541d9ec369a9fd4bcdad885cc3b166da70ed6a83066924727d9ff147fcedf", + "value": "0x00000000000000000000000000000000000000000000000000000000000000e0" + }, + { + "key": "0x004c3a8541d9ec369a9fd4bcdad885cc3b166da70ed6a83066924727d9ff147fcee0", + "value": "0x00000000000000000000000000000000000000000000000000000000000000e1" + }, + { + "key": "0x004c3a8541d9ec369a9fd4bcdad885cc3b166da70ed6a83066924727d9ff147fcee1", + "value": "0x00000000000000000000000000000000000000000000000000000000000000e2" + }, + { + "key": "0x004c3a8541d9ec369a9fd4bcdad885cc3b166da70ed6a83066924727d9ff147fcee2", + "value": "0x00000000000000000000000000000000000000000000000000000000000000e3" + }, + { + "key": "0x004c3a8541d9ec369a9fd4bcdad885cc3b166da70ed6a83066924727d9ff147fcee3", + "value": "0x00000000000000000000000000000000000000000000000000000000000000e4" + }, + { + "key": "0x004c3a8541d9ec369a9fd4bcdad885cc3b166da70ed6a83066924727d9ff147fcee4", + "value": "0x00000000000000000000000000000000000000000000000000000000000000e5" + }, + { + "key": "0x004c3a8541d9ec369a9fd4bcdad885cc3b166da70ed6a83066924727d9ff147fcee5", + "value": "0x00000000000000000000000000000000000000000000000000000000000000e6" + }, + { + "key": "0x004c3a8541d9ec369a9fd4bcdad885cc3b166da70ed6a83066924727d9ff147fcee6", + "value": "0x00000000000000000000000000000000000000000000000000000000000000e7" + }, + { + "key": "0x004c3a8541d9ec369a9fd4bcdad885cc3b166da70ed6a83066924727d9ff147fcee7", + "value": "0x00000000000000000000000000000000000000000000000000000000000000e8" + }, + { + "key": "0x004c3a8541d9ec369a9fd4bcdad885cc3b166da70ed6a83066924727d9ff147fcee8", + "value": "0x00000000000000000000000000000000000000000000000000000000000000e9" + }, + { + "key": "0x004c3a8541d9ec369a9fd4bcdad885cc3b166da70ed6a83066924727d9ff147fcee9", + "value": "0x00000000000000000000000000000000000000000000000000000000000000ea" + }, + { + "key": "0x004c3a8541d9ec369a9fd4bcdad885cc3b166da70ed6a83066924727d9ff147fceea", + "value": "0x00000000000000000000000000000000000000000000000000000000000000eb" + }, + { + "key": "0x004c3a8541d9ec369a9fd4bcdad885cc3b166da70ed6a83066924727d9ff147fceeb", + "value": "0x00000000000000000000000000000000000000000000000000000000000000ec" + }, + { + "key": "0x004c3a8541d9ec369a9fd4bcdad885cc3b166da70ed6a83066924727d9ff147fceec", + "value": "0x00000000000000000000000000000000000000000000000000000000000000ed" + }, + { + "key": "0x004c3a8541d9ec369a9fd4bcdad885cc3b166da70ed6a83066924727d9ff147fceed", + "value": "0x00000000000000000000000000000000000000000000000000000000000000ee" + }, + { + "key": "0x004c3a8541d9ec369a9fd4bcdad885cc3b166da70ed6a83066924727d9ff147fceee", + "value": "0x00000000000000000000000000000000000000000000000000000000000000ef" + }, + { + "key": "0x004c3a8541d9ec369a9fd4bcdad885cc3b166da70ed6a83066924727d9ff147fceef", + "value": "0x00000000000000000000000000000000000000000000000000000000000000f0" + }, + { + "key": "0x004c3a8541d9ec369a9fd4bcdad885cc3b166da70ed6a83066924727d9ff147fcef0", + "value": "0x00000000000000000000000000000000000000000000000000000000000000f1" + }, + { + "key": "0x004c3a8541d9ec369a9fd4bcdad885cc3b166da70ed6a83066924727d9ff147fcef1", + "value": "0x00000000000000000000000000000000000000000000000000000000000000f2" + }, + { + "key": "0x004c3a8541d9ec369a9fd4bcdad885cc3b166da70ed6a83066924727d9ff147fcef2", + "value": "0x00000000000000000000000000000000000000000000000000000000000000f3" + }, + { + "key": "0x004c3a8541d9ec369a9fd4bcdad885cc3b166da70ed6a83066924727d9ff147fcef3", + "value": "0x00000000000000000000000000000000000000000000000000000000000000f4" + }, + { + "key": "0x004c3a8541d9ec369a9fd4bcdad885cc3b166da70ed6a83066924727d9ff147fcef4", + "value": "0x00000000000000000000000000000000000000000000000000000000000000f5" + }, + { + "key": "0x004c3a8541d9ec369a9fd4bcdad885cc3b166da70ed6a83066924727d9ff147fcef5", + "value": "0x00000000000000000000000000000000000000000000000000000000000000f6" + }, + { + "key": "0x004c3a8541d9ec369a9fd4bcdad885cc3b166da70ed6a83066924727d9ff147fcef6", + "value": "0x00000000000000000000000000000000000000000000000000000000000000f7" + }, + { + "key": "0x004c3a8541d9ec369a9fd4bcdad885cc3b166da70ed6a83066924727d9ff147fcef7", + "value": "0x00000000000000000000000000000000000000000000000000000000000000f8" + }, + { + "key": "0x004c3a8541d9ec369a9fd4bcdad885cc3b166da70ed6a83066924727d9ff147fcef8", + "value": "0x00000000000000000000000000000000000000000000000000000000000000f9" + }, + { + "key": "0x004c3a8541d9ec369a9fd4bcdad885cc3b166da70ed6a83066924727d9ff147fcef9", + "value": "0x00000000000000000000000000000000000000000000000000000000000000fa" + }, + { + "key": "0x004c3a8541d9ec369a9fd4bcdad885cc3b166da70ed6a83066924727d9ff147fcefa", + "value": "0x00000000000000000000000000000000000000000000000000000000000000fb" + }, + { + "key": "0x004c3a8541d9ec369a9fd4bcdad885cc3b166da70ed6a83066924727d9ff147fcefb", + "value": "0x00000000000000000000000000000000000000000000000000000000000000fc" + }, + { + "key": "0x004c3a8541d9ec369a9fd4bcdad885cc3b166da70ed6a83066924727d9ff147fcefc", + "value": "0x00000000000000000000000000000000000000000000000000000000000000fd" + }, + { + "key": "0x004c3a8541d9ec369a9fd4bcdad885cc3b166da70ed6a83066924727d9ff147fcefd", + "value": "0x00000000000000000000000000000000000000000000000000000000000000fe" + }, + { + "key": "0x004c3a8541d9ec369a9fd4bcdad885cc3b166da70ed6a83066924727d9ff147fcefe", + "value": "0x00000000000000000000000000000000000000000000000000000000000000ff" + }, + { + "key": "0x004c3a8541d9ec369a9fd4bcdad885cc3b166da70ed6a83066924727d9ff147fceff", + "value": "0x0000000000000000000000000000000000000000000000000000000000000100" + } + ], + "root": "0x090ef773023b99997f3c8c72e3fa1fbd9b0b62833ffb662e457b60530b358721" + } + ], + "sequence_vectors": [ + { + "seed": 8297, + "ops": [ + { + "op": "set", + "key": "0x0015c4813bb5b1499e0a7e0d46daab8e10c70940c008c566d5c68557b1523d1f2999", + "value": "0x00000000000000000000000000000000000000000000000000000000362952bd" + }, + { + "op": "set", + "key": "0xff2eb90959edcd0b6a8a7922b8b8d37cd9664001c217b4c8e6d2df4b8ede329a7191c71427661efcc7f991940e68aae22bede52be877c5e2b9b4e0c22051e2c61706", + "value": "0x000000000000000000000000000000000000000000000000000000005912e971" + }, + { + "op": "delete", + "key": "0xff2eb90959edcd0b6a8a7922b8b8d37cd9664001c217b4c8e6d2df4b8ede329a7191c71427661efcc7f991940e68aae22bede52be877c5e2b9b4e0c22051e2c61706" + }, + { + "op": "set", + "key": "0xff9c847c439d6431d36ddeb326cb553da2e967778a24afbd1d23a62412186faf016d21858cec8b9c323663e513206217295d0693bb4eb042fd7e5e863612ec45fe5c", + "value": "0x000000000000000000000000000000000000000000000000000000009e92aea6" + }, + { + "op": "set", + "key": "0x00998f08d7054351e5fa3ccadda32ab8d6c0c86a825dd360d57c0fe6b46612297c21", + "value": "0x0000000000000000000000000000000000000000000000000000000037f3974d" + }, + { + "op": "set", + "key": "0x00e61994d31ceae39cae5feb02303eb90e0d4ae90f5b0755c3c26adc81f360ec8516", + "value": "0x0000000000000000000000000000000000000000000000000000000091546180" + }, + { + "op": "set", + "key": "0xffd1afd6427b13f2ce7beb82a2cd6bbfc58eac8b2d46bc87bfbc78fbf440e18a635bd91a1bb3fea09cd07c1f6f0b5b0928ec465c3d5409c3dc4d9f3b384fd276b0df", + "value": "0x00000000000000000000000000000000000000000000000000000000d560d2d0" + }, + { + "op": "set", + "key": "0x002eb90959edcd0b6a8a7922b8b8d37cd9664001c217b4c8e6d2df4b8ede329a717c", + "value": "0x00000000000000000000000000000000000000000000000000000000b7e649ff" + }, + { + "op": "delete", + "key": "0xff9c847c439d6431d36ddeb326cb553da2e967778a24afbd1d23a62412186faf016d21858cec8b9c323663e513206217295d0693bb4eb042fd7e5e863612ec45fe5c" + }, + { + "op": "set", + "key": "0xffe60d91d1d11837912311124089c6b73c9d726c99f4df797eb48c2b7fec6e5e2cedb6b011186812600d2c38950eb3aa1e9e39d11456e96ac38d3ec9cb37e4816783", + "value": "0x0000000000000000000000000000000000000000000000000000000015716296" + }, + { + "op": "set", + "key": "0x0078a2812688114caf4452a5611322977e4b1000e59ef408d4c8e2c90028370bcbf3", + "value": "0x00000000000000000000000000000000000000000000000000000000d566656c" + }, + { + "op": "set", + "key": "0x000861030791246737d706c0d0017de01a26278bbe4ce8a134f5e0cd6f7eb69bcf6b", + "value": "0x00000000000000000000000000000000000000000000000000000000c6f30fd3" + }, + { + "op": "set", + "key": "0x0068e867f5f899fc01d641a00cea2c42d58f3cc061797135198a85301d1e2903062a", + "value": "0x00000000000000000000000000000000000000000000000000000000308a8072" + }, + { + "op": "set", + "key": "0xff46aeb3243f905d4e0eac7f60ee8962848c87e79dfbd019ece14fe45bd720ea6bc7d37b238b059b3f39fb34f03f189fc8c596ad3bc45f7231d1a8e723510074014d", + "value": "0x00000000000000000000000000000000000000000000000000000000fd6c27f9" + }, + { + "op": "set", + "key": "0x00a43fb2d7beb12b34786a837467f62b7d6c5dab4ab7eecc89c0b67db76a682927c1", + "value": "0x000000000000000000000000000000000000000000000000000000000b5daa14" + }, + { + "op": "set", + "key": "0x00543907253acb230f5dc96bbafd8ea0f37dd63b30e8a4a3675bb007673a31fbc5d7", + "value": "0x00000000000000000000000000000000000000000000000000000000ef86c437" + }, + { + "op": "set", + "key": "0xff0fb23a930b05e3bdfbdd0e6694fa9df9c5059ceabc60618bc40931ddb379f1f64c1111b166c73d061477381d77db7818b678c5a5595df51af307fccfda674238c3", + "value": "0x000000000000000000000000000000000000000000000000000000008687ece2" + }, + { + "op": "set", + "key": "0x0046aeb3243f905d4e0eac7f60ee8962848c87e79dfbd019ece14fe45bd720ea6b69", + "value": "0x000000000000000000000000000000000000000000000000000000008d81d15d" + }, + { + "op": "set", + "key": "0xff998f08d7054351e5fa3ccadda32ab8d6c0c86a825dd360d57c0fe6b46612297c216fd3a73b07a45409a19b5e6d545779e55b6900fddc4443dcb06e3d97e25e1908", + "value": "0x000000000000000000000000000000000000000000000000000000008f91e546" + }, + { + "op": "set", + "key": "0x002cebacb183e2f51a3b6cb9f7fe2bd11c399bf1bdb8ce95ed457d3e46190197a0fd", + "value": "0x00000000000000000000000000000000000000000000000000000000b638fa76" + } + ], + "roots_after": [ + "0xcf6ca0dad8d69c45120da214b9052e614649a640e52560d0f47487073325e93d", + "0xa7247b2d9c8c6e49a18add897d235a5e72c292a5144008676972983ebd5e624e", + "0xcf6ca0dad8d69c45120da214b9052e614649a640e52560d0f47487073325e93d", + "0x9b659884c441cf90291fe8e2336c65d34cb0da60709da8a6165486c1a00829e4", + "0x1b5adc55d232b1c9b93c8bae8c1e736a2e596efabd3cc466de195d38ecbe9746", + "0xa604a44382e79cbca1ae6140d150d84980510447609e2a2356dce852861b3217", + "0x67994f2a47248a4677f1691706ec0ad76d479894fb7015f06bda6d4af4d46a55", + "0x99b2ce17b9b779b3f63f68d218182a59b928c80c1ac8d8bcb716cd3c202a20b3", + "0xda86ba5028d29db7d6006f7cf340f6af5203506cbcc76e72ba27302933a4943f", + "0xaa845405281cd62dc569bf03f8a6c1bd35609cd2c1860db56a9478254f8cb25e", + "0x1708bbd96a838a1f0816667f74a2d2a1a01b8c5499bcb533be4a45cf684b8bef", + "0xf8850e79c7adbebb051eb0ea11e3ca9866c11f660cb2b7d2c3892beb20246f26", + "0xc6c221bffc3947d15ad0e994e150fa0e654897bde5039e88bcda287cf24dc827", + "0xf1036ba9ebb2780554ab2596581122a999fe301b33446a3bf13c623dd8796f96", + "0x61cd8daf48607b812e063019741d2ae0d77bf9193b2b5f5996b756a22a488b1f", + "0x73aaf9a0a5b4f179bacf1f1c2fbcb630163797d02cdc5115977d26e8ea9f48ff", + "0xff83d59bd08841abbfc5cf10620dd334ec2fbb9bd0e579aec30905b4e40ccab9", + "0x44b55ce7592e1ba9aa3ea9c465cdadee3f7e09abccbec49aec6290eab112d0a4", + "0xe1d9ef033ebdf35275483848038f858fa43063805c1cc1b5ea96654745cc3b94", + "0xcff0d627850d4d4ea368e66ff2349c0caf454129e387d23df3c8dcf71534768f" + ] + }, + { + "seed": 11832, + "ops": [ + { + "op": "set", + "key": "0xff1df77729bb76b139342cfc86aff011590bf6bb810f47e69a6babac30af96f9f5ffa1e869c4566231d09357daec90e71aa93821a7347b29175ef4a1bc02a0b38120", + "value": "0x0000000000000000000000000000000000000000000000000000000079b57838" + }, + { + "op": "set", + "key": "0xff3cbfd03e3f083b0987f841c4372031faf655a63472518a3613e2e30ecb542b51ef2c19e3ed459de7a67968b7004609b54e8e34fef08d7f9ed3c6eedcc980488255", + "value": "0x00000000000000000000000000000000000000000000000000000000449c8b5d" + }, + { + "op": "set", + "key": "0x007b1eefd69ea014244937aaeaea4e8d4d296c04edd75ae47897e5612ef5a1a25289", + "value": "0x00000000000000000000000000000000000000000000000000000000b5b13d29" + }, + { + "op": "set", + "key": "0xffa43fb2d7beb12b34786a837467f62b7d6c5dab4ab7eecc89c0b67db76a6829272094bf023796a3d4bdc16643cddf5395ab048ba346a4f50f1cc64da78ad7dd5266", + "value": "0x000000000000000000000000000000000000000000000000000000008cc69019" + }, + { + "op": "set", + "key": "0xff5963897639cd11d686b199366a1249d76bc89e0f7161c9eca48d9a199435952d4a28b184a4e022d38844c7967da6fbdf879ffafc6cfabf88c929ec9c9ff90040d1", + "value": "0x00000000000000000000000000000000000000000000000000000000af9bbd7d" + }, + { + "op": "set", + "key": "0x00e61994d31ceae39cae5feb02303eb90e0d4ae90f5b0755c3c26adc81f360ec85bf", + "value": "0x000000000000000000000000000000000000000000000000000000005dde837c" + }, + { + "op": "delete", + "key": "0xffa43fb2d7beb12b34786a837467f62b7d6c5dab4ab7eecc89c0b67db76a6829272094bf023796a3d4bdc16643cddf5395ab048ba346a4f50f1cc64da78ad7dd5266" + }, + { + "op": "delete", + "key": "0xff1df77729bb76b139342cfc86aff011590bf6bb810f47e69a6babac30af96f9f5ffa1e869c4566231d09357daec90e71aa93821a7347b29175ef4a1bc02a0b38120" + }, + { + "op": "delete", + "key": "0xff3cbfd03e3f083b0987f841c4372031faf655a63472518a3613e2e30ecb542b51ef2c19e3ed459de7a67968b7004609b54e8e34fef08d7f9ed3c6eedcc980488255" + }, + { + "op": "delete", + "key": "0x00e61994d31ceae39cae5feb02303eb90e0d4ae90f5b0755c3c26adc81f360ec85bf" + }, + { + "op": "set", + "key": "0xff0fb23a930b05e3bdfbdd0e6694fa9df9c5059ceabc60618bc40931ddb379f1f6065b90a89bb317ff71b1f933493a08b55f152abe9f5ae5b58400664d141c512119", + "value": "0x000000000000000000000000000000000000000000000000000000000a082d85" + }, + { + "op": "set", + "key": "0xff998f08d7054351e5fa3ccadda32ab8d6c0c86a825dd360d57c0fe6b46612297c725184c1ce99f155151a19aa14a427e1a784778319e2b193ac66bd6374b328cdff", + "value": "0x00000000000000000000000000000000000000000000000000000000a3ea3eb4" + }, + { + "op": "set", + "key": "0xff78a2812688114caf4452a5611322977e4b1000e59ef408d4c8e2c90028370bcb71e2e06bd4204550cd67cae560e42f8afcdc75e454a14f00ed2a8ec7c885869e40", + "value": "0x000000000000000000000000000000000000000000000000000000007435a9e4" + }, + { + "op": "delete", + "key": "0xff0fb23a930b05e3bdfbdd0e6694fa9df9c5059ceabc60618bc40931ddb379f1f6065b90a89bb317ff71b1f933493a08b55f152abe9f5ae5b58400664d141c512119" + }, + { + "op": "set", + "key": "0x000fb23a930b05e3bdfbdd0e6694fa9df9c5059ceabc60618bc40931ddb379f1f668", + "value": "0x000000000000000000000000000000000000000000000000000000000275abc8" + }, + { + "op": "delete", + "key": "0xff5963897639cd11d686b199366a1249d76bc89e0f7161c9eca48d9a199435952d4a28b184a4e022d38844c7967da6fbdf879ffafc6cfabf88c929ec9c9ff90040d1" + }, + { + "op": "set", + "key": "0x007293ce391f5cd652216ca8ea31069a06a82f393028dfc8786aa5efd7be734b2cbe", + "value": "0x0000000000000000000000000000000000000000000000000000000094f87f55" + }, + { + "op": "set", + "key": "0xff4c3a8541d9ec369a9fd4bcdad885cc3b166da70ed6a83066924727d9ff147fce963472d01342d91a0a268bdfee2ecf01a6d30f9021151faf2e2e6719120bf40f0a", + "value": "0x00000000000000000000000000000000000000000000000000000000fdac9fff" + }, + { + "op": "set", + "key": "0xff1df77729bb76b139342cfc86aff011590bf6bb810f47e69a6babac30af96f9f5ffa1e869c4566231d09357daec90e71aa93821a7347b29175ef4a1bc02a0b38181", + "value": "0x00000000000000000000000000000000000000000000000000000000e4d876b8" + }, + { + "op": "set", + "key": "0x00e2f41518226671332da4adddab1fdcb9903c7b895c7943b669067fe96b0baacfd5", + "value": "0x0000000000000000000000000000000000000000000000000000000019be8821" + } + ], + "roots_after": [ + "0x5cdba549646b7612059692efa58d33f90059dfa9df06433f4438510c1b32e049", + "0x6ae6cd7fe0f7f9dfbff03d1d6b88c55f8c9f247584c70427a2a53612cdf202e6", + "0xcbe8e97d34728b819654a46096d4fe13abcdeed3e31cf088be21de915051259a", + "0x155969c29e159077b8a9997325ac9962fee077d64c9467f1ccd68618c9abbcdb", + "0x2439229c4decda037eb0a692540821cdd6c77df8ba51d478291d905e5de4f5e4", + "0xc252ede6498166ca351dc0346fdd1ee5df6694e3acf7f5714f2e80db687ccc79", + "0x7fcfd5973d297879b65209bb9cf1ca74c30ee717e13350d2c0447675165cb80b", + "0xdbb4e48dda8ae1f9287abccd62637085bb171f5897ad42e5019500209e89202d", + "0x14712da3d0ce63185cae268f29bf2593295476a83a8e95b3b4daf01a83274d6f", + "0x51098829074dc7618bdca7c079f0ac0577e0a08a0df5c92dcd104bf6cad43ea6", + "0x2643869090c9ca916157631cac68996dd01de67c66639eeb443f9b93ebe7df1c", + "0x9793adae9d275d569398dc631e09da40464320a0dc0c644d36d79f97d34b7288", + "0xef2bd1092bc3404855dd94e9452f97cebeb425d8027e7a56dc1c5fc0e93c38f6", + "0x85170f6f2bed9f200e8835f9d2c9bd13cb9267d77e834dcd73d43d1f897cc346", + "0x71c46fa0b2804adc2012db0d399c477429071b47f13fc31ddc125bc599e8fafb", + "0xeba1b27a56ff60fd186ede1c5ee445c59b2733f527c6ccc30b765b6917491720", + "0x2d0dcb278bc7892ad37afe2c523b73d18a60e76b1dc41d04469896c3b355d490", + "0x5aadbab832910092a62a49e78c5d76b10ffe26f3452a0bd47fd049e36eb31d63", + "0x53b8d1dab39520c882f5666b092ab47f59f4f3084c9471eefbb15d6a0c58ad5d", + "0x52ff67405b74b808b1649b64764de113df17e70ea5aebf5489bc1db7aad52422" + ] + }, + { + "seed": 3102, + "ops": [ + { + "op": "set", + "key": "0xff2cebacb183e2f51a3b6cb9f7fe2bd11c399bf1bdb8ce95ed457d3e46190197a02a6a55293936c1e1b6894fbb3b2873c993bd580ec835889bf0f211410e368fc199", + "value": "0x000000000000000000000000000000000000000000000000000000002e422f9a" + }, + { + "op": "delete", + "key": "0xff2cebacb183e2f51a3b6cb9f7fe2bd11c399bf1bdb8ce95ed457d3e46190197a02a6a55293936c1e1b6894fbb3b2873c993bd580ec835889bf0f211410e368fc199" + }, + { + "op": "set", + "key": "0xff29e4e6bbd168fe5b46b7d34c34b628f95d54c4e75f9335a7e96282d1173b07dfd0f9925799d37553e0f34b2d7a49a135fc34a90b6fb99bfe70f2e43c28e758854d", + "value": "0x000000000000000000000000000000000000000000000000000000002ecaa733" + }, + { + "op": "set", + "key": "0x00d1afd6427b13f2ce7beb82a2cd6bbfc58eac8b2d46bc87bfbc78fbf440e18a635f", + "value": "0x0000000000000000000000000000000000000000000000000000000076fe3750" + }, + { + "op": "set", + "key": "0x000861030791246737d706c0d0017de01a26278bbe4ce8a134f5e0cd6f7eb69bcff9", + "value": "0x0000000000000000000000000000000000000000000000000000000035fd5ae2" + }, + { + "op": "set", + "key": "0xff998f08d7054351e5fa3ccadda32ab8d6c0c86a825dd360d57c0fe6b46612297cc290a8b5ac7a453c9cec5724a8a85ad4002fb6dc9c73e86aedfa05133317c0402e", + "value": "0x00000000000000000000000000000000000000000000000000000000be9e2390" + }, + { + "op": "set", + "key": "0x005963897639cd11d686b199366a1249d76bc89e0f7161c9eca48d9a199435952d33", + "value": "0x00000000000000000000000000000000000000000000000000000000b3e90b26" + }, + { + "op": "set", + "key": "0xffe57860ba0adcdd1294caf74da8b990a80a8a6861bc01b30dfe07450d25bab38f20483a0da833c6de846c07a8214bee6ee90bd1da5d9b9553f74784602fdd83dfb9", + "value": "0x0000000000000000000000000000000000000000000000000000000051dcd3af" + }, + { + "op": "set", + "key": "0x00e6b6d8d98be54d4c00cee378bedf964f755b5257bf1eabf52cf39aa987686fa743", + "value": "0x0000000000000000000000000000000000000000000000000000000083a3dad3" + }, + { + "op": "set", + "key": "0xff0fb23a930b05e3bdfbdd0e6694fa9df9c5059ceabc60618bc40931ddb379f1f6065b90a89bb317ff71b1f933493a08b55f152abe9f5ae5b58400664d141c512161", + "value": "0x00000000000000000000000000000000000000000000000000000000939e31a5" + }, + { + "op": "set", + "key": "0xff2cebacb183e2f51a3b6cb9f7fe2bd11c399bf1bdb8ce95ed457d3e46190197a018eb952465e3dd252f35c719bb6d7d14f62bf363e0afa74100d3046f81539e08b9", + "value": "0x000000000000000000000000000000000000000000000000000000002da16542" + }, + { + "op": "set", + "key": "0xff4b83f061efb2f4708114e46b5ef50a255528c28c80bbe5fc6ad88e892bd4c4ea37491f84eb77d352d7d8fca0484b856ceab7236e2f773d157b3fbd6d0586de7f48", + "value": "0x000000000000000000000000000000000000000000000000000000003b1510f6" + }, + { + "op": "set", + "key": "0x00c901cfbb39e551189323bea0af6cf9e81008c90dce65cbc2ff4672e263d7bb87f3", + "value": "0x0000000000000000000000000000000000000000000000000000000087d0f3c4" + }, + { + "op": "set", + "key": "0x0055a6923f7414ba482ae8a64b407f386a0bb0491b742bd4b22b5b4b44fca84e848f", + "value": "0x000000000000000000000000000000000000000000000000000000008cfbc63e" + }, + { + "op": "set", + "key": "0xff7b1eefd69ea014244937aaeaea4e8d4d296c04edd75ae47897e5612ef5a1a25254d51e5b726c0f3cfd1b03f07f25907b18767b99fef9eb727475858c458e1c7087", + "value": "0x00000000000000000000000000000000000000000000000000000000af70ae1b" + }, + { + "op": "set", + "key": "0xff29e4e6bbd168fe5b46b7d34c34b628f95d54c4e75f9335a7e96282d1173b07df754b0f807e1dc46306d4cb12e85e18d008678fa80cd9e6fa118c34e179bd412a87", + "value": "0x00000000000000000000000000000000000000000000000000000000d15c3b16" + }, + { + "op": "set", + "key": "0xff29e4e6bbd168fe5b46b7d34c34b628f95d54c4e75f9335a7e96282d1173b07df754b0f807e1dc46306d4cb12e85e18d008678fa80cd9e6fa118c34e179bd412a57", + "value": "0x000000000000000000000000000000000000000000000000000000003e5f6e17" + }, + { + "op": "set", + "key": "0x0055bbfb5b05f80543bd662a7b7a74980a2992612798087fa24e1fc86b8e8508e4b6", + "value": "0x000000000000000000000000000000000000000000000000000000002a25f39d" + }, + { + "op": "delete", + "key": "0xff0fb23a930b05e3bdfbdd0e6694fa9df9c5059ceabc60618bc40931ddb379f1f6065b90a89bb317ff71b1f933493a08b55f152abe9f5ae5b58400664d141c512161" + }, + { + "op": "set", + "key": "0x00e61994d31ceae39cae5feb02303eb90e0d4ae90f5b0755c3c26adc81f360ec85de", + "value": "0x00000000000000000000000000000000000000000000000000000000fd3f724c" + } + ], + "roots_after": [ + "0xdd16f2b8e1c012d8ed988df1cd9e31fc06bceee33304ec90fdee4c9e0d6f2522", + "0x0000000000000000000000000000000000000000000000000000000000000000", + "0xe7c887160ebfbf178105a04157a5b67d669ac6899fbf632526152fc5cb82cd80", + "0xd77cc71cfc52b7a272f47bb93cf6e3b5ae4a2d7fc991e4e2b76586018620570b", + "0xfb180e479837451381e4ebbf7a21be6937dbab9b830eeb798cf1854b48bc82ad", + "0x663475566fe0b6afa6fb71c73802d98b37948ccc16237229d8722a66b31c48f0", + "0xe26dac98c3f044695b7bda1e7bab4e9e0422d41b367b3fb873d50734d15f8de8", + "0x90263ca13708230b2ae2d6413b76995a7c177ed239310a9628fe4aac1e2d51fe", + "0xd7893e542d99ae8c18bd7a495190a58e2e1e25252d4fa9378ef2694db0e751ee", + "0x4df5042f26d44caf9d655bda9bb46284498628761474d302392daff333e5b610", + "0x1113ff21db5505e243f390d5df0fc11549690813ff7a25e8bc60b5209d39ed72", + "0xe14995d380c1552d2901cacc440b51fed58732d5eb6b6adb202e891f3f53b6d6", + "0x5d3fd815f8282caf95aab8105212fb98b6f5516beb9fba2ed4f4ee502b85ed74", + "0x09b380d8286220693a4a9e4b85e074ea94cf0dadbccacd05d6f8dc5470438087", + "0x957d9940952d058f2523c52603f8cc27537ee0e40b8c452d9691e28ff0f9d782", + "0x7cc8b1cbe418de642947381e3fd9251730dff6165bb7d48de4388cce893d8aea", + "0x5ea579760671e854abc2adfcb037bd6f1470c26e5f4ba6e9392d05e1af830b6d", + "0xb4281adb36825d1f7dd6b7f0536a81ad9950fb727124828f0a94a79c32885052", + "0xa90bdbe39f8833c02f914c2ca37a5d5b91be83ba6a4721923b1ffe8899c40d9e", + "0x8c3fff51ed5749c6147c9bb35b5ac0f20f0f0ff92604b0d7cd0f83bcaecf52b4" + ] + }, + { + "seed": 90210, + "ops": [ + { + "op": "set", + "key": "0xffd1afd6427b13f2ce7beb82a2cd6bbfc58eac8b2d46bc87bfbc78fbf440e18a63aeb0aca39c8f6ba440835a8e371b8b2719dec5b24a57f1da0e7519c3cd74beff52", + "value": "0x00000000000000000000000000000000000000000000000000000000cec06895" + }, + { + "op": "set", + "key": "0x00e61994d31ceae39cae5feb02303eb90e0d4ae90f5b0755c3c26adc81f360ec8587", + "value": "0x0000000000000000000000000000000000000000000000000000000026a125de" + }, + { + "op": "delete", + "key": "0x00e61994d31ceae39cae5feb02303eb90e0d4ae90f5b0755c3c26adc81f360ec8587" + }, + { + "op": "delete", + "key": "0xffd1afd6427b13f2ce7beb82a2cd6bbfc58eac8b2d46bc87bfbc78fbf440e18a63aeb0aca39c8f6ba440835a8e371b8b2719dec5b24a57f1da0e7519c3cd74beff52" + }, + { + "op": "set", + "key": "0x00c901cfbb39e551189323bea0af6cf9e81008c90dce65cbc2ff4672e263d7bb871a", + "value": "0x0000000000000000000000000000000000000000000000000000000038f9aacc" + }, + { + "op": "set", + "key": "0x005963897639cd11d686b199366a1249d76bc89e0f7161c9eca48d9a199435952d0f", + "value": "0x0000000000000000000000000000000000000000000000000000000053b3bca6" + }, + { + "op": "set", + "key": "0x0068e867f5f899fc01d641a00cea2c42d58f3cc061797135198a85301d1e290306c3", + "value": "0x0000000000000000000000000000000000000000000000000000000058e273d9" + }, + { + "op": "set", + "key": "0x00e61994d31ceae39cae5feb02303eb90e0d4ae90f5b0755c3c26adc81f360ec85b3", + "value": "0x000000000000000000000000000000000000000000000000000000008debe84f" + }, + { + "op": "delete", + "key": "0x00c901cfbb39e551189323bea0af6cf9e81008c90dce65cbc2ff4672e263d7bb871a" + }, + { + "op": "set", + "key": "0x0055bbfb5b05f80543bd662a7b7a74980a2992612798087fa24e1fc86b8e8508e4b2", + "value": "0x0000000000000000000000000000000000000000000000000000000052fbeee9" + }, + { + "op": "set", + "key": "0xff68e867f5f899fc01d641a00cea2c42d58f3cc061797135198a85301d1e290306efdc84373704529ffcbc3d4ed318de0ec2cdf5f61dd800c143f44a741af2f838a0", + "value": "0x0000000000000000000000000000000000000000000000000000000012acb6e5" + }, + { + "op": "set", + "key": "0xffe60d91d1d11837912311124089c6b73c9d726c99f4df797eb48c2b7fec6e5e2c3b9034b692f2597d8c9c9ad6921ecd112d2f69cc50a6e065034e2ace540e8928ab", + "value": "0x0000000000000000000000000000000000000000000000000000000088a67fe9" + }, + { + "op": "set", + "key": "0xffe60d91d1d11837912311124089c6b73c9d726c99f4df797eb48c2b7fec6e5e2c9c921b846417391ff7e9525580b53876b30e3f6cf563e5849525f6fa88bf230f1a", + "value": "0x0000000000000000000000000000000000000000000000000000000075b67af1" + }, + { + "op": "set", + "key": "0xff78a2812688114caf4452a5611322977e4b1000e59ef408d4c8e2c90028370bcb279b454b0f4a6026943322996543a3cbb6c41d7c467067d9bc010242feef3c2c4b", + "value": "0x00000000000000000000000000000000000000000000000000000000fd6d065d" + }, + { + "op": "set", + "key": "0x00bded9122e6171a5dad9ce2aaebd933abf4718e9bf12448cf113064c9fd8e6ad0a4", + "value": "0x000000000000000000000000000000000000000000000000000000001c92d573" + }, + { + "op": "delete", + "key": "0xffe60d91d1d11837912311124089c6b73c9d726c99f4df797eb48c2b7fec6e5e2c9c921b846417391ff7e9525580b53876b30e3f6cf563e5849525f6fa88bf230f1a" + }, + { + "op": "set", + "key": "0x003cbfd03e3f083b0987f841c4372031faf655a63472518a3613e2e30ecb542b514a", + "value": "0x00000000000000000000000000000000000000000000000000000000617ad32c" + }, + { + "op": "set", + "key": "0xffec64b2de1fc1d53b795f49a1c84ca4172bc3292472c3e244e4f8c225fa786f56b7bce92ed9ae70f9ca2c10d21fe857f18cd2b64d54c70193d8a0dd772967c2580d", + "value": "0x00000000000000000000000000000000000000000000000000000000565e29f9" + }, + { + "op": "delete", + "key": "0xff78a2812688114caf4452a5611322977e4b1000e59ef408d4c8e2c90028370bcb279b454b0f4a6026943322996543a3cbb6c41d7c467067d9bc010242feef3c2c4b" + }, + { + "op": "set", + "key": "0xffd1afd6427b13f2ce7beb82a2cd6bbfc58eac8b2d46bc87bfbc78fbf440e18a635bd91a1bb3fea09cd07c1f6f0b5b0928ec465c3d5409c3dc4d9f3b384fd276b0e3", + "value": "0x00000000000000000000000000000000000000000000000000000000f03eb650" + } + ], + "roots_after": [ + "0xe4a4cc81bad5ec263e1470b6766f0e48c44cb6de9edfafe42ce6a742e7c0de35", + "0x628d4752f5a37f73d798da0fd84216c8c96d376e81ca8c011a1ef0e5b293037d", + "0xe4a4cc81bad5ec263e1470b6766f0e48c44cb6de9edfafe42ce6a742e7c0de35", + "0x0000000000000000000000000000000000000000000000000000000000000000", + "0x36d0ac2cbaa8daac84e3abb0711753ab2803d39fb7def6e34b95e607b5177c92", + "0xcdd2d3edb22a14874aa1b8adc01bad8e80dd4f6f0c603788bd28705981409ba7", + "0x0438e8ad308f977fe36353cde64fef7f1210dd3820b5275c3fffe712e342f125", + "0xe8514dc5fbf6794122aad2a058ad95df0835731e49061dfc5238fe8673b7cb57", + "0xfa1bed5d0c8187d176cdabee2a0b6d96ec72791326c1d9d00555e2752fa4ae06", + "0x388f561f8cd9b274f5c9cd3154ab49c7586c9cae7b28c0b7471be9be98cba3be", + "0x36fca86e1497e245753b5662e45cac10ce68278494adef2b1219b534d63411e2", + "0x98ebe09efe3ff35ee2f8b20732a3f30408119a25e620107c33f503cd39e55143", + "0xcd53e141a34d3c96464d66bc027b4cc0731739f1e645aced12c51072463f8fc4", + "0xbba122989fb7ec24899f02a1aa37fcf967d84f6c5207d843ac9a754b1eaa7ba3", + "0xb219a14dd80c8cc69152edbcec566fe773892d5e615e310b063e99def0f6aa79", + "0x3531deb75570f3a160ba9b471a10be04750c59ed54984186a46d4521e5058145", + "0xd3e835c31b1c00c8f864d7da97f035c6147452c3762a7f69b174751b815737a8", + "0xd74280c2db61bd2bd9d739ae4c38053ce6192f683907b95c6c0fed0ddf6b1b66", + "0x24a2abd1e5f37cbb1e104406be5ee8b6c4989c27bb6ec117ae4e18c0b66dfc9c", + "0x3a7c4099580d9b766538946c88846c95d7822fd4d6121a20e57e1791026600f5" + ] + }, + { + "seed": 20260727, + "ops": [ + { + "op": "set", + "key": "0x002eb90959edcd0b6a8a7922b8b8d37cd9664001c217b4c8e6d2df4b8ede329a7170", + "value": "0x0000000000000000000000000000000000000000000000000000000068535e9a" + }, + { + "op": "set", + "key": "0xff3cbfd03e3f083b0987f841c4372031faf655a63472518a3613e2e30ecb542b5111589c57f4d8ad55d2c4ece4c6b63961c45be7479bfd08d04fab2d26d0acb7a091", + "value": "0x0000000000000000000000000000000000000000000000000000000056756dfe" + }, + { + "op": "set", + "key": "0x007b1eefd69ea014244937aaeaea4e8d4d296c04edd75ae47897e5612ef5a1a2521a", + "value": "0x000000000000000000000000000000000000000000000000000000005959a793" + }, + { + "op": "delete", + "key": "0x002eb90959edcd0b6a8a7922b8b8d37cd9664001c217b4c8e6d2df4b8ede329a7170" + }, + { + "op": "delete", + "key": "0x007b1eefd69ea014244937aaeaea4e8d4d296c04edd75ae47897e5612ef5a1a2521a" + }, + { + "op": "set", + "key": "0x0055a6923f7414ba482ae8a64b407f386a0bb0491b742bd4b22b5b4b44fca84e8468", + "value": "0x000000000000000000000000000000000000000000000000000000003c2b7202" + }, + { + "op": "delete", + "key": "0xff3cbfd03e3f083b0987f841c4372031faf655a63472518a3613e2e30ecb542b5111589c57f4d8ad55d2c4ece4c6b63961c45be7479bfd08d04fab2d26d0acb7a091" + }, + { + "op": "delete", + "key": "0x0055a6923f7414ba482ae8a64b407f386a0bb0491b742bd4b22b5b4b44fca84e8468" + }, + { + "op": "set", + "key": "0xffe60d91d1d11837912311124089c6b73c9d726c99f4df797eb48c2b7fec6e5e2c6066e8c722a574a900a59551ed956d32ca2b36e48771ed8b373da6b223bbbe9c8f", + "value": "0x000000000000000000000000000000000000000000000000000000009bb7df73" + }, + { + "op": "set", + "key": "0x009c847c439d6431d36ddeb326cb553da2e967778a24afbd1d23a62412186faf0130", + "value": "0x00000000000000000000000000000000000000000000000000000000def11b80" + }, + { + "op": "set", + "key": "0x007b1eefd69ea014244937aaeaea4e8d4d296c04edd75ae47897e5612ef5a1a2524d", + "value": "0x00000000000000000000000000000000000000000000000000000000f05708e7" + }, + { + "op": "set", + "key": "0x00998f08d7054351e5fa3ccadda32ab8d6c0c86a825dd360d57c0fe6b46612297cba", + "value": "0x00000000000000000000000000000000000000000000000000000000c433224b" + }, + { + "op": "set", + "key": "0xff5ac76a8f23131e4ca988c8afcc313130cc95e521d2691635fc26196d63e66a1f33b1e44125772918b9d44a12766eb0dad773e863d120780173c394789aa668e925", + "value": "0x00000000000000000000000000000000000000000000000000000000abbc594e" + }, + { + "op": "set", + "key": "0xff998f08d7054351e5fa3ccadda32ab8d6c0c86a825dd360d57c0fe6b46612297c36c8cce2938e709d43205827ddebecc780d7e631bb1acb24046af6e5b23e445aa2", + "value": "0x00000000000000000000000000000000000000000000000000000000219ea23a" + }, + { + "op": "set", + "key": "0x00d1d141c1bed8bb4f809292e9340a0209cd167484a24a5ca993fcac4b97086d89af", + "value": "0x00000000000000000000000000000000000000000000000000000000e015951e" + }, + { + "op": "delete", + "key": "0x00d1d141c1bed8bb4f809292e9340a0209cd167484a24a5ca993fcac4b97086d89af" + }, + { + "op": "set", + "key": "0xffc28c21982a56a6cecf595bfeb78297c7432abfb487cc75c09db2856a118f441df2a84c02b0cbaafcabc4cbb5f988d916f3287032f07e3f8bf51588b4311ed04c72", + "value": "0x00000000000000000000000000000000000000000000000000000000973ab40a" + }, + { + "op": "set", + "key": "0x00e2f41518226671332da4adddab1fdcb9903c7b895c7943b669067fe96b0baacfdb", + "value": "0x000000000000000000000000000000000000000000000000000000000c8a8e64" + }, + { + "op": "delete", + "key": "0x007b1eefd69ea014244937aaeaea4e8d4d296c04edd75ae47897e5612ef5a1a2524d" + }, + { + "op": "set", + "key": "0xff15c4813bb5b1499e0a7e0d46daab8e10c70940c008c566d5c68557b1523d1f29cac055e3c06fef0ce191be15524a246c11d1103158d4f8b49d31c629e552005366", + "value": "0x000000000000000000000000000000000000000000000000000000001a5e6148" + } + ], + "roots_after": [ + "0x8a7b32f0179f8591d8d29bbc5ce478d2d1cca5ceebddb5c6880e6682fd1a7458", + "0x63e670849972d58347a67cb04d30b369145ea59ac06fd411c231b46c05b534c8", + "0xeedfc71a50202763dc37c74f994b52783a5e3ac19ff7c3d818c9bd2c95b562f7", + "0x1cfbe1c8b87bb9ecafd506ee003ddaeef688eaa24a564cd7147270130425709e", + "0xf8daed5ef73e34987561ea33d980ed96600f48700aa75ea5706c74f79babdb3d", + "0xcf348977fb91b672c061404e5bc6f063a444d19478b4aba7450531d48ae5b8f8", + "0xfa4116de24b5dce1e1ebb9736c78a00ec1eb957aaee7e9f9af3b74f68e43a73e", + "0x0000000000000000000000000000000000000000000000000000000000000000", + "0x5f2fc0f08e034ab42e6b4b8eec8038618707b8f2ae9722e14a6048d1935a438c", + "0xbc0812ac0dbf193721b4e90a5f23d702399df82d1e1da571c9e2388155b5409f", + "0xe105f7395ae6845ec524e9dcdf9a33600d7eece3b9dacaf7fee43deca2ad0957", + "0x5f30927f1062975cf9ad72910c99386ffc609b4dac5e1633a0a7a051ff221ca1", + "0x6f8469d0d9572b492c6c170c43f734aea5714da7d2e47ac9c10a01938439a1cb", + "0x7a75f5784c851040e436de70bafabe4abddc0baab0db46583879fdc1a7bcbd01", + "0xaa03aca82638a0c50233cb98152c122b0c21c4efb2a47035a690b5a65a2e54ed", + "0x7a75f5784c851040e436de70bafabe4abddc0baab0db46583879fdc1a7bcbd01", + "0xf6d984c0ed4b562ad15789d556f759f72b44627fb197780e88d36f450c60cd4e", + "0x52ae36bcfdb7559ffaf993e3d23b7727ada4f1403f12a9f16ba0324610852a8e", + "0x7d48f61dc2656ca3fc52efbb16bf80b4806cf43aaef56397c9d526d739ab2ad4", + "0x867a92986423254fd9f362a682695d3eb986bd5106cadabbd72dd0faf70c0cf9" + ] + } + ], + "embedding_vectors": { + "address": "0xaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa", + "basic_data_key": "0x00d9ae2d236f8713a5bf808cda488167a56cc97e4b83006f42b1c06c0c3f053bbf00", + "code_hash_key": "0x00d9ae2d236f8713a5bf808cda488167a56cc97e4b83006f42b1c06c0c3f053bbf01", + "slots": [ + { + "slot": 0, + "key": "0x00d9ae2d236f8713a5bf808cda488167a56cc97e4b83006f42b1c06c0c3f053bbf40" + }, + { + "slot": 5, + "key": "0x00d9ae2d236f8713a5bf808cda488167a56cc97e4b83006f42b1c06c0c3f053bbf45" + }, + { + "slot": 63, + "key": "0x00d9ae2d236f8713a5bf808cda488167a56cc97e4b83006f42b1c06c0c3f053bbf7f" + }, + { + "slot": 64, + "key": "0xffd9ae2d236f8713a5bf808cda488167a56cc97e4b83006f42b1c06c0c3f053bbf181495ef75f8d2005a4a5bd3c5139cf6b5cdd1a02574832253cc9932687b332a40" + }, + { + "slot": 255, + "key": "0xffd9ae2d236f8713a5bf808cda488167a56cc97e4b83006f42b1c06c0c3f053bbf181495ef75f8d2005a4a5bd3c5139cf6b5cdd1a02574832253cc9932687b332aff" + }, + { + "slot": 256, + "key": "0xffd9ae2d236f8713a5bf808cda488167a56cc97e4b83006f42b1c06c0c3f053bbfe717382bfeaa9f4c014431fb7bbc3c6946dd510a82d49c925d9df7735c26b16f00" + }, + { + "slot": 1000, + "key": "0xffd9ae2d236f8713a5bf808cda488167a56cc97e4b83006f42b1c06c0c3f053bbf2b1a6c18962d993b9da5ed300ce5696bdcad6e09f08b9c0735fa749d417650f9e8" + }, + { + "slot": 57896044618658097711785492504343953926634992332820282019728792003956564819968, + "key": "0xffd9ae2d236f8713a5bf808cda488167a56cc97e4b83006f42b1c06c0c3f053bbf23d37150867b80a99fea951c62d43f974e7d2f53089c5f5683b4fc1d4387286c00" + } + ], + "chunks": [ + { + "chunk": 0, + "key": "0x00d9ae2d236f8713a5bf808cda488167a56cc97e4b83006f42b1c06c0c3f053bbf80" + }, + { + "chunk": 5, + "key": "0x00d9ae2d236f8713a5bf808cda488167a56cc97e4b83006f42b1c06c0c3f053bbf85" + }, + { + "chunk": 127, + "key": "0x00d9ae2d236f8713a5bf808cda488167a56cc97e4b83006f42b1c06c0c3f053bbfff" + }, + { + "chunk": 128, + "key": "0x01465f911ccfe7707c51a877fc621cffa8e700a5828cf92df27b142de919c0f6c800" + }, + { + "chunk": 300, + "key": "0x01465f911ccfe7707c51a877fc621cffa8e700a5828cf92df27b142de919c0f6c8ac" + }, + { + "chunk": 383, + "key": "0x01465f911ccfe7707c51a877fc621cffa8e700a5828cf92df27b142de919c0f6c8ff" + }, + { + "chunk": 384, + "key": "0x0171ad9e407a8d200ab2858bcf828d9016a0b4bc54e6e39dd3e31847b6220a63ce00" + } + ] + }, + "basic_data_vectors": [ + { + "code_size": 0, + "nonce": 0, + "balance": "0", + "value": "0x0000000000000000000000000000000000000000000000000000000000000000" + }, + { + "code_size": 0, + "nonce": 1, + "balance": "1000000000000000000", + "value": "0x0000000000000000000000000000000100000000000000000de0b6b3a7640000" + }, + { + "code_size": 287454020, + "nonce": 6153737369425722316, + "balance": "1512366075204170929049582354406559215", + "value": "0x00000000112233445566778899aabbcc0123456789abcdef0123456789abcdef" + }, + { + "code_size": 24576, + "nonce": 1, + "balance": "1", + "value": "0x0000000000006000000000000000000100000000000000000000000000000001" + } + ], + "chunkify_vectors": [ + { + "name": "empty", + "code": "0x", + "chunks": [] + }, + { + "name": "short", + "code": "0x6001", + "chunks": [ + "0x0060010000000000000000000000000000000000000000000000000000000000" + ] + }, + { + "name": "push_boundary", + "code": "0x6060606060606060606060606060606060606060606060606060606060606060606060606060606060606060606060606060606060606060606060606060", + "chunks": [ + "0x0060606060606060606060606060606060606060606060606060606060606060", + "0x0160606060606060606060606060606060606060606060606060606060606060" + ] + }, + { + "name": "push32_tail", + "code": "0x7feeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeee", + "chunks": [ + "0x007feeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeee", + "0x02eeeeeeeeeeeeeeeeeeee000000000000000000000000000000000000000000" + ] + }, + { + "name": "zeros62", + "code": "0x0000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000", + "chunks": [ + "0x0000000000000000000000000000000000000000000000000000000000000000", + "0x0000000000000000000000000000000000000000000000000000000000000000" + ] + } + ] +} diff --git a/execution/stagedsync/exec3.go b/execution/stagedsync/exec3.go index acbe4b74b87..5fad8d5b526 100644 --- a/execution/stagedsync/exec3.go +++ b/execution/stagedsync/exec3.go @@ -108,11 +108,6 @@ func restoreTxNum(ctx context.Context, cfg *ExecuteBlockCfg, applyTx kv.Tx, curr return inputTxNum, maxTxNum, offsetFromBlockBeginning, blockNum, nil } -// deferCommitmentUpdates reports whether Process() may leave branch updates as a -// pending update flushed at the block boundary instead of applying them inline. -// Deferring cuts re-org validation overhead; the parallel apply path also needs -// Flush() to carry the pending update across sync cycles. The bin trie has no -// deferred-update path and refuses the request, so it stays on the inline path. // executeInParallel picks the executor. The parallel executor's normalized write // set produces a different bin-trie root than the serial one for the same block, // so the bin variant stays on the serial executor until that is resolved. @@ -123,6 +118,11 @@ func executeInParallel(variant commitment.TrieVariant, exec3Parallel, experiment return exec3Parallel || experimentalBAL } +// deferCommitmentUpdates reports whether Process() may leave branch updates as a +// pending update flushed at the block boundary instead of applying them inline. +// Deferring cuts re-org validation overhead; the parallel apply path also needs +// Flush() to carry the pending update across sync cycles. The bin trie has no +// deferred-update path and refuses the request, so it stays on the inline path. func deferCommitmentUpdates(variant commitment.TrieVariant, isForkValidation, parallel, isApplyingBlocks bool) bool { if variant == commitment.VariantBinPatriciaTrie { return false @@ -775,7 +775,13 @@ func (te *txExecutor) executeBlocks(ctx context.Context, startBlockNum uint64, m // dbg.CheckHeaderStateRoot switches the check off for a chain whose headers // this node cannot reproduce. func headerRootMismatch(computed, expected []byte) bool { - return dbg.CheckHeaderStateRoot && !bytes.Equal(computed, expected) + if !dbg.CheckHeaderStateRoot { + // Every execution entry point runs through here, so this is what reaches the + // integration and test runners too, not just node startup. + dbg.WarnHeaderStateRootCheckDisabled() + return false + } + return !bytes.Equal(computed, expected) } func handleIncorrectRootHashError(blockNumber uint64, blockHash common.Hash, applyTx kv.TemporalRwTx, cfg ExecuteBlockCfg, s *StageState, logger log.Logger, u Unwinder) error { diff --git a/execution/tests/testutil/block_test_util.go b/execution/tests/testutil/block_test_util.go index ab6aae9e521..c696f5328d7 100644 --- a/execution/tests/testutil/block_test_util.go +++ b/execution/tests/testutil/block_test_util.go @@ -26,6 +26,7 @@ import ( "errors" "fmt" "math/big" + "sync" "testing" "github.com/holiman/uint256" @@ -221,6 +222,89 @@ func (bt *BlockTest) Run(t *testing.T) error { return err } +// The commitment variant and its hash are datadir properties resolved +// process-wide, not per-tester options, so one run covers BinaryTree fixtures +// or Merkle-Patricia ones and never both. The latch is what keeps the block +// runner's concurrent workers off a racing write to the globals, and what turns +// a mixed corpus into an error instead of fixtures silently re-rooted under the +// wrong engine. +var commitmentVariant struct { + sync.Mutex + holders int + bin bool + prevBin bool + prevHash string + prevSuite string +} + +func commitmentVariantName(bin bool) string { + if bin { + return "binary" + } + return "Merkle-Patricia" +} + +// selectCommitmentVariant commits the process to one commitment trie. Under go +// test the choice is handed back once the last holder is done, so a later test +// reads the process it expects; the CLI passes a nil tb and keeps it for the run. +func selectCommitmentVariant(tb testing.TB, bin bool) error { + release, err := acquireCommitmentVariant(bin) + if err != nil { + return err + } + if tb != nil { + tb.Cleanup(release) + } + return nil +} + +// acquireCommitmentVariant latches the variant and returns the release its +// caller owes. Fixture files run as parallel subtests, so holders overlap: the +// first applies the selection and only the last hands it back. +func acquireCommitmentVariant(bin bool) (func(), error) { + commitmentVariant.Lock() + defer commitmentVariant.Unlock() + + if commitmentVariant.holders > 0 { + if commitmentVariant.bin != bin { + return nil, fmt.Errorf("the commitment trie is selected process-wide: this run started under the %s trie and cannot also cover %s fixtures", + commitmentVariantName(commitmentVariant.bin), commitmentVariantName(bin)) + } + commitmentVariant.holders++ + return releaseCommitmentVariant, nil + } + + prevBin, prevHash, prevSuite := statecfg.ExperimentalBinCommitment, statecfg.BinCommitmentHash, commitment.PBinHashSuiteName() + if bin { + if err := commitment.SetPBinHashSuite(commitment.PBinHashBlake3); err != nil { + return nil, err + } + // Setting the statecfg field is what makes the settings resolver persist + // blake3 and re-apply it; calling SetPBinHashSuite alone would be undone by + // the resolver's keccak default. + statecfg.ExperimentalBinCommitment = true + statecfg.BinCommitmentHash = commitment.PBinHashBlake3 + } + commitmentVariant.bin = bin + commitmentVariant.prevBin, commitmentVariant.prevHash, commitmentVariant.prevSuite = prevBin, prevHash, prevSuite + commitmentVariant.holders = 1 + return releaseCommitmentVariant, nil +} + +func releaseCommitmentVariant() { + commitmentVariant.Lock() + defer commitmentVariant.Unlock() + + commitmentVariant.holders-- + if commitmentVariant.holders > 0 { + return + } + statecfg.ExperimentalBinCommitment, statecfg.BinCommitmentHash = commitmentVariant.prevBin, commitmentVariant.prevHash + if err := commitment.SetPBinHashSuite(commitmentVariant.prevSuite); err != nil { + panic(err) + } +} + // newTester builds the ExecModuleTester for this block test. tb may be nil for // CLI usage, in which case the caller owns the tester's lifecycle and MUST Close it. func (bt *BlockTest) newTester(tb testing.TB) (*execmoduletester.ExecModuleTester, error) { @@ -228,17 +312,8 @@ func (bt *BlockTest) newTester(tb testing.TB) (*execmoduletester.ExecModuleTeste if !ok { return nil, testforks.UnsupportedForkError{Name: bt.json.Network} } - if bt.json.Network == testforks.BinaryTree { - // The commitment variant and its hash are datadir properties resolved - // process-wide, not per-tester options, so they are set here rather than - // passed through mOpts. Setting the statecfg field is what makes the - // settings resolver persist blake3 and re-apply it; calling - // SetPBinHashSuite alone would be undone by the resolver's keccak default. - statecfg.ExperimentalBinCommitment = true - statecfg.BinCommitmentHash = commitment.PBinHashBlake3 - if err := commitment.SetPBinHashSuite(commitment.PBinHashBlake3); err != nil { - return nil, err - } + if err := selectCommitmentVariant(tb, bt.json.Network == testforks.BinaryTree); err != nil { + return nil, err } engine := rulesconfig.CreateRulesEngineBareBones(context.Background(), config, log.New()) mOpts := []execmoduletester.Option{ diff --git a/execution/tests/testutil/block_test_util_test.go b/execution/tests/testutil/block_test_util_test.go new file mode 100644 index 00000000000..f0963a776b0 --- /dev/null +++ b/execution/tests/testutil/block_test_util_test.go @@ -0,0 +1,68 @@ +// Copyright 2026 The Erigon Authors +// This file is part of Erigon. +// +// Erigon is free software: you can redistribute it and/or modify +// it under the terms of the GNU Lesser General Public License as published by +// the Free Software Foundation, either version 3 of the License, or +// (at your option) any later version. +// +// Erigon is distributed in the hope that it will be useful, +// but WITHOUT ANY WARRANTY; without even the implied warranty of +// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +// GNU Lesser General Public License for more details. +// +// You should have received a copy of the GNU Lesser General Public License +// along with Erigon. If not, see . + +package testutil + +import ( + "testing" + + "github.com/stretchr/testify/require" + + "github.com/erigontech/erigon/db/state/statecfg" + "github.com/erigontech/erigon/execution/commitment" +) + +// TestSelectCommitmentVariantLatches: the trie the fixtures run under is a +// process-global choice, so a mixed run has to fail rather than re-root one +// network's fixtures under the other's engine. No t.Parallel here — the test +// writes the same globals a concurrent one would read. +func TestSelectCommitmentVariantLatches(t *testing.T) { + t.Run("bin refuses to share the process", func(t *testing.T) { + require.NoError(t, selectCommitmentVariant(t, true)) + require.True(t, statecfg.ExperimentalBinCommitment) + require.Equal(t, commitment.PBinHashBlake3, commitment.PBinHashSuiteName()) + + require.NoError(t, selectCommitmentVariant(t, true), "the same variant twice is the ordinary case") + require.Error(t, selectCommitmentVariant(t, false)) + }) + + require.False(t, statecfg.ExperimentalBinCommitment, "the subtest has to hand the process back") + require.Equal(t, commitment.PBinHashKeccak, commitment.PBinHashSuiteName()) + + t.Run("hex refuses to share the process", func(t *testing.T) { + require.NoError(t, selectCommitmentVariant(t, false)) + require.False(t, statecfg.ExperimentalBinCommitment) + require.Error(t, selectCommitmentVariant(t, true)) + }) +} + +// TestSelectCommitmentVariantHoldsForOverlappingUsers: fixture files run as +// parallel subtests, so two of them hold the same variant at once and the one +// that finishes first must not hand the process back under the other. +func TestSelectCommitmentVariantHoldsForOverlappingUsers(t *testing.T) { + first, err := acquireCommitmentVariant(true) + require.NoError(t, err) + second, err := acquireCommitmentVariant(true) + require.NoError(t, err) + + first() + require.True(t, statecfg.ExperimentalBinCommitment, "a variant is still held, so it cannot be handed back") + require.Equal(t, commitment.PBinHashBlake3, commitment.PBinHashSuiteName()) + + second() + require.False(t, statecfg.ExperimentalBinCommitment, "the last holder has to hand the process back") + require.Equal(t, commitment.PBinHashKeccak, commitment.PBinHashSuiteName()) +} diff --git a/node/eth/backend.go b/node/eth/backend.go index 058adcf41e1..20662857385 100644 --- a/node/eth/backend.go +++ b/node/eth/backend.go @@ -337,9 +337,7 @@ func New(ctx context.Context, stack *node.Node, config *ethconfig.Config, logger return nil, err } - if !dbg.CheckHeaderStateRoot { - logger.Warn("HEADER STATE-ROOT CHECK IS DISABLED (CHECK_HEADER_STATE_ROOT=false): nothing cross-checks execution results against headers; a wrong chain will look healthy") - } + dbg.WarnHeaderStateRootCheckDisabled() ctx, ctxCancel := context.WithCancel(context.Background()) @@ -371,9 +369,15 @@ func New(ctx context.Context, stack *node.Node, config *ethconfig.Config, logger return nil, err } - // After the resolve: a flagless restart of a bin datadir adopts the variant there. + // After the resolve: a flagless restart of a bin datadir adopts the variant + // and the hash recorded there, so both are read back rather than assumed. if statecfg.ExperimentalBinCommitment { - logger.Warn("EXPERIMENTAL BINARY COMMITMENT TRIE IS ENABLED: roots follow EIP-8297 over Keccak-256 and agree with no other client; witness, eth_getProof, eth_simulateV1, receipt regeneration, deferred commitment updates, collapse tracing and trie traces are unsupported and refuse rather than degrade") + peers := "matches the execution-specs reference" + if commitment.PBinHashSuiteName() == commitment.PBinHashKeccak { + peers = "agrees with no other client" + } + logger.Warn("EXPERIMENTAL BINARY COMMITMENT TRIE IS ENABLED: roots follow EIP-8297 and "+peers+"; eth_getProof, eth_getWitness, eth_simulateV1, receipt regeneration, deferred commitment updates, collapse tracing and trie traces are unsupported and refuse rather than degrade; debug_executionWitness is supported and verifies each witness by stateless re-execution before returning it", + "hash", commitment.PBinHashSuiteName()) } var chainConfig *chain.Config diff --git a/rpc/jsonrpc/debug_execution_witness.go b/rpc/jsonrpc/debug_execution_witness.go index 28062f6e7fe..43ec717f967 100644 --- a/rpc/jsonrpc/debug_execution_witness.go +++ b/rpc/jsonrpc/debug_execution_witness.go @@ -56,6 +56,11 @@ type RecordingState struct { // createdCodeHashes holds code hashes written in-block; a pre-state read of a hash // already created in-block is redundant in the witness (the verifier replays the create). createdCodeHashes map[common.Hash]struct{} + // PreStateHasStorage holds the accounts whose pre-state storage the EIP-7610 + // CREATE-collision check found non-empty. The binary trie commits no + // per-account storage root, so a verifier can only re-derive that answer from + // a proof of the account's storage zone (see accessedState.pbinStorageProbes). + PreStateHasStorage map[common.Address]struct{} //HashedCodes map[common.Hash][]byte // set of code hashes seen during execution, used to avoid duplicate code entries in result.Codes @@ -90,6 +95,7 @@ func NewRecordingState(inner state.StateReader) *RecordingState { AccessedCode: make(map[common.Address][]byte), PreStateCode: make(map[common.Address][]byte), createdCodeHashes: make(map[common.Hash]struct{}), + PreStateHasStorage: make(map[common.Address]struct{}), accountOverlay: make(map[common.Address]*accounts.Account), storageOverlay: make(map[common.Address]map[common.Hash]uint256.Int), codeOverlay: make(map[common.Address][]byte), @@ -237,6 +243,9 @@ func (s *RecordingState) HasStorage(address accounts.Address) (bool, error) { return false, nil } has, err := s.inner.HasStorage(address) + if err == nil && has { + s.PreStateHasStorage[addr] = struct{}{} + } if s.tracing(addr) { fmt.Printf("[TRACE] HasStorage %s -> inner %v (err=%v)\n", addr.Hex(), has, err) } @@ -585,22 +594,37 @@ const ( witnessModeCanonical ) -// resolveWitnessMode resolves the witness mode from the request param; absent, defaults to legacy. -// An explicit param value other than "legacy"/"canonical" is rejected. -func resolveWitnessMode(modeParam *string) (witnessMode, error) { +// errWitnessCanonicalHexOnly rejects an explicit canonical request under the binary +// trie. The legacy/canonical split is an MPT distinction (empty nodes, minimum +// siblings); bin has a single witness form, which the legacy default names. +var errWitnessCanonicalHexOnly = errors.New("canonical witness mode is hex-only: the binary trie has a single witness form") + +// resolveWitnessMode resolves the witness mode from the request param; absent or empty, +// it defaults to legacy. An explicit param value other than "legacy"/"canonical" is +// rejected, as is canonical under the binary trie. +func resolveWitnessMode(modeParam *string, binTrie bool) (witnessMode, error) { if modeParam == nil { return witnessModeLegacy, nil } switch *modeParam { - case "legacy": + case "", "legacy": return witnessModeLegacy, nil case "canonical": + if binTrie { + return witnessModeLegacy, errWitnessCanonicalHexOnly + } return witnessModeCanonical, nil default: return witnessModeLegacy, fmt.Errorf("invalid witness mode %q: must be \"legacy\" or \"canonical\"", *modeParam) } } +// binCommitmentTrie reports whether the datadir runs the EIP-8297 binary commitment +// trie, which skips the witness pipeline's MPT-shaped phases. +func binCommitmentTrie() bool { + return execctx.PickTrieVariant() == commitment.VariantBinPatriciaTrie +} + // buildAccessedState re-executes a block against a recording historical-state reader // and rolls the recorded accesses into an accessedState. The returned accessedBlockHashes // are the block numbers the BLOCKHASH opcode resolved during execution. @@ -714,7 +738,7 @@ func (api *BaseAPI) buildAccessedState( // It executes a block using a historical state reader, records all state accesses // (accounts, storage, code), and builds merkle proofs for the accessed keys. func (api *DebugAPIImpl) ExecutionWitness(ctx context.Context, blockNrOrHash rpc.BlockNumberOrHash, mode *string) (*ExecutionWitnessResult, error) { - resolvedMode, err := resolveWitnessMode(mode) + resolvedMode, err := resolveWitnessMode(mode, binCommitmentTrie()) if err != nil { return nil, err } @@ -871,6 +895,7 @@ func (api *DebugAPIImpl) buildWitnessResultHeadCapture(ctx context.Context, comm // hc redirects only the commitment-domain reads to a pinned parent snapshot (head-capture); // nil is the durable-history path. func (api *DebugAPIImpl) buildWitnessResult(ctx context.Context, tx kv.TemporalTx, hc *headCaptureSource, info *witnessBlockInfo, mode witnessMode) (*ExecutionWitnessResult, error) { + binTrie := binCommitmentTrie() blockNum := info.BlockNum block := info.Block firstTxNumInBlock := info.FirstTxNumInBlock @@ -898,9 +923,10 @@ func (api *DebugAPIImpl) buildWitnessResult(ctx context.Context, tx kv.TemporalT // Build merkle proofs for all accessed accounts // Use the proof infrastructure from the commitment context. - // Witness generation requires the sequential HexPatriciaHashed (Witness() - // type-asserts it); the parallel trie cannot serve it. - domains, err := execctx.NewSharedDomains(ctx, tx, log.New(), execctx.WithoutDeferredBranchUpdates(), execctx.WithHexCommitmentOnly()) + // Witness capture is served by the sequential HexPatriciaHashed and by + // PBinPatriciaHashed, so bin is allowed through; only the parallel trie + // cannot serve it and is demoted. + domains, err := execctx.NewSharedDomains(ctx, tx, log.New(), execctx.WithoutDeferredBranchUpdates(), execctx.WithoutParallelCommitment()) if err != nil { return nil, err } @@ -936,14 +962,14 @@ func (api *DebugAPIImpl) buildWitnessResult(ctx context.Context, tx kv.TemporalT siblingPaths, err := detectCollapseSiblings(ctx, tx, hc, domains, sdCtx, firstTxNumInBlock, endTxNum, blockNum, parentNum, - block.Root(), accessed, mode) + block.Root(), accessed, mode, binTrie) if err != nil { return nil, err } // Materialize exclusion-proof branches for strict sparse-trie verifiers in legacy/default // mode; canonical mode stays minimal to match the reference witness. - nodes, err := buildWitnessTrie(ctx, tx, hc, domains, sdCtx, firstTxNumInBlock, expectedParentRoot, siblingPaths, accessed, mode != witnessModeCanonical) + nodes, err := buildWitnessTrie(ctx, tx, hc, domains, sdCtx, firstTxNumInBlock, expectedParentRoot, siblingPaths, accessed, mode != witnessModeCanonical, binTrie) if err != nil { return nil, err } @@ -960,21 +986,11 @@ func (api *DebugAPIImpl) buildWitnessResult(ctx context.Context, tx kv.TemporalT if !ok { return nil, fmt.Errorf("engine does not support full rules.Engine interface") } - if err := api.verifyWitnessStateless(ctx, tx, result, block, fullEngine); err != nil { + if err := api.verifyWitnessStateless(ctx, tx, result, block, fullEngine, binTrie, expectedParentRoot); err != nil { return nil, fmt.Errorf("%w: %w", errWitnessVerifyFailed, err) } - // legacy carries the empty storage-trie node (0x80) once when some account has an - // empty storage root (EmptyRoot appears only as an account-leaf storage-root field); - // canonical omits it. Added after stateless verification, which rejects the bare node. - if mode == witnessModeLegacy { - for _, node := range result.State { - if bytes.Contains(node, trie.EmptyRoot[:]) { - result.State = append(result.State, hexutil.Bytes{0x80}) - break - } - } - } + result.State = appendLegacyEmptyStorageNode(result.State, mode, binTrie) // Sort after verifyWitnessStateless: RLPDecode treats result.State[0] as the trie root. slices.SortFunc(result.State, func(a, b hexutil.Bytes) int { @@ -984,6 +1000,22 @@ func (api *DebugAPIImpl) buildWitnessResult(ctx context.Context, tx kv.TemporalT return result, nil } +// appendLegacyEmptyStorageNode appends the empty storage-trie node (0x80) once when some +// account leaf carries an empty storage root (EmptyRoot appears only as an account-leaf +// storage-root field). It is an MPT artifact: canonical mode omits it and the binary trie +// has no such node. Called after stateless verification, which rejects the bare node. +func appendLegacyEmptyStorageNode(nodes []hexutil.Bytes, mode witnessMode, binTrie bool) []hexutil.Bytes { + if mode != witnessModeLegacy || binTrie { + return nodes + } + for _, node := range nodes { + if bytes.Contains(node, trie.EmptyRoot[:]) { + return append(nodes, hexutil.Bytes{0x80}) + } + } + return nodes +} + // accessedState summarizes everything the witness needs from a recorded execution: // the deduplicated set of accessed accounts/storage/code addresses, the sorted code // blobs that go into result.Codes, and the pre-state code reads that feed witness @@ -997,6 +1029,30 @@ type accessedState struct { SortedCodes []hexutil.Bytes CodeReads map[common.Hash]witnesstypes.CodeWithHash Deleted map[common.Address]struct{} + // ModifiedCode is the code the block writes, per address. The binary trie + // commits code, so a witness for it has to cover the chunk keys these imply. + ModifiedCode map[common.Address][]byte + // StorageZoneProbes names the accounts whose storage the CREATE-collision + // check read out of pre-state (RecordingState.PreStateHasStorage). + StorageZoneProbes map[common.Address]struct{} +} + +// pbinStorageProbes returns one plain storage key per account the +// CREATE-collision check found storage on. Touching them brings those accounts' +// storage zones into the witness, without which a binary-trie verifier reads a +// zone slot as no storage at all: the tree commits no per-account storage root, +// and the zone sits off the proof path the account's own leaves lie on. +func (a *accessedState) pbinStorageProbes() [][]byte { + probe := commitment.PBinStorageZoneProbeSlot() + keys := make([][]byte, 0, len(a.StorageZoneProbes)) + for addr := range a.StorageZoneProbes { + key := make([]byte, 0, len(addr)+len(probe)) + key = append(key, addr[:]...) + key = append(key, probe[:]...) + keys = append(keys, key) + } + slices.SortFunc(keys, bytes.Compare) + return keys } // isEmpty reports whether no accounts, storage slots, or code addresses were touched. @@ -1042,8 +1098,9 @@ func (a *accessedState) touchNonZeroKeys(sdCtx *commitmentdb.SharedDomainsCommit // touchAll touches every accessed account, storage slot, and code address on the // commitment context. Order matches the original inline implementation: accounts -// first, then storage, then code. -func (a *accessedState) touchAll(sdCtx *commitmentdb.SharedDomainsCommitmentContext) { +// first, then storage, then code. Bin additionally touches the storage-zone +// probes; hex needs none, since an MPT account leaf carries its storage root. +func (a *accessedState) touchAll(sdCtx *commitmentdb.SharedDomainsCommitmentContext, binTrie bool) { for addr := range a.Addresses { sdCtx.TouchKey(kv.AccountsDomain, string(addr[:]), nil) } @@ -1056,6 +1113,11 @@ func (a *accessedState) touchAll(sdCtx *commitmentdb.SharedDomainsCommitmentCont for addr := range a.CodeAddrs { sdCtx.TouchKey(kv.CodeDomain, string(addr[:]), nil) } + if binTrie { + for _, probe := range a.pbinStorageProbes() { + sdCtx.TouchKey(kv.StorageDomain, string(probe), nil) + } + } } // collectAccessedState rolls the RecordingState maps into an accessedState. @@ -1063,17 +1125,24 @@ func (a *accessedState) touchAll(sdCtx *commitmentdb.SharedDomainsCommitmentCont // verifier re-derives in-block-created code by replaying the transactions. func collectAccessedState(rs *RecordingState, mode witnessMode) *accessedState { out := &accessedState{ - Addresses: make(map[common.Address]struct{}), - Storage: make(map[common.Address]map[common.Hash]struct{}), - CodeAddrs: make(map[common.Address]struct{}), - SortedCodes: []hexutil.Bytes{}, - WitnessKeys: []hexutil.Bytes{}, - CodeReads: make(map[common.Hash]witnesstypes.CodeWithHash), - Deleted: make(map[common.Address]struct{}), + Addresses: make(map[common.Address]struct{}), + Storage: make(map[common.Address]map[common.Hash]struct{}), + CodeAddrs: make(map[common.Address]struct{}), + SortedCodes: []hexutil.Bytes{}, + WitnessKeys: []hexutil.Bytes{}, + CodeReads: make(map[common.Hash]witnesstypes.CodeWithHash), + ModifiedCode: make(map[common.Address][]byte), + Deleted: make(map[common.Address]struct{}), + + StorageZoneProbes: make(map[common.Address]struct{}, len(rs.PreStateHasStorage)), } + for addr := range rs.DeletedAccounts { out.Deleted[addr] = struct{}{} } + for addr := range rs.PreStateHasStorage { + out.StorageZoneProbes[addr] = struct{}{} + } readAddresses, readStorageKeys := rs.GetAccessedKeys() writeAddresses, writeStorageKeys := rs.GetModifiedKeys() @@ -1212,8 +1281,9 @@ func collectAccessedState(rs *RecordingState, mode witnessMode) *accessedState { for addr := range preCode { out.CodeAddrs[addr] = struct{}{} } - for addr := range modCode { + for addr, code := range modCode { out.CodeAddrs[addr] = struct{}{} + out.ModifiedCode[addr] = code } return out @@ -1223,6 +1293,12 @@ func collectAccessedState(rs *RecordingState, mode witnessMode) *accessedState { // (commitment from parent state, plain state from block end) and returns the sibling // paths the trie collapses through. The witness build must touch them, else collapsed- // sibling data is missing and stateless re-execution diverges from the root. +// +// The whole phase is hex-only. The binary trie does collapse branches, but it needs no +// second pass to find them: its pruner keeps the sibling hanging off every branch a +// proved key descends, so the survivor of any collapse is already in the witness. Both +// tools this phase uses refuse bin anyway — SetCollapseTracer panics and +// BranchChildCount is keyed by a hex nibble prefix. func detectCollapseSiblings( ctx context.Context, tx kv.TemporalTx, @@ -1233,7 +1309,12 @@ func detectCollapseSiblings( expectedBlockRoot common.Hash, accessed *accessedState, mode witnessMode, + binTrie bool, ) (siblingPaths [][]byte, err error) { + if binTrie { + return nil, nil + } + // Set up split reader: commitment from block beginning (durable) or the pinned // parent snapshot (head-capture), plain state from block end. withHistory=false // so branch updates are written using PutBranch(). @@ -1314,7 +1395,16 @@ func buildWitnessTrie( siblingPaths [][]byte, accessed *accessedState, produceExclusionProofs bool, + binTrie bool, ) (encodedNodes []hexutil.Bytes, err error) { + // TouchHashedKey records a hashed path with an empty plain key, which the bin update + // stream cannot resolve. Bin carries its collapse survivors through the pruner instead, + // so detectCollapseSiblings returns none for it and one arriving here is a bug to + // surface, not a case to serve. + if binTrie && len(siblingPaths) > 0 { + return nil, fmt.Errorf("binary trie witness got %d collapse sibling paths; the binary trie names none", len(siblingPaths)) + } + encodedNodes = []hexutil.Bytes{} sdCtx.SetCustomHistoryStateReader(trieReaderFor(hc, tx, firstTxNumInBlock)) @@ -1322,7 +1412,24 @@ func buildWitnessTrie( return nil, fmt.Errorf("failed to reset commitment for regular witness: %w", err) } - accessed.touchAll(sdCtx) + accessed.touchAll(sdCtx, binTrie) + + // The pass walks the parent state, which holds neither the code the block + // deploys nor any sign of which accounts it removed — and under bin both + // decide which keys the block touches. + if binTrie { + block := commitment.PBinWitnessBlock{ + Code: make(map[string][]byte, len(accessed.ModifiedCode)), + Removed: make(map[string]struct{}, len(accessed.Deleted)), + } + for addr, code := range accessed.ModifiedCode { + block.Code[string(addr[:])] = code + } + for addr := range accessed.Deleted { + block.Removed[string(addr[:])] = struct{}{} + } + sdCtx.SetWitnessBlock(block) + } if len(siblingPaths) > 0 { log.Debug("[debug_executionWitness] detected sibling paths", "count", len(siblingPaths)) @@ -1447,17 +1554,18 @@ func (api *BaseAPI) collectAccessedHeaders( return headers, byNumber, nil } -// verifyWitnessStateless optionally re-executes the block statelessly against the -// generated witness and asserts the resulting state root matches. Verification is -// a no-op when ERIGON_WITNESS_NO_VERIFY=true (it roughly doubles execution cost). +// verifyWitnessStateless re-executes the block statelessly against the generated +// witness and asserts the resulting state root matches. func (api *DebugAPIImpl) verifyWitnessStateless( ctx context.Context, tx kv.TemporalTx, result *ExecutionWitnessResult, block *types.Block, fullEngine rules.Engine, + binTrie bool, + parentRoot common.Hash, ) error { - if dbg.EnvBool("ERIGON_WITNESS_NO_VERIFY", false) { + if witnessVerifySkipped(binTrie) { return nil } @@ -1466,7 +1574,51 @@ func (api *DebugAPIImpl) verifyWitnessStateless( return fmt.Errorf("failed to get chain config: %w", err) } - newStateRoot, stateless, err := execBlockStatelessly(result, block, chainCfg, fullEngine) + return verifyWitnessAgainstBlock(ctx, result, block, parentRoot, chainCfg, fullEngine, binTrie) +} + +// witnessVerifySkipped reports whether ERIGON_WITNESS_NO_VERIFY may turn the +// stateless gate off. Under bin it never may: binary witnesses have no external +// conformance oracle, so re-execution is the only correctness evidence there is, +// while hex's opt-out exists only to save the roughly doubled execution cost. +func witnessVerifySkipped(binTrie bool) bool { + return !binTrie && dbg.EnvBool("ERIGON_WITNESS_NO_VERIFY", false) +} + +// verifyWitnessAgainstBlock re-executes the block from the witness alone and +// asserts it reaches the header's post-state root, then that keys[] carries a +// preimage for every leaf the re-execution resolved. The two variants share the +// replay and differ only in how a leaf resolves and how the root is merkelized; +// bin needs parentRoot because its decoder is told its root rather than deriving +// it from the node set. +func verifyWitnessAgainstBlock( + ctx context.Context, + result *ExecutionWitnessResult, + block *types.Block, + parentRoot common.Hash, + chainCfg *chain.Config, + fullEngine rules.Engine, + binTrie bool, +) error { + var ( + newStateRoot common.Hash + usedAddrs map[common.Address]struct{} + usedSlots map[common.Hash]struct{} + err error + ) + if binTrie { + var stateless *pbinWitnessStateless + newStateRoot, stateless, err = pbinExecBlockStatelessly(ctx, result, block, parentRoot, chainCfg, fullEngine) + if stateless != nil { + usedAddrs, usedSlots = stateless.usedTrieAddrs, stateless.usedTrieSlots + } + } else { + var stateless *witnessStateless + newStateRoot, stateless, err = execBlockStatelessly(result, block, chainCfg, fullEngine) + if stateless != nil { + usedAddrs, usedSlots = stateless.usedTrieAddrs, stateless.usedTrieSlots + } + } if err != nil { return fmt.Errorf("[debug_executionWitness] stateless block execution failed: %w", err) } @@ -1476,8 +1628,8 @@ func (api *DebugAPIImpl) verifyWitnessStateless( return fmt.Errorf("[debug_executionWitness] state root mismatch after stateless execution : got %x, expected %x", newStateRoot, expectedRoot) } - if stateless != nil { - if err := checkWitnessKeysComplete(stateless.usedTrieAddrs, stateless.usedTrieSlots, result.Keys); err != nil { + if usedAddrs != nil || usedSlots != nil { + if err := checkWitnessKeysComplete(usedAddrs, usedSlots, result.Keys); err != nil { return fmt.Errorf("[debug_executionWitness] %w", err) } } @@ -2057,6 +2209,30 @@ func execBlockStatelessly(result *ExecutionWitnessResult, block *types.Block, ch // common.HexToAddress("0x8863786beBE8eB9659DF00b49f8f1eeEc7e2C8c1"), }) + if err = replayBlockOverWitness(result, block, chainConfig, engine, stateless); err != nil { + return common.Hash{}, stateless, err + } + + // Finalize and compute the resulting state root + newStateRoot, err := stateless.Finalize() + if err != nil { + return common.Hash{}, stateless, fmt.Errorf("[statelessExec] stateless.Finalize() failed: %w", err) + } + return newStateRoot, stateless, nil +} + +// statelessWitnessState is the reader/writer seam a witness re-execution runs +// against. Hex and bin resolve a leaf and merkelize differently but replay a +// block identically, so the replay itself is shared. +type statelessWitnessState interface { + state.StateReader + state.StateWriter +} + +// replayBlockOverWitness drives the block through the EVM against a witness-backed +// reader/writer. It stops short of the post-state root, which each variant computes +// its own way. +func replayBlockOverWitness(result *ExecutionWitnessResult, block *types.Block, chainConfig *chain.Config, engine rules.Engine, stateless statelessWitnessState) error { // Create the in-block state with the witness stateless as reader ibs := state.New(stateless) defer ibs.Close() @@ -2074,18 +2250,18 @@ func execBlockStatelessly(result *ExecutionWitnessResult, block *types.Block, ch systemCallCustom := func(contract accounts.Address, data []byte, ibState *state.IntraBlockState, hdr *types.Header, constCall bool) ([]byte, error) { return protocol.SysCallContract(contract, data, chainConfig, ibState, hdr, engine, constCall, vm.Config{}) } - if err = engine.Initialize(chainConfig, nil /* chainReader */, header, ibs, systemCallCustom, log.Root(), nil); err != nil { - return common.Hash{}, stateless, fmt.Errorf("verification: failed to initialize block: %w", err) + if err := engine.Initialize(chainConfig, nil /* chainReader */, header, ibs, systemCallCustom, log.Root(), nil); err != nil { + return fmt.Errorf("verification: failed to initialize block: %w", err) } - if err = ibs.FinalizeTx(blockRules, stateless); err != nil { - return common.Hash{}, stateless, fmt.Errorf("verification: failed to finalize engine.Initialize tx: %w", err) + if err := ibs.FinalizeTx(blockRules, stateless); err != nil { + return fmt.Errorf("verification: failed to finalize engine.Initialize tx: %w", err) } // Execute all transactions in the block for txIndex, txn := range block.Transactions() { msg, err := txn.AsMessage(*signer, header.BaseFee, blockRules) if err != nil { - return common.Hash{}, stateless, fmt.Errorf("[statelessExec] failed to convert tx %d to message: %w", txIndex, err) + return fmt.Errorf("[statelessExec] failed to convert tx %d to message: %w", txIndex, err) } txCtx := protocol.NewEVMTxContext(msg) @@ -2095,14 +2271,13 @@ func execBlockStatelessly(result *ExecutionWitnessResult, block *types.Block, ch ibs.SetTxContext(blockNum, txIndex) // Apply the message - gasBailout must be false to properly deduct gas from sender - _, err = protocol.ApplyMessage(evm, msg, gp, true /* refunds */, false /* gasBailout */, engine) - if err != nil { - return common.Hash{}, stateless, fmt.Errorf("[statelessExec] failed to apply tx %d: %w", txIndex, err) + if _, err = protocol.ApplyMessage(evm, msg, gp, true /* refunds */, false /* gasBailout */, engine); err != nil { + return fmt.Errorf("[statelessExec] failed to apply tx %d: %w", txIndex, err) } // Finalize tx - state changes go to the witness stateless if err = ibs.FinalizeTx(blockRules, stateless); err != nil { - return common.Hash{}, stateless, fmt.Errorf("[statelessExec] failed to finalize tx %d: %w", txIndex, err) + return fmt.Errorf("[statelessExec] failed to finalize tx %d: %w", txIndex, err) } } @@ -2117,20 +2292,13 @@ func execBlockStatelessly(result *ExecutionWitnessResult, block *types.Block, ch // only Bor and AuRa engine use ChainReader. And the ChainReader is only used to read headers. This means their // witness may need to be augmented with headers accessed during their engine.Finalize(). This is something that // can be implemented later. For now use ChainReader = nil, as this is sufficient for Ethereum. - _, err = engine.Finalize(chainConfig, types.CopyHeader(header), ibs, block.Uncles(), statelessReceipts, block.Withdrawals(), nil /* chainReader */, syscall, false /*skipReceiptsEval*/, log.Root()) - if err != nil { - return common.Hash{}, stateless, fmt.Errorf("[statelessExec] engine.Finalize failed: %w", err) + if _, err := engine.Finalize(chainConfig, types.CopyHeader(header), ibs, block.Uncles(), statelessReceipts, block.Withdrawals(), nil /* chainReader */, syscall, false /*skipReceiptsEval*/, log.Root()); err != nil { + return fmt.Errorf("[statelessExec] engine.Finalize failed: %w", err) } - err = ibs.CommitBlock(blockRules, stateless) - if err != nil { - return common.Hash{}, stateless, fmt.Errorf("[statelessExec] ibs.CommitBlock() failed : %w", err) + if err := ibs.CommitBlock(blockRules, stateless); err != nil { + return fmt.Errorf("[statelessExec] ibs.CommitBlock() failed : %w", err) } - // Finalize and compute the resulting state root - newStateRoot, err := stateless.Finalize() - if err != nil { - return common.Hash{}, stateless, fmt.Errorf("[statelessExec] stateless.Finalize() failed: %w", err) - } - return newStateRoot, stateless, nil + return nil } diff --git a/rpc/jsonrpc/debug_execution_witness_test.go b/rpc/jsonrpc/debug_execution_witness_test.go index 7ce9b271d2a..5a99a233e50 100644 --- a/rpc/jsonrpc/debug_execution_witness_test.go +++ b/rpc/jsonrpc/debug_execution_witness_test.go @@ -32,6 +32,7 @@ import ( "github.com/erigontech/erigon/db/kv/prune" "github.com/erigontech/erigon/db/rawdb" "github.com/erigontech/erigon/db/state/statecfg" + "github.com/erigontech/erigon/execution/commitment" "github.com/erigontech/erigon/execution/commitment/commitmentdb" "github.com/erigontech/erigon/execution/protocol/params" "github.com/erigontech/erigon/execution/types" @@ -44,6 +45,7 @@ import ( // map, used to exercise RecordingState predicates without a database. type fakeStateReader struct { accounts map[common.Address]*accounts.Account + storage map[common.Address]bool } func (r *fakeStateReader) ReadAccountData(address accounts.Address) (*accounts.Account, error) { @@ -55,7 +57,9 @@ func (r *fakeStateReader) ReadAccountDataForDebug(address accounts.Address) (*ac func (r *fakeStateReader) ReadAccountStorage(address accounts.Address, key accounts.StorageKey) (uint256.Int, bool, error) { return uint256.Int{}, false, nil } -func (r *fakeStateReader) HasStorage(address accounts.Address) (bool, error) { return false, nil } +func (r *fakeStateReader) HasStorage(address accounts.Address) (bool, error) { + return r.storage[address.Value()], nil +} func (r *fakeStateReader) ReadAccountCode(address accounts.Address) ([]byte, error) { return nil, nil } func (r *fakeStateReader) ReadAccountCodeSize(address accounts.Address) (int, error) { return 0, nil } func (r *fakeStateReader) ReadAccountIncarnation(address accounts.Address) (uint64, error) { @@ -367,7 +371,7 @@ func TestResolveWitnessMode(t *testing.T) { str := func(s string) *string { return &s } t.Run("param selects mode", func(t *testing.T) { - got, err := resolveWitnessMode(str("legacy")) + got, err := resolveWitnessMode(str("legacy"), false) if err != nil { t.Fatal(err) } @@ -375,7 +379,7 @@ func TestResolveWitnessMode(t *testing.T) { t.Errorf("param legacy should resolve to legacy mode, got %v", got) } - got, err = resolveWitnessMode(str("canonical")) + got, err = resolveWitnessMode(str("canonical"), false) if err != nil { t.Fatal(err) } @@ -385,13 +389,13 @@ func TestResolveWitnessMode(t *testing.T) { }) t.Run("unknown param rejected", func(t *testing.T) { - if _, err := resolveWitnessMode(str("bogus")); err == nil { + if _, err := resolveWitnessMode(str("bogus"), false); err == nil { t.Error("expected error for unknown mode param") } }) t.Run("legacy default when param nil", func(t *testing.T) { - got, err := resolveWitnessMode(nil) + got, err := resolveWitnessMode(nil, false) if err != nil { t.Fatal(err) } @@ -651,3 +655,32 @@ func TestGetWitnessHeadCaptureOutOfWindowWhenPruned(t *testing.T) { _, err := api.GetWitness(ctx, rpc.BlockNumberOrHash{BlockNumber: &bn}) require.ErrorIs(t, err, errWitnessOutOfWindow) } + +// TestCollectAccessedState_PBinStorageZoneProbes asserts the CREATE-collision +// check leaves a storage-zone probe behind for an account whose pre-state +// storage it found. The binary trie commits no per-account storage root, so +// without the probe the witness carries nothing that can answer EIP-7610 for a +// slot outside the account header and the replay re-runs the CREATE. +func TestCollectAccessedState_PBinStorageZoneProbes(t *testing.T) { + withStorage := common.HexToAddress("0x1111111111111111111111111111111111111111") + without := common.HexToAddress("0x2222222222222222222222222222222222222222") + + inner := &fakeStateReader{ + accounts: map[common.Address]*accounts.Account{withStorage: {Nonce: 1}, without: {Nonce: 1}}, + storage: map[common.Address]bool{withStorage: true}, + } + rs := NewRecordingState(inner) + for _, addr := range []common.Address{withStorage, without} { + if _, err := rs.HasStorage(accounts.InternAddress(addr)); err != nil { + t.Fatal(err) + } + } + + accessed := collectAccessedState(rs, witnessModeCanonical) + probes := accessed.pbinStorageProbes() + + probe := commitment.PBinStorageZoneProbeSlot() + want := append(bytes.Clone(withStorage[:]), probe[:]...) + require.Equal(t, [][]byte{want}, probes, + "the probe belongs to the account whose pre-state storage the collision check found, and to no other") +} diff --git a/rpc/jsonrpc/eth_call.go b/rpc/jsonrpc/eth_call.go index b2244f5df5c..3a1578d43b9 100644 --- a/rpc/jsonrpc/eth_call.go +++ b/rpc/jsonrpc/eth_call.go @@ -789,7 +789,7 @@ func (api *BaseAPI) getWitness(ctx context.Context, db kv.TemporalRoDB, blockNrO siblingPaths, err := detectCollapseSiblings(ctx, tx, nil, domains, sdCtx, firstTxNumInBlock, endTxNum, blockNr, parentNum, - block.Root(), accessed, witnessModeLegacy) + block.Root(), accessed, witnessModeLegacy, false /* binTrie: WithHexCommitmentOnly refuses bin above */) if err != nil { return nil, err } @@ -799,7 +799,7 @@ func (api *BaseAPI) getWitness(ctx context.Context, db kv.TemporalRoDB, blockNrO return nil, fmt.Errorf("failed to reset commitment for witness: %w", err) } - accessed.touchAll(sdCtx) + accessed.touchAll(sdCtx, false /* binTrie: WithHexCommitmentOnly refuses bin above */) for _, siblingPath := range siblingPaths { sdCtx.TouchHashedKey(siblingPath) } diff --git a/rpc/jsonrpc/pbin_witness_altspec_test.go b/rpc/jsonrpc/pbin_witness_altspec_test.go new file mode 100644 index 00000000000..5b8066ecfb5 --- /dev/null +++ b/rpc/jsonrpc/pbin_witness_altspec_test.go @@ -0,0 +1,240 @@ +package jsonrpc + +// What a binary witness would weigh if code were not committed as chunk leaves: +// never chunk; every contract ships as a blob, as hex does. +// +// The variant is measured, not modelled: the chunk keys are dropped from the +// proved set and the real pruner re-runs, so the branches that existed only to +// bind those chunks go too. The blob term is the contract's own bytecode, the +// same bytes hex carries in Codes. +// +// The variant is not a proposal and its root differs from the spec's — this +// prices the choice, it does not implement it. + +import ( + "bytes" + "fmt" + "maps" + "slices" + "testing" + + "github.com/stretchr/testify/require" + + "github.com/erigontech/erigon/execution/commitment" + "github.com/erigontech/erigon/rpc/rpccfg" +) + +type pbinAltRow struct { + name, hex string + size int + chunks int + + hexProof, hexCode, hexTotal int + hexNodes int + + binNodes, binTotal int + leanNodes, leanProof int // re-pruned, code-chunk keys dropped +} + +// blob is what the contract's bytecode weighs when a witness ships it whole. +func (r pbinAltRow) blob() int { return r.size } + +// noChunk drops chunking outright. +func (r pbinAltRow) noChunk() int { return r.leanProof + r.blob() } + +func TestPBinWitnessNoCodeZone(t *testing.T) { + withCommitmentHistory(t) + n := len(pbinGranCases) + rows := make([]pbinAltRow, n) + for i := range rows { + rows[i].name = pbinGranCases[i].name + rows[i].size = pbinGranCases[i].size + rows[i].chunks = pbinGranCases[i].chunks + } + + t.Run("hex", func(t *testing.T) { + c, _ := pbinGranChain(t) + enableCommitmentHistoryFlag(t, c.m.DB) + api := NewPrivateDebugAPI(newBaseApiForTest(c.m), c.m.DB, nil, &rpccfg.DebugApiConfig{}) + for i := range rows { + w := pbinWitnessOf(t, api, uint64(n+i+1)) + r := &rows[i] + r.hexProof = sumBytes(w.State) + sumBytes(w.Headers) + r.hexCode = sumBytes(w.Codes) + r.hexTotal = r.hexProof + r.hexCode + r.hexNodes = len(w.State) + } + }) + + t.Run("bin", func(t *testing.T) { + withBinCommitmentDatadir(t) + c, _ := pbinGranChain(t) + enableCommitmentHistoryFlag(t, c.m.DB) + api := NewPrivateDebugAPI(newBaseApiForTest(c.m), c.m.DB, nil, &rpccfg.DebugApiConfig{}) + for i := range rows { + num := uint64(n + i + 1) + w := pbinWitnessOf(t, api, num) + r := &rows[i] + + nodes := make([][]byte, 0, len(w.State)) + keep := make([][]byte, 0, len(w.State)) + for _, node := range w.State { + nodes = append(nodes, node) + r.binNodes++ + r.binTotal += len(node) + key := pbinLeafKeyOf(node) + if key != nil && !isCodeChunkKey(key) { + keep = append(keep, key) + } + } + r.binTotal += sumBytes(w.Headers) + + // The RPC sorts result.State, so take the root from the parent header. + root := c.block(t, num-1).Root() + lean, err := commitment.PBinWitnessNodesForKeys(nodes, root[:], keep) + require.NoError(t, err, "%s re-prune", r.name) + r.leanNodes = len(lean) + r.leanProof = sumBytes(w.Headers) + for _, node := range lean { + r.leanProof += len(node) + } + // What the "blob" column costs is only meaningful if dropping the + // chunk keys really drops nodes: the code zone has to be a separable + // part of the witness, not entangled with the account's proof. + require.Less(t, r.leanNodes, r.binNodes, "%s: re-pruning kept every node", r.name) + require.NotZero(t, r.leanNodes, "%s: re-pruning kept nothing", r.name) + } + }) + + t.Log("witness bytes for a call executing 8 bytes, by how code is committed\n" + pbinAltTable(rows)) +} + +// TestPBinWitnessPartialChunks prices chunking's own premise. A blob costs the +// bytecode once; a chunk leaf plus the branch binding it costs ~4.35x the 31 +// bytes it carries, so chunking only wins when a witness can prove a fraction +// of the contract rather than all of it. This sweeps that fraction against the +// real pruner and reports where the two meet. +func TestPBinWitnessPartialChunks(t *testing.T) { + withCommitmentHistory(t) + withBinCommitmentDatadir(t) + n := len(pbinGranCases) + c, _ := pbinGranChain(t) + enableCommitmentHistoryFlag(t, c.m.DB) + api := NewPrivateDebugAPI(newBaseApiForTest(c.m), c.m.DB, nil, &rpccfg.DebugApiConfig{}) + + out := fmt.Sprintf("%-16s %6s %8s %6s | %9s %8s | %8s %9s\n", + "case", "chunks", "pattern", "proved", "witness B", "vs blob", "blob B", "break-even") + for i, gc := range pbinGranCases { + if gc.chunks < 32 || gc.zeroPad { + continue + } + w := pbinWitnessOf(t, api, uint64(n+i+1)) + nodes := make([][]byte, 0, len(w.State)) + var base, chunkKeys [][]byte + for _, node := range w.State { + nodes = append(nodes, node) + key := pbinLeafKeyOf(node) + if key == nil { + continue + } + if isCodeChunkKey(key) { + chunkKeys = append(chunkKeys, key) + continue + } + base = append(base, key) + } + chunkKeys = pbinContractChunkKeys(t, chunkKeys, gc.chunks) + + root := c.block(t, uint64(n+i)).Root() + size := func(keep [][]byte) int { + lean, err := commitment.PBinWitnessNodesForKeys(nodes, root[:], keep) + require.NoError(t, err) + return sumBytes(w.Headers) + func() int { + total := 0 + for _, node := range lean { + total += len(node) + } + return total + }() + } + blob := size(base) + gc.size + + for _, pattern := range []string{"adjacent", "scattered"} { + var prev int + for _, proved := range []int{1, 8, 32, 64, 128, 256, gc.chunks} { + if proved > gc.chunks || proved == prev { + continue + } + prev = proved + keep := slices.Clone(base) + for j := range proved { + idx := j + if pattern == "scattered" { + idx = j * gc.chunks / proved + } + keep = append(keep, chunkKeys[idx]) + } + got := size(keep) + mark := "" + if got > blob { + mark = " (dearer than a blob)" + } + out += fmt.Sprintf("%-16s %6d %8s %6d | %9d %7.2fx | %8d%s\n", + gc.name, gc.chunks, pattern, proved, got, float64(got)/float64(blob), blob, mark) + } + } + } + t.Log("witness bytes when only part of a contract's chunks are proved\n" + out) +} + +// pbinContractChunkKeys picks the chunk leaves of the contract under test out of +// every chunk leaf the witness carries. The pruner also keeps the sibling +// hanging off each branch it descends, so a chunk leaf of an unrelated contract +// can ride along; the contract's own stems are the ones holding a whole group, +// plus one holding the remainder. +func pbinContractChunkKeys(t *testing.T, keys [][]byte, chunks int) [][]byte { + t.Helper() + stems := map[string][][]byte{} + for _, key := range keys { + stems[string(key[:len(key)-1])] = append(stems[string(key[:len(key)-1])], key) + } + out := make([][]byte, 0, chunks) + for left := chunks; left > 0; left -= pbinCodeGroupChunks { + size := min(left, pbinCodeGroupChunks) + group := "" + for _, stem := range slices.Sorted(maps.Keys(stems)) { + if len(stems[stem]) == size { + group = stem + break + } + } + require.NotEmpty(t, group, "no stem holds a group of %d chunks", size) + out = append(out, stems[group]...) + delete(stems, group) + } + require.Len(t, out, chunks) + slices.SortFunc(out, bytes.Compare) + return out +} + +func pbinAltTable(rows []pbinAltRow) string { + s := fmt.Sprintf("%-16s %7s %6s | %8s | %9s %7s | %9s %7s\n", + "case", "code B", "chunks", "hex tot", + "spec", "/hex", "blob", "/hex") + for _, r := range rows { + ratio := func(v int) float64 { return float64(v) / float64(r.hexTotal) } + s += fmt.Sprintf("%-16s %7d %6d | %8d | %9d %6.2fx | %9d %6.2fx\n", + r.name, r.size, r.chunks, r.hexTotal, + r.binTotal, ratio(r.binTotal), + r.noChunk(), ratio(r.noChunk())) + } + s += "\nproof bytes alone, code blob excluded from both sides:\n" + s += fmt.Sprintf("%-16s %9s %8s | %9s %8s %7s | %8s\n", + "case", "hexProof", "hexNodes", "binProof", "binNodes", "/hex", "blob B") + for _, r := range rows { + s += fmt.Sprintf("%-16s %9d %8d | %9d %8d %6.2fx | %8d\n", + r.name, r.hexProof, r.hexNodes, + r.leanProof, r.leanNodes, float64(r.leanProof)/float64(r.hexProof), r.blob()) + } + return s +} diff --git a/rpc/jsonrpc/pbin_witness_bytesplit_test.go b/rpc/jsonrpc/pbin_witness_bytesplit_test.go new file mode 100644 index 00000000000..255c67f6e94 --- /dev/null +++ b/rpc/jsonrpc/pbin_witness_bytesplit_test.go @@ -0,0 +1,103 @@ +package jsonrpc + +// Measures what code chunking costs a binary witness, two ways: a byte split by +// node kind, and a re-prune with every code-chunk key dropped from the proved +// set so the branches that existed only to bind those chunks go too. +// +// The re-prune is not a proposal — the keys are inside the leaf hashes and the +// root would move. It sizes the cost. +// +// Two limits on what the numbers mean. Proved keys are derived from the leaves +// present in the witness, so absence proofs are undercounted: a blinded node for +// a key with no leaf is off every derived proof path and the re-prune drops it, +// which reads as a code saving on a block holding no code chunks. And the +// re-prune baseline is not the input witness, so the saved column is only a code +// figure for blocks whose code% is non-zero. + +import ( + "testing" + + "github.com/stretchr/testify/require" + + "github.com/erigontech/erigon/execution/commitment" + "github.com/erigontech/erigon/rpc/rpccfg" +) + +// pbinLeafKeyOf returns the tree key of a leaf preimage, or nil for a branch. +func pbinLeafKeyOf(node []byte) []byte { + if len(node) == 0 || node[0] != 0x00 { + return nil + } + return node[1 : len(node)-32] +} + +// isCodeChunkKey reports whether a tree key names a code chunk: every chunk +// lives in the code zone, content-addressed by code hash. +func isCodeChunkKey(key []byte) bool { + return len(key) > 0 && key[0] == 0x01 +} + +// pbinDelegationSubIndex is the EIP's DELEGATION_LEAF_KEY, unexported by package +// commitment. It stands where CODE_HASH does for a 7702-delegated account. +const pbinDelegationSubIndex = 2 + +func TestPBinWitnessCodeWeight(t *testing.T) { + withCommitmentHistory(t) + withBinCommitmentDatadir(t) + c := buildPBinWitnessChain(t) + enableCommitmentHistoryFlag(t, c.m.DB) + api := NewPrivateDebugAPI(newBaseApiForTest(c.m), c.m.DB, nil, &rpccfg.DebugApiConfig{}) + + t.Logf("%-6s %-38s %6s %8s %8s %8s %7s %8s %8s %7s", + "block", "shape", "nodes", "total", "leafkey", "branch", "code%", "noCodeN", "noCodeB", "saved") + + var gTotal, gNoCode int + for _, block := range pbinWitnessCorpus { + result := pbinWitnessOf(t, api, block.num) + require.NotEmpty(t, result.State) + + nodes := make([][]byte, 0, len(result.State)) + for _, n := range result.State { + nodes = append(nodes, n) + } + // The RPC sorts result.State bytewise before returning, so the root-first + // contract is gone by here; take the root from the parent header instead. + root := c.block(t, block.num-1).Root() + + var total, leafKey, branch, codeBytes int + var keep [][]byte + for _, n := range nodes { + total += len(n) + key := pbinLeafKeyOf(n) + if key == nil { + branch += len(n) + continue + } + leafKey += len(key) + if isCodeChunkKey(key) { + codeBytes += len(n) + continue + } + keep = append(keep, key) + } + + lean, err := commitment.PBinWitnessNodesForKeys(nodes, root[:], keep) + require.NoError(t, err, "block %d re-prune", block.num) + noCodeBytes := 0 + for _, n := range lean { + noCodeBytes += len(n) + } + + gTotal += total + gNoCode += noCodeBytes + saved := 0.0 + if total > 0 { + saved = 100 * float64(total-noCodeBytes) / float64(total) + } + t.Logf("%-6d %-38s %6d %8d %8d %8d %6.1f%% %8d %8d %6.1f%%", + block.num, block.shape, len(nodes), total, leafKey, branch, + 100*float64(codeBytes)/float64(total), len(lean), noCodeBytes, saved) + } + t.Logf("corpus: %d B with code, %d B without (%.1f%% is code chunking)", + gTotal, gNoCode, 100*float64(gTotal-gNoCode)/float64(gTotal)) +} diff --git a/rpc/jsonrpc/pbin_witness_clone_test.go b/rpc/jsonrpc/pbin_witness_clone_test.go new file mode 100644 index 00000000000..c678eed6491 --- /dev/null +++ b/rpc/jsonrpc/pbin_witness_clone_test.go @@ -0,0 +1,201 @@ +package jsonrpc + +// What duplicated bytecode costs a binary witness. +// +// Every chunk lives in the code zone, keyed by code hash alone, so a block +// calling several clones of one contract proves one shared chunk set; distinct +// contracts of the same size prove one set each. This measures that sharing +// against hex, which stores code by hash and so ships it once either way. + +import ( + "fmt" + "math/big" + "testing" + + "github.com/holiman/uint256" + "github.com/jinzhu/copier" + "github.com/stretchr/testify/require" + + "github.com/erigontech/erigon/common" + "github.com/erigontech/erigon/common/crypto" + "github.com/erigontech/erigon/execution/chain" + "github.com/erigontech/erigon/execution/execmodule/execmoduletester" + "github.com/erigontech/erigon/execution/tests/blockgen" + "github.com/erigontech/erigon/execution/types" + "github.com/erigontech/erigon/rpc/rpccfg" +) + +const ( + pbinCloneCount = 8 + // One chunk past a full group, so sharing is pinned across a group boundary. + pbinCloneChunks = pbinCodeGroupChunks + 1 + pbinCloneSize = 31 * pbinCloneChunks +) + +// pbinCloneChain deploys pbinCloneCount identical contracts and as many +// distinct ones of the same size, then calls each group in one block. +func pbinCloneChain(t *testing.T) (*pbinWitnessChain, uint64, uint64) { + t.Helper() + bankKey, err := crypto.HexToECDSA("b71c71a67e1177ad4e901695e1b4b9ee17ae16c6668d313eac2f96dbcda3f291") + require.NoError(t, err) + bankAddress := crypto.PubkeyToAddress(bankKey.PublicKey) + bankFunds, ok := new(big.Int).SetString("100000000000000000000", 10) + require.True(t, ok) + + chainConfig := new(chain.Config) + require.NoError(t, copier.CopyWithOption(chainConfig, chain.TestChainBerlinConfig, copier.Option{DeepCopy: true})) + m := execmoduletester.New(t, + execmoduletester.WithGenesisSpec(&types.Genesis{ + Config: chainConfig, + Alloc: types.GenesisAlloc{bankAddress: {Balance: bankFunds}}, + GasLimit: 60_000_000, + }), + execmoduletester.WithKey(bankKey)) + + signer := types.LatestSignerForChainID(nil) + gasPrice := uint256.NewInt(1_000_000_000) + sign := func(txn *types.LegacyTx) types.Transaction { + t.Helper() + txn.GasPrice = *gasPrice + signed, err := types.SignTx(txn, *signer, bankKey) + require.NoError(t, err) + return signed + } + + runtimeOf := func(distinct bool, i int) []byte { + code := make([]byte, pbinCloneSize) + for j := range code { + code[j] = 0xfe + } + copy(code, pbinStoreRuntime) + if distinct { + code[len(code)-1] = byte(i) + } + return code + } + + clones := make([]common.Address, pbinCloneCount) + distinct := make([]common.Address, pbinCloneCount) + deploys := 2 * pbinCloneCount + + pack, err := blockgen.GenerateChain(m.ChainConfig, m.Genesis, m.Engine, m.DB, deploys+2, func(i int, b *blockgen.BlockGen) { + nonce := b.TxNonce(bankAddress) + switch { + case i < pbinCloneCount: + clones[i] = types.CreateAddress(bankAddress, nonce) + b.AddTx(sign(&types.LegacyTx{CommonTx: types.CommonTx{ + Nonce: nonce, GasLimit: 12_000_000, Data: pbinDeployCode(runtimeOf(false, i)), + }})) + case i < deploys: + k := i - pbinCloneCount + distinct[k] = types.CreateAddress(bankAddress, nonce) + b.AddTx(sign(&types.LegacyTx{CommonTx: types.CommonTx{ + Nonce: nonce, GasLimit: 12_000_000, Data: pbinDeployCode(runtimeOf(true, k)), + }})) + default: + targets := clones + if i == deploys+1 { + targets = distinct + } + for k := range targets { + b.AddTx(sign(&types.LegacyTx{CommonTx: types.CommonTx{ + Nonce: b.TxNonce(bankAddress), To: &targets[k], GasLimit: 200_000, + Data: pbinStoreCalldata(common.HexToHash("0x01"), uint64(k+1)), + }})) + } + } + }) + require.NoError(t, err) + require.NoError(t, m.InsertChain(pack)) + return &pbinWitnessChain{m: m, pack: pack}, uint64(deploys + 1), uint64(deploys + 2) +} + +func TestPBinWitnessCloneDedup(t *testing.T) { + withCommitmentHistory(t) + + type row struct { + name string + hexState, hexCodes, hexTot int + binTot, chunkB, branchB int + binNodes int + } + rows := []row{{name: "8 clones"}, {name: "8 distinct"}} + + t.Run("hex", func(t *testing.T) { + c, cloneBlock, distinctBlock := pbinCloneChain(t) + enableCommitmentHistoryFlag(t, c.m.DB) + api := NewPrivateDebugAPI(newBaseApiForTest(c.m), c.m.DB, nil, &rpccfg.DebugApiConfig{}) + for i, num := range []uint64{cloneBlock, distinctBlock} { + w := pbinWitnessOf(t, api, num) + rows[i].hexState = sumBytes(w.State) + sumBytes(w.Headers) + rows[i].hexCodes = sumBytes(w.Codes) + rows[i].hexTot = rows[i].hexState + rows[i].hexCodes + } + require.Less(t, rows[0].hexCodes, rows[1].hexCodes, "hex ships duplicated code once") + }) + + t.Run("bin", func(t *testing.T) { + withBinCommitmentDatadir(t) + c, cloneBlock, distinctBlock := pbinCloneChain(t) + enableCommitmentHistoryFlag(t, c.m.DB) + api := NewPrivateDebugAPI(newBaseApiForTest(c.m), c.m.DB, nil, &rpccfg.DebugApiConfig{}) + for i, num := range []uint64{cloneBlock, distinctBlock} { + w := pbinWitnessOf(t, api, num) + r := &rows[i] + r.binNodes = len(w.State) + r.binTot = sumBytes(w.State) + sumBytes(w.Headers) + for _, node := range w.State { + key := pbinLeafKeyOf(node) + switch { + case key == nil: + r.branchB += len(node) + case isCodeChunkKey(key): + r.chunkB += len(node) + } + } + } + // Direction stated up front: hex ships duplicated code once by hash, so + // only bin's chunk sharing can keep the clone block anywhere near the + // distinct block — clones must prove fewer bytes than distinct code. + require.Less(t, rows[0].chunkB, rows[1].chunkB, "clones must prove fewer chunk bytes than distinct contracts") + require.Less(t, rows[0].binTot, rows[1].binTot, "clones must prove a smaller witness than distinct contracts") + }) + + out := fmt.Sprintf("%d contracts of %d B (%d chunks, one spilling past a full group), all called in one block\n", + pbinCloneCount, pbinCloneSize, pbinCloneChunks) + out += fmt.Sprintf("%-12s %9s %9s %8s | %8s %7s %8s %9s %9s\n", + "block", "hexState", "hexCodes", "hex tot", "bin tot", "/hex", "binNod", "chunkB", "branchB") + for _, r := range rows { + out += fmt.Sprintf("%-12s %9d %9d %8d | %8d %6.2fx %8d %9d %9d\n", + r.name, r.hexState, r.hexCodes, r.hexTot, + r.binTot, float64(r.binTot)/float64(max(r.hexTot, 1)), r.binNodes, r.chunkB, r.branchB) + } + t.Log("witness cost of duplicated bytecode\n" + out) +} + +// TestPBinWitnessClonesProveOneChunkSet pins what content addressing was +// adopted for: accounts sharing bytecode share its chunk leaves, so the clone +// block proves exactly one chunk set and the distinct block one per contract. +func TestPBinWitnessClonesProveOneChunkSet(t *testing.T) { + withCommitmentHistory(t) + withBinCommitmentDatadir(t) + + c, cloneBlock, distinctBlock := pbinCloneChain(t) + enableCommitmentHistoryFlag(t, c.m.DB) + api := NewPrivateDebugAPI(newBaseApiForTest(c.m), c.m.DB, nil, &rpccfg.DebugApiConfig{}) + + countChunks := func(num uint64) int { + chunks := 0 + for _, node := range pbinWitnessOf(t, api, num).State { + if key := pbinLeafKeyOf(node); key != nil && isCodeChunkKey(key) { + chunks++ + } + } + return chunks + } + + require.Equal(t, pbinCloneChunks, countChunks(cloneBlock), + "%d clones share one content-addressed chunk set", pbinCloneCount) + require.Equal(t, pbinCloneCount*pbinCloneChunks, countChunks(distinctBlock), + "distinct bytecode proves one chunk set per contract") +} diff --git a/rpc/jsonrpc/pbin_witness_deploy_test.go b/rpc/jsonrpc/pbin_witness_deploy_test.go new file mode 100644 index 00000000000..fe2921bc4bd --- /dev/null +++ b/rpc/jsonrpc/pbin_witness_deploy_test.go @@ -0,0 +1,83 @@ +package jsonrpc + +// A block that deploys a contract writes code-chunk leaves. Under bin the +// witness pass walks the parent state, where that code does not exist yet, so +// the chunk keys have to come from the block's own code — otherwise the nodes +// those insertions split go unproved and a stateless verifier cannot reach the +// post-state root. +// +// The second deploy is what makes this bite: the first lands in an empty code +// zone and splits nothing. + +import ( + "math/big" + "testing" + + "github.com/holiman/uint256" + "github.com/jinzhu/copier" + "github.com/stretchr/testify/require" + + "github.com/erigontech/erigon/common/crypto" + "github.com/erigontech/erigon/execution/chain" + "github.com/erigontech/erigon/execution/execmodule/execmoduletester" + "github.com/erigontech/erigon/execution/tests/blockgen" + "github.com/erigontech/erigon/execution/types" + "github.com/erigontech/erigon/rpc/rpccfg" +) + +func TestPBinWitnessConsecutiveDeploys(t *testing.T) { + withCommitmentHistory(t) + withBinCommitmentDatadir(t) + + bankKey, err := crypto.HexToECDSA("b71c71a67e1177ad4e901695e1b4b9ee17ae16c6668d313eac2f96dbcda3f291") + require.NoError(t, err) + bankAddress := crypto.PubkeyToAddress(bankKey.PublicKey) + bankFunds, ok := new(big.Int).SetString("100000000000000000000", 10) + require.True(t, ok) + + chainConfig := new(chain.Config) + require.NoError(t, copier.CopyWithOption(chainConfig, chain.TestChainBerlinConfig, copier.Option{DeepCopy: true})) + m := execmoduletester.New(t, + execmoduletester.WithGenesisSpec(&types.Genesis{ + Config: chainConfig, + Alloc: types.GenesisAlloc{bankAddress: {Balance: bankFunds}}, + GasLimit: 60_000_000, + }), + execmoduletester.WithKey(bankKey)) + + signer := types.LatestSignerForChainID(nil) + const deploys = 3 + pack, err := blockgen.GenerateChain(m.ChainConfig, m.Genesis, m.Engine, m.DB, deploys, func(i int, b *blockgen.BlockGen) { + // Each contract is distinct, so every deploy opens its own code-zone + // stem beside the ones already there. + runtime := make([]byte, 31*(8+i)) + for j := range runtime { + runtime[j] = 0xfe + } + copy(runtime, pbinStoreRuntime) + runtime[len(runtime)-1] = byte(i) + + nonce := b.TxNonce(bankAddress) + txn := &types.LegacyTx{CommonTx: types.CommonTx{ + Nonce: nonce, GasLimit: 12_000_000, Data: pbinDeployCode(runtime), + }} + txn.GasPrice = *uint256.NewInt(1_000_000_000) + signed, err := types.SignTx(txn, *signer, bankKey) + require.NoError(t, err) + b.AddTx(signed) + }) + require.NoError(t, err) + require.NoError(t, m.InsertChain(pack)) + + c := &pbinWitnessChain{m: m, pack: pack} + enableCommitmentHistoryFlag(t, c.m.DB) + api := NewPrivateDebugAPI(newBaseApiForTest(c.m), c.m.DB, nil, &rpccfg.DebugApiConfig{}) + + for block := uint64(1); block <= deploys; block++ { + // pbinWitnessOf fails the test if stateless verification rejects the + // witness, which is the assertion here. + w := pbinWitnessOf(t, api, block) + require.NotEmpty(t, w.State, "block %d", block) + } + +} diff --git a/rpc/jsonrpc/pbin_witness_e2e_test.go b/rpc/jsonrpc/pbin_witness_e2e_test.go new file mode 100644 index 00000000000..13b8aaded9f --- /dev/null +++ b/rpc/jsonrpc/pbin_witness_e2e_test.go @@ -0,0 +1,362 @@ +// Copyright 2026 The Erigon Authors +// This file is part of Erigon. +// +// Erigon is free software: you can redistribute it and/or modify +// it under the terms of the GNU Lesser General Public License as published by +// the Free Software Foundation, either version 3 of the License, or +// (at your option) any later version. +// +// Erigon is distributed in the hope that it will be useful, +// but WITHOUT ANY WARRANTY; without even the implied warranty of +// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +// GNU Lesser General Public License for more details. +// +// You should have received a copy of the GNU Lesser General Public License +// along with Erigon. If not, see . + +package jsonrpc + +import ( + "math/big" + "testing" + + "github.com/holiman/uint256" + "github.com/jinzhu/copier" + "github.com/stretchr/testify/require" + + "github.com/erigontech/erigon/common" + "github.com/erigontech/erigon/common/crypto" + "github.com/erigontech/erigon/db/kv/rawdbv3" + "github.com/erigontech/erigon/execution/chain" + "github.com/erigontech/erigon/execution/execmodule/execmoduletester" + "github.com/erigontech/erigon/execution/state" + "github.com/erigontech/erigon/execution/tests/blockgen" + "github.com/erigontech/erigon/execution/types" + "github.com/erigontech/erigon/execution/types/accounts" + "github.com/erigontech/erigon/rpc" + "github.com/erigontech/erigon/rpc/rpccfg" + "github.com/erigontech/erigon/rpc/rpchelper" +) + +// pbinCodeGroupChunks is how many 31-byte chunks one code-zone stem holds: the +// spec's STEM_SUBTREE_WIDTH, unexported by package commitment. +const pbinCodeGroupChunks = 256 + +// pbinStoreRuntime stores calldata[32:64] at slot calldata[0:32], so one deploy +// serves both a non-zero SSTORE and an SSTORE-to-zero. +var pbinStoreRuntime = []byte{ + 0x60, 0x20, 0x35, // PUSH1 32; CALLDATALOAD -> value + 0x60, 0x00, 0x35, // PUSH1 0; CALLDATALOAD -> slot + 0x55, // SSTORE + 0x00, // STOP +} + +// pbinDeployCode wraps runtime code in an initcode that returns it verbatim. +func pbinDeployCode(runtime []byte) []byte { + size := len(runtime) + const prefixLen = 14 + initcode := []byte{ + 0x61, byte(size >> 8), byte(size), // PUSH2 size + 0x60, prefixLen, // PUSH1 codeOffset + 0x60, 0x00, // PUSH1 destOffset + 0x39, // CODECOPY + 0x61, byte(size >> 8), byte(size), // PUSH2 size + 0x60, 0x00, // PUSH1 offset + 0xf3, // RETURN + } + return append(initcode, runtime...) +} + +// pbinStoreCalldata is the (slot, value) pair pbinStoreRuntime writes. +func pbinStoreCalldata(slot common.Hash, value uint64) []byte { + val := uint256.NewInt(value).Bytes32() + return append(slot[:], val[:]...) +} + +type pbinWitnessChain struct { + m *execmoduletester.ExecModuleTester + pack *blockgen.ChainPack + receiver common.Address + small common.Address // code fits one code-zone group + large common.Address // code crosses a code-zone group boundary + slot common.Hash + otherSlot common.Hash +} + +// block returns the block at the given height; height 0 is genesis. +func (c *pbinWitnessChain) block(t *testing.T, num uint64) *types.Block { + t.Helper() + if num == 0 { + return c.m.Genesis + } + require.LessOrEqual(t, num, uint64(len(c.pack.Blocks))) + return c.pack.Blocks[num-1] +} + +// buildPBinWitnessChain generates and imports a chain whose blocks each exercise +// one shape the binary witness has to carry: a plain transfer, a deploy fitting +// one code-zone group, a deploy crossing a group boundary, a non-zero SSTORE, +// an SSTORE-to-zero, a call reading code back across a group boundary, and a +// block with no transactions. +func buildPBinWitnessChain(t *testing.T) *pbinWitnessChain { + t.Helper() + + m, bankKey, bankAddress := fundedBankGenesis(t, chain.TestChainBerlinConfig) + signer := types.LatestSignerForChainID(nil) + gasPrice := uint256.NewInt(1_000_000_000) + + c := &pbinWitnessChain{ + m: m, + receiver: common.HexToAddress("0x00000000000000000000000000000000000f0f0f"), + slot: common.HexToHash("0x01"), + otherSlot: common.HexToHash("0x02"), + } + + overflowing := make([]byte, 31*(pbinCodeGroupChunks+8)) + copy(overflowing, pbinStoreRuntime) + + sign := func(txn *types.LegacyTx) types.Transaction { + t.Helper() + txn.GasPrice = *gasPrice + signed, err := types.SignTx(txn, *signer, bankKey) + require.NoError(t, err) + return signed + } + + pack, err := blockgen.GenerateChain(m.ChainConfig, m.Genesis, m.Engine, m.DB, 7, func(i int, b *blockgen.BlockGen) { + nonce := b.TxNonce(bankAddress) + switch i { + case 0: // plain transfer, creating the recipient + b.AddTx(sign(&types.LegacyTx{CommonTx: types.CommonTx{ + Nonce: nonce, To: &c.receiver, GasLimit: 21_000, Value: *uint256.NewInt(1e9), + }})) + case 1: // deploy whose code fits one code-zone group + c.small = types.CreateAddress(bankAddress, nonce) + b.AddTx(sign(&types.LegacyTx{CommonTx: types.CommonTx{ + Nonce: nonce, GasLimit: 200_000, Data: pbinDeployCode(pbinStoreRuntime), + }})) + case 2: // deploy whose code crosses a code-zone group boundary + c.large = types.CreateAddress(bankAddress, nonce) + b.AddTx(sign(&types.LegacyTx{CommonTx: types.CommonTx{ + Nonce: nonce, GasLimit: 4_000_000, Data: pbinDeployCode(overflowing), + }})) + case 3: // SSTORE a non-zero value + b.AddTx(sign(&types.LegacyTx{CommonTx: types.CommonTx{ + Nonce: nonce, To: &c.small, GasLimit: 100_000, Data: pbinStoreCalldata(c.slot, 42), + }})) + case 4: // SSTORE the same slot back to zero + b.AddTx(sign(&types.LegacyTx{CommonTx: types.CommonTx{ + Nonce: nonce, To: &c.small, GasLimit: 100_000, Data: pbinStoreCalldata(c.slot, 0), + }})) + case 5: // call the large contract, so its code is read back across groups + b.AddTx(sign(&types.LegacyTx{CommonTx: types.CommonTx{ + Nonce: nonce, To: &c.large, GasLimit: 200_000, Data: pbinStoreCalldata(c.otherSlot, 7), + }})) + case 6: // no transactions + } + }) + require.NoError(t, err) + require.NoError(t, m.InsertChain(pack)) + c.pack = pack + + // An under-budgeted transaction fails silently as a reverted receipt and would + // leave the shape it was meant to build out of the chain entirely. + for i, receipts := range pack.Receipts { + for _, receipt := range receipts { + require.EqualValues(t, types.ReceiptStatusSuccessful, receipt.Status, + "transaction in block %d failed", i+1) + } + } + requirePBinChainShape(t, c) + return c +} + +// requirePBinChainShape reads back what each block was written to exercise. A +// block whose transaction ran but did something else — a deploy that no longer +// overflows the header, an SSTORE the runtime code silently skipped — would +// still produce a verifying witness, and the corpus would quietly stop covering +// the case it names. +func requirePBinChainShape(t *testing.T, c *pbinWitnessChain) { + t.Helper() + + tx, err := c.m.DB.BeginTemporalRo(t.Context()) + require.NoError(t, err) + defer tx.Rollback() + + // A history reader at block N reads the state block N starts from, so the + // effect of block N-1 is read at N. + stateAt := func(blockNum uint64) *state.IntraBlockState { + reader, err := rpchelper.CreateHistoryStateReader(t.Context(), tx, blockNum, 0, rawdbv3.TxNums) + require.NoError(t, err) + st := state.New(reader) + t.Cleanup(st.Close) + return st + } + + balance, err := stateAt(2).GetBalance(accounts.InternAddress(c.receiver)) + require.NoError(t, err) + require.Equal(t, uint64(1e9), balance.Uint64(), "block 1 transfers to a new account") + + smallCode, err := stateAt(3).GetCode(accounts.InternAddress(c.small)) + require.NoError(t, err) + require.Equal(t, pbinStoreRuntime, smallCode) + require.LessOrEqual(t, len(smallCode), 31*pbinCodeGroupChunks, "block 2's code fits one code-zone group") + + largeCode, err := stateAt(4).GetCode(accounts.InternAddress(c.large)) + require.NoError(t, err) + require.Greater(t, len(largeCode), 31*pbinCodeGroupChunks, "block 3's code must cross a group boundary") + + written, err := stateAt(5).GetState(accounts.InternAddress(c.small), accounts.InternKey(c.slot)) + require.NoError(t, err) + require.Equal(t, uint64(42), written.Uint64(), "block 4 writes the slot") + + cleared, err := stateAt(6).GetState(accounts.InternAddress(c.small), accounts.InternKey(c.slot)) + require.NoError(t, err) + require.True(t, cleared.IsZero(), "block 5 stores the slot back to zero") + + viaLargeCode, err := stateAt(7).GetState(accounts.InternAddress(c.large), accounts.InternKey(c.otherSlot)) + require.NoError(t, err) + require.Equal(t, uint64(7), viaLargeCode.Uint64(), "block 6 runs the overflowing contract's code") +} + +func pbinWitnessAPI(t *testing.T, m *execmoduletester.ExecModuleTester) *DebugAPIImpl { + t.Helper() + enableCommitmentHistoryFlag(t, m.DB) + require.True(t, binCommitmentTrie(), "the chain is committed with the binary trie") + return NewPrivateDebugAPI(newBaseApiForTest(m), m.DB, nil, &rpccfg.DebugApiConfig{}) +} + +// requirePBinWitnessVerifies re-executes the block from the witness alone and +// asserts it reaches the header's post-state root. The build applies the same +// gate before returning, so this repeats it deliberately: the assertion belongs +// in the test rather than only in the code under test. +func requirePBinWitnessVerifies(t *testing.T, c *pbinWitnessChain, result *ExecutionWitnessResult, num uint64) { + t.Helper() + + block := c.block(t, num) + parentRoot := c.block(t, num-1).Root() + require.NoError(t, verifyWitnessAgainstBlock(t.Context(), result, block, parentRoot, + c.m.ChainConfig, c.m.Engine, true /* binTrie */), + "block %d witness must re-execute to %x", num, block.Root()) +} + +func pbinWitnessOf(t *testing.T, api *DebugAPIImpl, num uint64) *ExecutionWitnessResult { + t.Helper() + + bn := rpc.BlockNumber(num) + result, err := api.ExecutionWitness(t.Context(), rpc.BlockNumberOrHash{BlockNumber: &bn}, nil) + require.NoError(t, err, "block %d", num) + require.NotNil(t, result) + return result +} + +// TestPBinExecutionWitnessEndToEnd is the end-to-end gate: over a bin-committed +// chain, every block's witness alone re-executes the block to its post-state root. +func TestPBinExecutionWitnessEndToEnd(t *testing.T) { + // No t.Parallel: mutates process-global commitment flags. + withCommitmentHistory(t) + withBinCommitmentDatadir(t) + + c := buildPBinWitnessChain(t) + api := pbinWitnessAPI(t, c.m) + + for _, tc := range []struct { + num uint64 + name string + }{ + {1, "plain transfer"}, + {2, "deploy within one code-zone group"}, + {3, "deploy crossing a group boundary"}, + {4, "storage write"}, + {5, "SSTORE to zero"}, + {6, "code read across a group boundary"}, + } { + t.Run(tc.name, func(t *testing.T) { + result := pbinWitnessOf(t, api, tc.num) + require.NotEmpty(t, result.State, "a block that touches state proves it with nodes") + require.NotEmpty(t, result.Keys) + requirePBinWitnessVerifies(t, c, result, tc.num) + }) + } +} + +// A block carrying no transactions still pays the block reward, so it has a +// post-state root of its own to prove. +func TestPBinExecutionWitnessEmptyBlock(t *testing.T) { + // No t.Parallel: mutates process-global commitment flags. + withCommitmentHistory(t) + withBinCommitmentDatadir(t) + + c := buildPBinWitnessChain(t) + api := pbinWitnessAPI(t, c.m) + + require.Empty(t, c.block(t, 7).Transactions(), "block 7 is the no-transaction block") + result := pbinWitnessOf(t, api, 7) + require.NotEmpty(t, result.State) + requirePBinWitnessVerifies(t, c, result, 7) +} + +// Each witness has to stand on its own: a block's proof may not lean on nodes +// its neighbour's witness happens to carry. +func TestPBinExecutionWitnessConsecutiveBlocks(t *testing.T) { + // No t.Parallel: mutates process-global commitment flags. + withCommitmentHistory(t) + withBinCommitmentDatadir(t) + + c := buildPBinWitnessChain(t) + api := pbinWitnessAPI(t, c.m) + + first, second := pbinWitnessOf(t, api, 4), pbinWitnessOf(t, api, 5) + requirePBinWitnessVerifies(t, c, first, 4) + requirePBinWitnessVerifies(t, c, second, 5) + require.NotEqual(t, first.State, second.State, + "consecutive blocks over different pre-states cannot prove with the same node set") +} + +// A CREATE over a nonce-0, code-empty account with storage outside the account +// header. EIP-7610 makes that create fail, and the binary tree commits no +// per-account storage root, so a verifier can only reach the same verdict from a +// proof of the account's storage zone — which no leaf of the account's own +// header stem carries. +func TestPBinExecutionWitnessCreateCollisionOnZoneStorage(t *testing.T) { + // No t.Parallel: mutates process-global commitment flags. + withCommitmentHistory(t) + withBinCommitmentDatadir(t) + + bankKey, err := crypto.HexToECDSA("b71c71a67e1177ad4e901695e1b4b9ee17ae16c6668d313eac2f96dbcda3f291") + require.NoError(t, err) + bankAddress := crypto.PubkeyToAddress(bankKey.PublicKey) + victim := types.CreateAddress(bankAddress, 0) + + chainConfig := new(chain.Config) + require.NoError(t, copier.CopyWithOption(chainConfig, chain.TestChainBerlinConfig, copier.Option{DeepCopy: true})) + m := execmoduletester.New(t, execmoduletester.WithGenesisSpec(&types.Genesis{ + Config: chainConfig, + Alloc: types.GenesisAlloc{ + bankAddress: {Balance: big.NewInt(1e18)}, + victim: { + Balance: big.NewInt(1), + Storage: map[common.Hash]common.Hash{pbinStatelessSlot(1 << 20): common.BigToHash(big.NewInt(9))}, + }, + }, + }), execmoduletester.WithKey(bankKey)) + + signer := types.LatestSignerForChainID(nil) + pack, err := blockgen.GenerateChain(m.ChainConfig, m.Genesis, m.Engine, m.DB, 1, func(i int, b *blockgen.BlockGen) { + require.Zero(t, b.TxNonce(bankAddress), "the victim address is derived from nonce 0") + txn, err := types.SignTx(&types.LegacyTx{ + CommonTx: types.CommonTx{GasLimit: 200_000, Data: pbinDeployCode(pbinStoreRuntime)}, + GasPrice: *uint256.NewInt(1_000_000_000), + }, *signer, bankKey) + require.NoError(t, err) + b.AddTx(txn) + }) + require.NoError(t, err) + require.NoError(t, m.InsertChain(pack)) + require.EqualValues(t, types.ReceiptStatusFailed, pack.Receipts[0][0].Status, + "the create must collide; a successful deploy proves nothing about the predicate") + + c := &pbinWitnessChain{m: m, pack: pack} + result := pbinWitnessOf(t, pbinWitnessAPI(t, m), 1) + requirePBinWitnessVerifies(t, c, result, 1) +} diff --git a/rpc/jsonrpc/pbin_witness_granularity_test.go b/rpc/jsonrpc/pbin_witness_granularity_test.go new file mode 100644 index 00000000000..bd04e175a1e --- /dev/null +++ b/rpc/jsonrpc/pbin_witness_granularity_test.go @@ -0,0 +1,262 @@ +package jsonrpc + +// Byte-level accounting for the EEST test_witness_growth corpus, both arms. +// +// Every measured block calls a contract that executes the same 8 bytes; only the +// dead padding behind the STOP differs. Under hex the code ships as one blob +// beside a short account proof; under bin it is committed as 31-byte chunk leaves +// in the code zone, so the same call proves every chunk the contract occupies. +// +// Bin bytes are attributed by reading each leaf's own key: the zone byte and the +// sub-index say what the leaf is, so nothing here is inferred from position. + +import ( + "fmt" + "math/big" + "testing" + + "github.com/holiman/uint256" + "github.com/jinzhu/copier" + "github.com/stretchr/testify/require" + + "github.com/erigontech/erigon/common" + "github.com/erigontech/erigon/common/crypto" + "github.com/erigontech/erigon/execution/chain" + "github.com/erigontech/erigon/execution/execmodule/execmoduletester" + "github.com/erigontech/erigon/execution/tests/blockgen" + "github.com/erigontech/erigon/execution/types" + "github.com/erigontech/erigon/rpc/rpccfg" +) + +// The chunk counts of tests/binary_tree/.../test_witness_growth.py, plus the +// code-zone group boundary at STEM_SUBTREE_WIDTH and a zero-padded tail. +var pbinGranCases = []struct { + name string + size int + chunks int + zeroPad bool +}{ + {"single_chunk", 31, 1, false}, + {"chunks_128", 31 * 128, 128, false}, + {"chunks_129", 31 * 129, 129, false}, + {"group_full", 31 * pbinCodeGroupChunks, 256, false}, + {"group_spill", 31 * (pbinCodeGroupChunks + 1), 257, false}, + {"max_code_size", 24576, 793, false}, + {"max_zero_padded", 24576, 793, true}, +} + +type pbinGranRow struct { + name string + // bin, by what the leaf's own key says it is + basicData, codeHash, codeChunk, storageLeaf int + branches, binNodes, binTotal int + // hex + hexState, hexCodes, hexNodes, hexTotal int + // block headers, measured in the arm they belong to + hexHeaders, binHeaders int + // the block that deploys the contract, against the block that reads it + deployBinNodes, deployBinTotal int + deployHexNodes, deployHexTotal int +} + +// pbinGranChain deploys one contract per case, then calls each in its own block. +// Deploys come first so every measured block is a pure read of pre-existing code. +func pbinGranChain(t *testing.T) (*pbinWitnessChain, []common.Address) { + t.Helper() + // Own genesis rather than fundedBankGenesis: a 24,576-byte code deposit is + // ~5M gas, past the default block limit that helper leaves on a Berlin config. + bankKey, err := crypto.HexToECDSA("b71c71a67e1177ad4e901695e1b4b9ee17ae16c6668d313eac2f96dbcda3f291") + require.NoError(t, err) + bankAddress := crypto.PubkeyToAddress(bankKey.PublicKey) + bankFunds, ok := new(big.Int).SetString("100000000000000000000", 10) + require.True(t, ok) + + chainConfig := new(chain.Config) + require.NoError(t, copier.CopyWithOption(chainConfig, chain.TestChainBerlinConfig, copier.Option{DeepCopy: true})) + m := execmoduletester.New(t, + execmoduletester.WithGenesisSpec(&types.Genesis{ + Config: chainConfig, + Alloc: types.GenesisAlloc{bankAddress: {Balance: bankFunds}}, + GasLimit: 60_000_000, + }), + execmoduletester.WithKey(bankKey)) + + signer := types.LatestSignerForChainID(nil) + gasPrice := uint256.NewInt(1_000_000_000) + addrs := make([]common.Address, len(pbinGranCases)) + + sign := func(txn *types.LegacyTx) types.Transaction { + t.Helper() + txn.GasPrice = *gasPrice + signed, err := types.SignTx(txn, *signer, bankKey) + require.NoError(t, err) + return signed + } + + n := len(pbinGranCases) + pack, err2 := blockgen.GenerateChain(m.ChainConfig, m.Genesis, m.Engine, m.DB, 2*n, func(i int, b *blockgen.BlockGen) { + nonce := b.TxNonce(bankAddress) + if i < n { // deploy + // Padding is INVALID, not zero: a chunk of 31 zero bytes is stored as no + // leaf at all, so zero padding would measure the collapse rather than the + // cost of code. The zeroPad case covers that collapse deliberately. + runtime := make([]byte, pbinGranCases[i].size) + for j := range runtime { + runtime[j] = 0xfe + } + copy(runtime, pbinStoreRuntime) + if pbinGranCases[i].zeroPad { + clear(runtime[len(pbinStoreRuntime):]) + } + addrs[i] = types.CreateAddress(bankAddress, nonce) + b.AddTx(sign(&types.LegacyTx{CommonTx: types.CommonTx{ + Nonce: nonce, GasLimit: 12_000_000, Data: pbinDeployCode(runtime), + }})) + return + } + c := i - n // call + b.AddTx(sign(&types.LegacyTx{CommonTx: types.CommonTx{ + Nonce: nonce, To: &addrs[c], GasLimit: 200_000, + Data: pbinStoreCalldata(common.HexToHash("0x01"), uint64(c+1)), + }})) + }) + require.NoError(t, err2) + require.NoError(t, m.InsertChain(pack)) + return &pbinWitnessChain{m: m, pack: pack}, addrs +} + +func TestPBinWitnessGranularity(t *testing.T) { + withCommitmentHistory(t) + n := len(pbinGranCases) + rows := make([]pbinGranRow, n) + for i := range rows { + rows[i].name = pbinGranCases[i].name + } + + // The relations below are bin against hex, so they hold only once both arms + // have measured; a run filtered to one subtest compares against zeroes. + var hexRan, binRan bool + + // hex arm + t.Run("hex", func(t *testing.T) { + c, _ := pbinGranChain(t) + enableCommitmentHistoryFlag(t, c.m.DB) + api := NewPrivateDebugAPI(newBaseApiForTest(c.m), c.m.DB, nil, &rpccfg.DebugApiConfig{}) + for i := range pbinGranCases { + w := pbinWitnessOf(t, api, uint64(n+i+1)) + rows[i].hexState = sumBytes(w.State) + rows[i].hexNodes = len(w.State) + rows[i].hexCodes = sumBytes(w.Codes) + rows[i].hexHeaders = sumBytes(w.Headers) + rows[i].hexTotal = rows[i].hexState + rows[i].hexCodes + rows[i].hexHeaders + + d := pbinWitnessOf(t, api, uint64(i+1)) + rows[i].deployHexNodes = len(d.State) + rows[i].deployHexTotal = sumBytes(d.State) + sumBytes(d.Codes) + sumBytes(d.Headers) + } + hexRan = true + }) + + // bin arm + t.Run("bin", func(t *testing.T) { + withBinCommitmentDatadir(t) + c, _ := pbinGranChain(t) + enableCommitmentHistoryFlag(t, c.m.DB) + api := NewPrivateDebugAPI(newBaseApiForTest(c.m), c.m.DB, nil, &rpccfg.DebugApiConfig{}) + for i := range pbinGranCases { + w := pbinWitnessOf(t, api, uint64(n+i+1)) + r := &rows[i] + for _, node := range w.State { + r.binNodes++ + r.binTotal += len(node) + key := pbinLeafKeyOf(node) + if key == nil { + r.branches += len(node) + continue + } + switch sub := key[len(key)-1]; { + case key[0] == 0x01: + r.codeChunk += len(node) + case key[0] == 0xFF: + r.storageLeaf += len(node) + case sub == 0: + r.basicData += len(node) + case sub == 1: + r.codeHash += len(node) + case sub == pbinDelegationSubIndex: + r.codeHash += len(node) + default: + // The header window is the only other allocated part of the + // account zone; everything between is reserved. + require.True(t, sub >= 64 && sub < 128, + "%s: account-zone leaf at reserved sub-index %d", r.name, sub) + r.storageLeaf += len(node) + } + } + r.binHeaders = sumBytes(w.Headers) + r.binTotal += r.binHeaders + + d := pbinWitnessOf(t, api, uint64(i+1)) + r.deployBinNodes = len(d.State) + r.deployBinTotal = sumBytes(d.State) + sumBytes(d.Headers) + } + binRan = true + }) + + if !hexRan || !binRan { + t.Log("bin-vs-hex relations need both arms; run the test without a subtest filter") + return + } + + // Direction stated up front: chunk leaves and the branches binding them + // outweigh hex's flat code blob at every size, the gap widens with chunk + // count, and a zero-padded tail collapses to elided leaves that undercut + // the blob. + ratio := map[int]float64{} + for i, gc := range pbinGranCases { + r := &rows[i] + if gc.zeroPad { + require.Less(t, r.binTotal, r.hexTotal, "%s: elided zero chunks must undercut hex", gc.name) + continue + } + require.Greater(t, r.binTotal, r.hexTotal, "%s: chunked code must outweigh hex", gc.name) + ratio[gc.chunks] = float64(r.binTotal) / float64(r.hexTotal) + } + for _, step := range [][2]int{{1, 128}, {128, 256}, {256, 793}} { + require.Greater(t, ratio[step[1]], ratio[step[0]], + "bin/hex must grow from %d to %d chunks", step[0], step[1]) + } + + t.Log("witness bytes for a call executing 8 bytes, by contract size\n" + pbinGranTable(rows)) +} + +func pbinGranTable(rows []pbinGranRow) string { + s := fmt.Sprintf("%-16s %7s %5s %8s %9s %8s | %6s %7s %8s %8s | %7s\n", + "case", "code B", "chunks", "hex tot", "bin tot", "bin/hex", "hexNod", "hexCode", "binNod", "chunkB", "noChunk") + for i := range rows { + r := &rows[i] + noChunk := r.binTotal - r.codeChunk + s += fmt.Sprintf("%-16s %7d %5d %8d %9d %7.2fx | %6d %7d %8d %8d | %7d\n", + r.name, pbinGranCases[i].size, pbinGranCases[i].chunks, + r.hexTotal, r.binTotal, float64(r.binTotal)/float64(r.hexTotal), + r.hexNodes, r.hexCodes, r.binNodes, r.codeChunk, noChunk) + } + s += "\ndeploying the contract against reading it back:\n" + s += fmt.Sprintf("%-16s %8s %8s | %8s %8s | %8s %8s\n", + "case", "depBinN", "depBinB", "readBinN", "readBinB", "depHexB", "readHexB") + for i := range rows { + r := &rows[i] + s += fmt.Sprintf("%-16s %8d %8d | %8d %8d | %8d %8d\n", + r.name, r.deployBinNodes, r.deployBinTotal, r.binNodes, r.binTotal, + r.deployHexTotal, r.hexTotal) + } + s += "\nbin state bytes by what the leaf's key says it is:\n" + s += fmt.Sprintf("%-16s %10s %9s %12s %8s %10s\n", + "case", "BASIC_DATA", "CODE_HASH", "code chunks", "storage", "branches") + for i := range rows { + r := &rows[i] + s += fmt.Sprintf("%-16s %10d %9d %12d %8d %10d\n", + r.name, r.basicData, r.codeHash, r.codeChunk, r.storageLeaf, r.branches) + } + return s +} diff --git a/rpc/jsonrpc/pbin_witness_phases_test.go b/rpc/jsonrpc/pbin_witness_phases_test.go new file mode 100644 index 00000000000..f06d3e093f0 --- /dev/null +++ b/rpc/jsonrpc/pbin_witness_phases_test.go @@ -0,0 +1,137 @@ +// Copyright 2026 The Erigon Authors +// This file is part of Erigon. +// +// Erigon is free software: you can redistribute it and/or modify +// it under the terms of the GNU Lesser General Public License as published by +// the Free Software Foundation, either version 3 of the License, or +// (at your option) any later version. +// +// Erigon is distributed in the hope that it will be useful, +// but WITHOUT ANY WARRANTY; without even the implied warranty of +// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +// GNU Lesser General Public License for more details. +// +// You should have received a copy of the GNU Lesser General Public License +// along with Erigon. If not, see . + +package jsonrpc + +import ( + "errors" + "testing" + + "github.com/stretchr/testify/require" + + "github.com/erigontech/erigon/cmd/rpcdaemon/rpcdaemontest" + "github.com/erigontech/erigon/common" + "github.com/erigontech/erigon/common/hexutil" + "github.com/erigontech/erigon/db/state/statecfg" + "github.com/erigontech/erigon/execution/commitment/trie" + "github.com/erigontech/erigon/rpc" + "github.com/erigontech/erigon/rpc/rpccfg" +) + +// The collapse phase is driven with every dependency nil: if the bin skip ever goes +// away, the phase dereferences one of them instead of returning cleanly. +func TestPBinWitnessSkipsCollapseDetection(t *testing.T) { + t.Parallel() + + var siblingPaths [][]byte + var err error + require.NotPanics(t, func() { + siblingPaths, err = detectCollapseSiblings(t.Context(), nil, nil, nil, nil, + 0, 0, 0, 0, common.Hash{}, nil, witnessModeLegacy, true /* binTrie */) + }, "the binary trie must not enter collapse detection: SetCollapseTracer panics under bin") + require.NoError(t, err) + require.Empty(t, siblingPaths, "the binary trie never collapses a branch") +} + +// A collapse sibling reaching the bin trie phase would be touched as a hashed key with +// an empty plain key, which the bin update stream cannot resolve. The guard runs before +// any dependency is used, so nil deps are enough to reach it. +func TestPBinWitnessTrieRefusesCollapseSiblings(t *testing.T) { + t.Parallel() + + nodes, err := buildWitnessTrie(t.Context(), nil, nil, nil, nil, 0, common.Hash{}, + [][]byte{{0x01, 0x02}}, nil, true /* produceExclusionProofs */, true /* binTrie */) + require.Error(t, err) + require.Nil(t, nodes) + require.Contains(t, err.Error(), "collapse sibling") +} + +func TestPBinWitnessModeRejectsExplicitCanonical(t *testing.T) { + t.Parallel() + + str := func(s string) *string { return &s } + + for _, tc := range []struct { + name string + param *string + }{ + {"absent", nil}, + {"empty", str("")}, + {"legacy", str("legacy")}, + } { + t.Run(tc.name+" mode resolves to legacy under bin", func(t *testing.T) { + got, err := resolveWitnessMode(tc.param, true /* binTrie */) + require.NoError(t, err, "rejecting the legacy default would reject every bin request") + require.Equal(t, witnessModeLegacy, got) + }) + } + + got, err := resolveWitnessMode(str("canonical"), true /* binTrie */) + require.ErrorIs(t, err, errWitnessCanonicalHexOnly) + require.Equal(t, witnessModeLegacy, got) + + got, err = resolveWitnessMode(str("canonical"), false /* binTrie */) + require.NoError(t, err, "hex keeps both modes") + require.Equal(t, witnessModeCanonical, got) +} + +// TestPBinExecutionWitnessRejectsCanonicalRequest pins the wiring: the mode gate reads +// the datadir's variant, so an explicit canonical request under bin is refused, while a +// default-mode request gets past the gate and fails for its own reasons. +func TestPBinExecutionWitnessRejectsCanonicalRequest(t *testing.T) { + // No t.Parallel: mutates process-global statecfg flags. + m, _, _ := rpcdaemontest.CreateTestExecModule(t) + api := NewPrivateDebugAPI(newBaseApiForTest(m), m.DB, nil, &rpccfg.DebugApiConfig{}) + + orig := statecfg.ExperimentalBinCommitment + t.Cleanup(func() { statecfg.ExperimentalBinCommitment = orig }) + statecfg.ExperimentalBinCommitment = true + require.True(t, binCommitmentTrie()) + + canonical := "canonical" + latest := rpc.BlockNumberOrHashWithNumber(rpc.LatestBlockNumber) + _, err := api.ExecutionWitness(t.Context(), latest, &canonical) + require.ErrorIs(t, err, errWitnessCanonicalHexOnly) + + _, err = api.ExecutionWitness(t.Context(), latest, nil) + require.NotErrorIs(t, err, errWitnessCanonicalHexOnly, "the legacy default must get past the mode gate") +} + +// The 0x80 empty storage-trie node is an MPT artifact with no binary-trie counterpart. +func TestPBinWitnessOmitsEmptyStorageNode(t *testing.T) { + t.Parallel() + + accountLeaf := hexutil.Bytes(append([]byte{0xf8, 0x44}, trie.EmptyRoot[:]...)) + nodes := []hexutil.Bytes{accountLeaf} + + hexLegacy := appendLegacyEmptyStorageNode(nodes, witnessModeLegacy, false /* binTrie */) + require.Len(t, hexLegacy, 2) + require.Equal(t, hexutil.Bytes{0x80}, hexLegacy[1]) + + require.Equal(t, nodes, appendLegacyEmptyStorageNode(nodes, witnessModeLegacy, true /* binTrie */), + "the binary trie has no empty storage-trie node") + require.Equal(t, nodes, appendLegacyEmptyStorageNode(nodes, witnessModeCanonical, false /* binTrie */)) +} + +// errWitnessCanonicalHexOnly and errWitnessCanonicalUnavailable both refuse a canonical +// request but for unrelated reasons; a caller distinguishing them must not be able to +// match one with the other. +func TestPBinWitnessCanonicalErrorsAreDistinct(t *testing.T) { + t.Parallel() + + require.False(t, errors.Is(errWitnessCanonicalHexOnly, errWitnessCanonicalUnavailable)) + require.False(t, errors.Is(errWitnessCanonicalUnavailable, errWitnessCanonicalHexOnly)) +} diff --git a/rpc/jsonrpc/pbin_witness_reachable_test.go b/rpc/jsonrpc/pbin_witness_reachable_test.go new file mode 100644 index 00000000000..c9f99c3c563 --- /dev/null +++ b/rpc/jsonrpc/pbin_witness_reachable_test.go @@ -0,0 +1,182 @@ +// Copyright 2026 The Erigon Authors +// This file is part of Erigon. +// +// Erigon is free software: you can redistribute it and/or modify +// it under the terms of the GNU Lesser General Public License as published by +// the Free Software Foundation, either version 3 of the License, or +// (at your option) any later version. +// +// Erigon is distributed in the hope that it will be useful, +// but WITHOUT ANY WARRANTY; without even the implied warranty of +// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +// GNU Lesser General Public License for more details. +// +// You should have received a copy of the GNU Lesser General Public License +// along with Erigon. If not, see . + +package jsonrpc + +import ( + "os" + "path/filepath" + "strings" + "testing" + "time" + + "github.com/stretchr/testify/require" + + "github.com/erigontech/erigon/cmd/rpcdaemon/rpcdaemontest" + "github.com/erigontech/erigon/common/log/v3" + "github.com/erigontech/erigon/db/kv" + "github.com/erigontech/erigon/db/rawdb" + "github.com/erigontech/erigon/db/state/execctx" + "github.com/erigontech/erigon/db/state/statecfg" + "github.com/erigontech/erigon/execution/commitment" + "github.com/erigontech/erigon/node/ethconfig" + "github.com/erigontech/erigon/rpc" + "github.com/erigontech/erigon/rpc/rpccfg" +) + +// The commitment variant and its hash are datadir properties resolved process-wide, +// so a test using this may never run in parallel. +func withBinCommitmentDatadir(t *testing.T) { + t.Helper() + + origBin, origHash, origSuite := statecfg.ExperimentalBinCommitment, statecfg.BinCommitmentHash, commitment.PBinHashSuiteName() + t.Cleanup(func() { + statecfg.ExperimentalBinCommitment = origBin + statecfg.BinCommitmentHash = origHash + require.NoError(t, commitment.SetPBinHashSuite(origSuite)) + }) + statecfg.ExperimentalBinCommitment = true + statecfg.BinCommitmentHash = commitment.PBinHashBlake3 + require.NoError(t, commitment.SetPBinHashSuite(commitment.PBinHashBlake3)) +} + +func withCommitmentHistory(t *testing.T) { + t.Helper() + + previousSchema := statecfg.Schema + t.Cleanup(func() { statecfg.Schema = previousSchema }) + statecfg.EnableHistoricalCommitment() +} + +func enableCommitmentHistoryFlag(t *testing.T, db kv.TemporalRwDB) { + t.Helper() + + require.NoError(t, db.Update(t.Context(), func(tx kv.RwTx) error { + return rawdb.WriteDBCommitmentHistoryEnabled(tx, true) + })) +} + +// TestPBinExecutionWitnessReachable is what Task 10 unblocks: debug_executionWitness +// no longer declares itself hex-only, so a bin datadir reaches the pipeline instead of +// ErrBinCommitmentUnsupported. Under bin the stateless gate is not skippable, so a +// returned witness is one that re-executed the block to its post-state root. +func TestPBinExecutionWitnessReachable(t *testing.T) { + // No t.Parallel: mutates process-global commitment flags. + withCommitmentHistory(t) + withBinCommitmentDatadir(t) + + m, _, _, _ := chainWithDeployedContract(t) + enableCommitmentHistoryFlag(t, m.DB) + require.True(t, binCommitmentTrie(), "the chain above is committed with the binary trie") + + api := NewPrivateDebugAPI(newBaseApiForTest(m), m.DB, nil, &rpccfg.DebugApiConfig{}) + + // Block 2 calls the contract deployed by block 1, so its witness covers an account + // read, a storage write and a code read. + bn := rpc.BlockNumber(2) + result, err := api.ExecutionWitness(t.Context(), rpc.BlockNumberOrHash{BlockNumber: &bn}, nil) + require.NoError(t, err) + require.NotNil(t, result) + require.NotEmpty(t, result.State, "a block that touches state proves it with nodes") + require.NotEmpty(t, result.Keys) +} + +// The witness capture serves the sequential hex trie and the bin trie; the parallel +// trie it cannot serve must still be demoted rather than reaching the capture. +func TestWitnessPathDemotesParallelTrie(t *testing.T) { + // No t.Parallel: mutates process-global commitment flags. + withCommitmentHistory(t) + + m, _, _ := rpcdaemontest.CreateTestExecModule(t) + enableCommitmentHistoryFlag(t, m.DB) + + api := NewPrivateDebugAPI(newBaseApiForTest(m), m.DB, nil, &rpccfg.DebugApiConfig{}) + bn := rpc.BlockNumber(3) + block := rpc.BlockNumberOrHash{BlockNumber: &bn} + + sequential, err := api.ExecutionWitness(t.Context(), block, nil) + require.NoError(t, err) + + orig := statecfg.ExperimentalParallelCommitment + t.Cleanup(func() { statecfg.ExperimentalParallelCommitment = orig }) + statecfg.ExperimentalParallelCommitment = true + + demoted, err := api.ExecutionWitness(t.Context(), block, nil) + require.NoError(t, err, "the parallel trie must be demoted, not handed to the witness capture") + require.Equal(t, sequential.State, demoted.State) +} + +// eth_getWitness recomputes with the hex trie and has no bin implementation, so it +// must keep refusing a bin datadir rather than reading bit-path records as hex ones. +func TestPBinGetWitnessRefusesBin(t *testing.T) { + // No t.Parallel: mutates process-global commitment flags. + withCommitmentHistory(t) + + m, _, _ := rpcdaemontest.CreateTestExecModule(t) + enableCommitmentHistoryFlag(t, m.DB) + + cfg := &rpccfg.EthApiConfig{ + GasCap: 5000000, + FeeCap: ethconfig.Defaults.RPCTxFeeCap, + ReturnDataLimit: 100_000, + MaxGetProofRewindBlockCount: 1, + SubscribeLogsChannelSize: 128, + RpcTxSyncDefaultTimeout: 20 * time.Second, + RpcTxSyncMaxTimeout: 1 * time.Minute, + } + api := NewEthAPI(newBaseApiForTest(m), m.DB, nil, nil, nil, cfg, log.New()) + + // The chain above is built on the hex trie; only the witness call runs under bin. + origBin := statecfg.ExperimentalBinCommitment + t.Cleanup(func() { statecfg.ExperimentalBinCommitment = origBin }) + statecfg.ExperimentalBinCommitment = true + + bn := rpc.BlockNumber(3) + _, err := api.GetWitness(t.Context(), rpc.BlockNumberOrHash{BlockNumber: &bn}) + require.ErrorIs(t, err, execctx.ErrBinCommitmentUnsupported) +} + +// debug_executionWitness is the only caller that stopped declaring itself hex-only. +// The refusal of the rest is a source property — each has to keep passing the option +// whose bin behaviour execctx.TestPBinHexOnlyCommitmentRefusesBin pins — so it is +// checked where it lives rather than by re-deriving every caller's preconditions. +func TestPBinHexOnlyCallersStillRefuse(t *testing.T) { + t.Parallel() + + root := filepath.Join("..", "..") + for _, rel := range []string{ + "rpc/jsonrpc/eth_call.go", // eth_getProof, eth_getWitness + "rpc/jsonrpc/eth_simulation.go", // eth_simulateV1 + "rpc/jsonrpc/receipts/receipts_generator.go", + "rpc/rpchelper/commitment.go", + "db/integrity/commitment_integrity.go", + } { + src, err := os.ReadFile(filepath.Join(root, filepath.FromSlash(rel))) + require.NoError(t, err) + for i, line := range strings.Split(string(src), "\n") { + if !strings.Contains(line, "execctx.NewSharedDomains(") { + continue + } + require.Contains(t, line, "execctx.WithHexCommitmentOnly()", + "%s:%d recomputes with the hex trie and must keep refusing bin", rel, i+1) + } + } + + src, err := os.ReadFile(filepath.Join(root, filepath.FromSlash("rpc/jsonrpc/debug_execution_witness.go"))) + require.NoError(t, err) + require.NotContains(t, string(src), "execctx.WithHexCommitmentOnly()", + "the witness path serves bin through its own collector") +} diff --git a/rpc/jsonrpc/pbin_witness_size_test.go b/rpc/jsonrpc/pbin_witness_size_test.go new file mode 100644 index 00000000000..23d93fcc25d --- /dev/null +++ b/rpc/jsonrpc/pbin_witness_size_test.go @@ -0,0 +1,198 @@ +// Copyright 2026 The Erigon Authors +// This file is part of Erigon. +// +// Erigon is free software: you can redistribute it and/or modify +// it under the terms of the GNU Lesser General Public License as published by +// the Free Software Foundation, either version 3 of the License, or +// (at your option) any later version. +// +// Erigon is distributed in the hope that it will be useful, +// but WITHOUT ANY WARRANTY; without even the implied warranty of +// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +// GNU Lesser General Public License for more details. +// +// You should have received a copy of the GNU Lesser General Public License +// along with Erigon. If not, see . + +package jsonrpc + +import ( + "encoding/json" + "fmt" + "os" + "path/filepath" + "strings" + "testing" + + "github.com/stretchr/testify/require" + + "github.com/erigontech/erigon/common/dbg" + "github.com/erigontech/erigon/common/hexutil" + "github.com/erigontech/erigon/rpc/rpccfg" +) + +// hexWitnessBaselinePath pins the hex arm's measured sizes. Regenerate with +// ERIGON_UPDATE_HEX_WITNESS_BASELINE=true when hex witness output changes on purpose. +var hexWitnessBaselinePath = filepath.Join("testdata", "hex_witness_baseline.json") + +// witnessSizes is one block's witness payload split into the parts that scale +// differently. The split matters because under the binary trie a block's code is +// committed as chunk leaves inside State, so Codes repeats bytes State already +// carries; see totalBytes. +type witnessSizes struct { + Block uint64 `json:"block"` + Shape string `json:"shape"` + Nodes int `json:"nodes"` + StateBytes int `json:"stateBytes"` + Codes int `json:"codes"` + CodeBytes int `json:"codeBytes"` + Headers int `json:"headers"` + HeaderBytes int `json:"headerBytes"` +} + +// totalBytes is what a stateless verifier has to be handed. Under bin the code +// blobs are redundant — the reader reassembles code from the chunk leaves already +// counted in StateBytes — so adding Codes there would count code twice and make +// the two arms incomparable. +func (s witnessSizes) totalBytes(binTrie bool) int { + if binTrie { + return s.StateBytes + s.HeaderBytes + } + return s.StateBytes + s.CodeBytes + s.HeaderBytes +} + +func sumBytes(blobs []hexutil.Bytes) int { + total := 0 + for _, blob := range blobs { + total += len(blob) + } + return total +} + +// pbinWitnessCorpus names what each block of buildPBinWitnessChain exercises, so +// the measured table reads as a size per witness shape rather than per block number. +var pbinWitnessCorpus = []struct { + num uint64 + shape string +}{ + {1, "plain transfer"}, + {2, "deploy within one code-zone group"}, + {3, "deploy crossing a group boundary"}, + {4, "storage write"}, + {5, "SSTORE to zero"}, + {6, "code read across a group boundary"}, + {7, "no transactions"}, +} + +// measureWitnessSizes builds the corpus chain under one commitment variant and +// measures every block's witness. Both arms run with the stateless gate on, so a +// witness that got measured is a witness that re-executed its block to the header's +// post-state root. +func measureWitnessSizes(t *testing.T, binTrie bool) []witnessSizes { + t.Helper() + + t.Setenv("ERIGON_WITNESS_NO_VERIFY", "false") + if binTrie { + withBinCommitmentDatadir(t) + } + require.Equal(t, binTrie, binCommitmentTrie()) + require.False(t, witnessVerifySkipped(binTrie), "a measured witness must be a verified one") + + c := buildPBinWitnessChain(t) + enableCommitmentHistoryFlag(t, c.m.DB) + api := NewPrivateDebugAPI(newBaseApiForTest(c.m), c.m.DB, nil, &rpccfg.DebugApiConfig{}) + + sizes := make([]witnessSizes, 0, len(pbinWitnessCorpus)) + for _, block := range pbinWitnessCorpus { + result := pbinWitnessOf(t, api, block.num) + require.NotEmpty(t, result.State, "block %d touches state", block.num) + sizes = append(sizes, witnessSizes{ + Block: block.num, + Shape: block.shape, + Nodes: len(result.State), + StateBytes: sumBytes(result.State), + Codes: len(result.Codes), + CodeBytes: sumBytes(result.Codes), + Headers: len(result.Headers), + HeaderBytes: sumBytes(result.Headers), + }) + } + return sizes +} + +// requireHexBaseline holds the hex arm to its committed numbers. The bin work must +// leave hex witness output alone, and a golden file makes that checkable here rather +// than by building the same corpus on another branch. +func requireHexBaseline(t *testing.T, sizes []witnessSizes) { + t.Helper() + + encoded, err := json.MarshalIndent(sizes, "", " ") + require.NoError(t, err) + encoded = append(encoded, '\n') + + if dbg.EnvBool("ERIGON_UPDATE_HEX_WITNESS_BASELINE", false) { + require.NoError(t, os.WriteFile(hexWitnessBaselinePath, encoded, 0o644)) + t.Fatalf("rewrote %s; re-run without ERIGON_UPDATE_HEX_WITNESS_BASELINE", hexWitnessBaselinePath) + } + + baseline, err := os.ReadFile(hexWitnessBaselinePath) + require.NoError(t, err) + require.JSONEq(t, string(baseline), string(encoded), + "hex witness sizes moved: the bin witness path must leave the hex one byte-identical") +} + +// witnessSizeTable renders the measured arms as markdown, for the plan's table. +func witnessSizeTable(hexArm, binArm []witnessSizes) string { + var b strings.Builder + b.WriteString("| block | shape | hex nodes | hex state B | bin nodes | bin state B | bin/hex state |\n") + b.WriteString("|---|---|---:|---:|---:|---:|---:|\n") + + var hexTotal, binTotal int + for i, h := range hexArm { + n := binArm[i] + hexTotal += h.totalBytes(false) + binTotal += n.totalBytes(true) + fmt.Fprintf(&b, "| %d | %s | %d | %d | %d | %d | %.2fx |\n", + h.Block, h.Shape, h.Nodes, h.StateBytes, n.Nodes, n.StateBytes, + float64(n.StateBytes)/float64(h.StateBytes)) + } + + b.WriteString("\n| block | hex codes | hex code B | bin codes | bin code B | hex headers B | bin headers B |\n") + b.WriteString("|---|---:|---:|---:|---:|---:|---:|\n") + for i, h := range hexArm { + n := binArm[i] + fmt.Fprintf(&b, "| %d | %d | %d | %d | %d | %d | %d |\n", + h.Block, h.Codes, h.CodeBytes, n.Codes, n.CodeBytes, h.HeaderBytes, n.HeaderBytes) + } + + fmt.Fprintf(&b, "\ncorpus total handed to a verifier: hex %d B, bin %d B (%.2fx)\n", + hexTotal, binTotal, float64(binTotal)/float64(hexTotal)) + return b.String() +} + +// TestWitnessSizeBinVsHex builds one block sequence twice — same genesis, same +// transactions, different commitment trie — and measures both witnesses, so binary +// witness sizes come from real blocks instead of estimates. +func TestWitnessSizeBinVsHex(t *testing.T) { + // No t.Parallel, and the arms run in sequence: the commitment variant and its + // hash suite are process-global, and each arm restores what it set. + withCommitmentHistory(t) + + // Each arm checks itself, so either runs alone; only the joint table needs both. + var hexArm, binArm []witnessSizes + t.Run("hex", func(t *testing.T) { + hexArm = measureWitnessSizes(t, false) + require.Len(t, hexArm, len(pbinWitnessCorpus)) + requireHexBaseline(t, hexArm) + }) + t.Run("bin", func(t *testing.T) { + binArm = measureWitnessSizes(t, true) + require.Len(t, binArm, len(pbinWitnessCorpus)) + }) + + if len(hexArm) != len(pbinWitnessCorpus) || len(binArm) != len(pbinWitnessCorpus) { + t.Log("the bin-vs-hex table needs both arms; run the test without a subtest filter") + return + } + t.Log("witness sizes, bin vs hex:\n" + witnessSizeTable(hexArm, binArm)) +} diff --git a/rpc/jsonrpc/pbin_witness_stateless.go b/rpc/jsonrpc/pbin_witness_stateless.go new file mode 100644 index 00000000000..22b173d8d38 --- /dev/null +++ b/rpc/jsonrpc/pbin_witness_stateless.go @@ -0,0 +1,392 @@ +// Copyright 2026 The Erigon Authors +// This file is part of Erigon. +// +// Erigon is free software: you can redistribute it and/or modify +// it under the terms of the GNU Lesser General Public License as published by +// the Free Software Foundation, either version 3 of the License, or +// (at your option) any later version. +// +// Erigon is distributed in the hope that it will be useful, +// but WITHOUT ANY WARRANTY; without even the implied warranty of +// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +// GNU Lesser General Public License for more details. +// +// You should have received a copy of the GNU Lesser General Public License +// along with Erigon. If not, see . + +package jsonrpc + +import ( + "context" + "errors" + "fmt" + + "github.com/holiman/uint256" + + "github.com/erigontech/erigon/common" + "github.com/erigontech/erigon/common/empty" + "github.com/erigontech/erigon/execution/chain" + "github.com/erigontech/erigon/execution/commitment" + "github.com/erigontech/erigon/execution/protocol/rules" + "github.com/erigontech/erigon/execution/state" + "github.com/erigontech/erigon/execution/types" + "github.com/erigontech/erigon/execution/types/accounts" +) + +// Re-executing a block against a binary witness alone. This is the bin analogue +// of witnessStateless: same StateReader/StateWriter seams, resolving leaves by +// tree key instead of by MPT path, and finalizing through PBinPatriciaHashed +// rather than an in-memory MPT. +// +// Strict resolution is the only mode. Hex makes it a WITNESS_STRICT_VERIFY +// opt-in because an unresolved MPT node is not always a defect; under bin an +// unresolved hash is unambiguous, so a missing node is always an error and never +// an empty read. +// +// Code has one owner: the witness's own leaves — chunks reassembled and checked +// against the CODE_HASH leaf, or the delegation indicator read from its header +// leaf (commitment.PBinWitnessState.Code). result.Codes is not read. The leaves +// are committed by the root and the fold re-chunks every account it touches, so +// the pruned witness carries them wherever the post-state pass needs code; a +// blob list is keyed by code reads, a strictly narrower set. Code a block +// deploys has no pre-state leaves and arrives through UpdateAccountCode, as it +// does under hex. + +// pbinExecBlockStatelessly re-executes the block against the binary witness alone +// and returns the post-state root it reaches. It is the bin arm of the gate +// debug_executionWitness applies before returning a witness; the replay itself is +// shared with hex. parentRoot roots the decode: the node set is not self-rooting. +func pbinExecBlockStatelessly( + ctx context.Context, + result *ExecutionWitnessResult, + block *types.Block, + parentRoot common.Hash, + chainConfig *chain.Config, + engine rules.Engine, +) (postStateRoot common.Hash, stateless *pbinWitnessStateless, err error) { + // Genesis has no transactions but does have pre-allocated accounts, which no + // witness covers. + if block.NumberU64() == 0 { + return block.Root(), nil, nil + } + if len(result.State) == 0 { + return common.Hash{}, nil, errors.New("empty State field in witness") + } + + stateless, err = newPBinWitnessStateless(result, parentRoot) + if err != nil { + return common.Hash{}, nil, err + } + if err = replayBlockOverWitness(result, block, chainConfig, engine, stateless); err != nil { + return common.Hash{}, stateless, err + } + + root, err := stateless.Finalize(ctx) + if err != nil { + return common.Hash{}, stateless, fmt.Errorf("[statelessExec] pbin post-state root failed: %w", err) + } + return root, stateless, nil +} + +type pbinWitnessStateless struct { + state *commitment.PBinWitnessState + + codeUpdates map[common.Address][]byte + accountUpdates map[common.Address]*accounts.Account + storageWrites map[common.Address]map[common.Hash]uint256.Int + deleted map[common.Address]struct{} + + // preimages the witness supplied during re-exec; keys[] must cover these + usedTrieAddrs map[common.Address]struct{} + usedTrieSlots map[common.Hash]struct{} + + trace bool +} + +var ( + _ state.StateReader = (*pbinWitnessStateless)(nil) + _ state.StateWriter = (*pbinWitnessStateless)(nil) +) + +func newPBinWitnessStateless(result *ExecutionWitnessResult, parentRoot common.Hash) (*pbinWitnessStateless, error) { + nodes := make([][]byte, len(result.State)) + for i, node := range result.State { + nodes[i] = node + } + witnessState, err := commitment.PBinNewWitnessState(nodes, parentRoot[:]) + if err != nil { + return nil, fmt.Errorf("failed to decode binary witness: %w", err) + } + return &pbinWitnessStateless{ + state: witnessState, + codeUpdates: make(map[common.Address][]byte), + accountUpdates: make(map[common.Address]*accounts.Account), + storageWrites: make(map[common.Address]map[common.Hash]uint256.Int), + deleted: make(map[common.Address]struct{}), + usedTrieAddrs: make(map[common.Address]struct{}), + usedTrieSlots: make(map[common.Hash]struct{}), + }, nil +} + +func (s *pbinWitnessStateless) SetTrace(trace bool, tracePrefix string) { s.trace = trace } +func (s *pbinWitnessStateless) Trace() bool { return s.trace } +func (s *pbinWitnessStateless) TracePrefix() string { return "" } + +func (s *pbinWitnessStateless) ReadAccountDataForDebug(address accounts.Address) (*accounts.Account, error) { + return s.ReadAccountData(address) +} + +func (s *pbinWitnessStateless) ReadAccountData(address accounts.Address) (*accounts.Account, error) { + addr := address.Value() + if acc, ok := s.accountUpdates[addr]; ok { + return acc, nil + } + if _, ok := s.deleted[addr]; ok { + return nil, nil + } + return s.preStateAccount(addr) +} + +func (s *pbinWitnessStateless) preStateAccount(addr common.Address) (*accounts.Account, error) { + witnessAcc, ok, err := s.state.Account(addr[:]) + if err != nil { + return nil, err + } + if !ok { + return nil, nil + } + s.usedTrieAddrs[addr] = struct{}{} + // The binary tree commits no per-account storage root, so Root stays empty. + acc := &accounts.Account{ + Nonce: witnessAcc.Nonce, + Balance: witnessAcc.Balance, + Root: empty.RootHash, + CodeHash: accounts.InternCodeHash(witnessAcc.CodeHash), + } + return acc, nil +} + +func (s *pbinWitnessStateless) ReadAccountStorage(address accounts.Address, key accounts.StorageKey) (uint256.Int, bool, error) { + addr, slot := address.Value(), key.Value() + if m, ok := s.storageWrites[addr]; ok { + if v, ok := m[slot]; ok { + return v, true, nil + } + } + if _, ok := s.deleted[addr]; ok { + return uint256.Int{}, false, nil + } + value, ok, err := s.state.Storage(addr[:], slot[:]) + if err != nil || !ok { + return uint256.Int{}, false, err + } + s.usedTrieSlots[slot] = struct{}{} + var v uint256.Int + v.SetBytes(value[:]) + return v, !v.IsZero(), nil +} + +func (s *pbinWitnessStateless) ReadAccountCode(address accounts.Address) ([]byte, error) { + addr := address.Value() + if code, ok := s.codeUpdates[addr]; ok { + return code, nil + } + if _, ok := s.deleted[addr]; ok { + return nil, nil + } + code, _, err := s.state.Code(addr[:]) + return code, err +} + +func (s *pbinWitnessStateless) ReadAccountCodeSize(address accounts.Address) (int, error) { + code, err := s.ReadAccountCode(address) + if err != nil { + return 0, err + } + return len(code), nil +} + +func (s *pbinWitnessStateless) ReadAccountIncarnation(address accounts.Address) (uint64, error) { + return 0, nil +} + +// HasStorage answers EIP-7610's CREATE-collision predicate. The binary tree +// commits no per-account storage root, so the witness's own leaves are the +// source: the header slots resolve off the proof path the account's leaves sit +// on, and the storage zone off the probe the builder touches for it (see +// accessedState.pbinStorageProbes). +func (s *pbinWitnessStateless) HasStorage(address accounts.Address) (bool, error) { + addr := address.Value() + if _, ok := s.deleted[addr]; ok { + return false, nil + } + for _, v := range s.storageWrites[addr] { + if !v.IsZero() { + return true, nil + } + } + return s.state.HasStorage(addr[:]), nil +} + +func (s *pbinWitnessStateless) UpdateAccountData(address accounts.Address, original, account *accounts.Account) error { + addr := address.Value() + if account == nil { + s.accountUpdates[addr] = nil + return nil + } + accCopy := new(accounts.Account) + accCopy.Copy(account) + s.accountUpdates[addr] = accCopy + return nil +} + +// DeleteAccount records the removal. The pre-state read is what makes it strict: +// an account whose leaves the witness cannot resolve errors here rather than +// being dropped on a guess. +func (s *pbinWitnessStateless) DeleteAccount(address accounts.Address, original *accounts.Account) error { + addr := address.Value() + if _, err := s.preStateAccount(addr); err != nil { + return err + } + delete(s.accountUpdates, addr) + delete(s.storageWrites, addr) + delete(s.codeUpdates, addr) + s.deleted[addr] = struct{}{} + return nil +} + +func (s *pbinWitnessStateless) UpdateAccountCode(address accounts.Address, incarnation uint64, codeHash accounts.CodeHash, code []byte) error { + addr := address.Value() + s.codeUpdates[addr] = code + if acc, ok := s.accountUpdates[addr]; ok && acc != nil { + acc.CodeHash = codeHash + } + return nil +} + +func (s *pbinWitnessStateless) WriteAccountStorage(address accounts.Address, incarnation uint64, key accounts.StorageKey, original, value uint256.Int) error { + addr, slot := address.Value(), key.Value() + m, ok := s.storageWrites[addr] + if !ok { + m = make(map[common.Hash]uint256.Int) + s.storageWrites[addr] = m + } + m[slot] = value + return nil +} + +// CreateContract un-deletes the address: a create over an account dropped +// earlier in the block puts its leaves back. Pre-state storage under it is the +// one case this cannot express — the chain drops the whole storage prefix here, +// and no plain-key update reaches that subtree without also dropping the header +// the create rewrites. EIP-7610 keeps a create off such an account, so the case +// is refused rather than answered with a root that keeps the leaves. +func (s *pbinWitnessStateless) CreateContract(address accounts.Address) error { + addr := address.Value() + if s.state.HasStorage(addr[:]) { + return fmt.Errorf("create over account %x whose pre-state storage the witness proves", addr) + } + delete(s.deleted, addr) + return nil +} + +// Finalize turns the block's writes into the plain-key updates the commitment +// layer takes and recomputes the root over the witness. +func (s *pbinWitnessStateless) Finalize(ctx context.Context) (common.Hash, error) { + plainKeys, updates, err := s.pendingUpdates() + if err != nil { + return common.Hash{}, err + } + root, err := s.state.Root(ctx, plainKeys, updates) + if err != nil { + return common.Hash{}, err + } + return common.BytesToHash(root), nil +} + +func (s *pbinWitnessStateless) pendingUpdates() (plainKeys [][]byte, updates []commitment.Update, err error) { + // A removal is one update on the address: the engine drops the account's + // header stem and its storage subtree, neither of which the writes enumerate. + // Removals go first so that a write the block made after one merges over it, + // as the same pair merges when the domain layer collects a block's updates: + // DeleteAccount clears the maps below, so anything left in them is later. + for addr := range s.deleted { + plainKeys = append(plainKeys, addr[:]) + updates = append(updates, commitment.Update{Flags: commitment.DeleteUpdate}) + } + for addr, acc := range s.accountUpdates { + if acc == nil { + continue + } + update, err := s.accountUpdate(addr, acc) + if err != nil { + return nil, nil, err + } + plainKeys = append(plainKeys, addr[:]) + updates = append(updates, update) + } + for addr, written := range s.storageWrites { + for slot, value := range written { + update, keep, err := s.storageUpdate(addr, slot, value) + if err != nil { + return nil, nil, err + } + if !keep { + continue + } + key := make([]byte, 0, len(addr)+len(slot)) + key = append(append(key, addr[:]...), slot[:]...) + plainKeys = append(plainKeys, key) + updates = append(updates, update) + } + } + return plainKeys, updates, nil +} + +// accountUpdate carries the code size the BASIC_DATA leaf packs, which the +// account itself does not hold: code deployed in-block comes from the write, and +// unchanged code from the witness. +func (s *pbinWitnessStateless) accountUpdate(addr common.Address, acc *accounts.Account) (commitment.Update, error) { + update := commitment.Update{ + Flags: commitment.NonceUpdate | commitment.BalanceUpdate | commitment.CodeUpdate, + Nonce: acc.Nonce, + Balance: acc.Balance, + CodeHash: acc.CodeHash.Value(), + } + if update.CodeHash == empty.CodeHash { + return update, nil + } + if code, ok := s.codeUpdates[addr]; ok { + update.CodeSize = uint64(len(code)) + s.state.SetCode(addr[:], code) + return update, nil + } + witnessAcc, ok, err := s.state.Account(addr[:]) + if err != nil { + return update, err + } + if !ok || witnessAcc.CodeHash != update.CodeHash { + return update, fmt.Errorf("witness holds no code for account %x with code hash %x", addr, update.CodeHash) + } + update.CodeSize = witnessAcc.CodeSize + return update, nil +} + +// storageUpdate writes a zeroed slot the witness holds, which the fold reads as +// a removal of its leaf. A slot with no leaf to begin with is dropped instead: +// there is nothing to remove, and the walk would prove a key the block never +// reached. +func (s *pbinWitnessStateless) storageUpdate(addr common.Address, slot common.Hash, value uint256.Int) (commitment.Update, bool, error) { + update := commitment.Update{Flags: commitment.StorageUpdate} + if value.IsZero() { + _, ok, err := s.state.Storage(addr[:], slot[:]) + if err != nil || !ok { + return update, false, err + } + return update, true, nil + } + trimmed := value.Bytes() + update.StorageLen = int8(len(trimmed)) + copy(update.Storage[:], trimmed) + return update, true, nil +} diff --git a/rpc/jsonrpc/pbin_witness_stateless_test.go b/rpc/jsonrpc/pbin_witness_stateless_test.go new file mode 100644 index 00000000000..1adbf709c75 --- /dev/null +++ b/rpc/jsonrpc/pbin_witness_stateless_test.go @@ -0,0 +1,811 @@ +// Copyright 2026 The Erigon Authors +// This file is part of Erigon. +// +// Erigon is free software: you can redistribute it and/or modify +// it under the terms of the GNU Lesser General Public License as published by +// the Free Software Foundation, either version 3 of the License, or +// (at your option) any later version. +// +// Erigon is distributed in the hope that it will be useful, +// but WITHOUT ANY WARRANTY; without even the implied warranty of +// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +// GNU Lesser General Public License for more details. +// +// You should have received a copy of the GNU Lesser General Public License +// along with Erigon. If not, see . + +package jsonrpc + +import ( + "bytes" + "context" + "maps" + "slices" + "testing" + + "github.com/holiman/uint256" + "github.com/stretchr/testify/require" + + "github.com/erigontech/erigon/common" + "github.com/erigontech/erigon/common/crypto" + "github.com/erigontech/erigon/common/hexutil" + "github.com/erigontech/erigon/db/kv" + "github.com/erigontech/erigon/execution/chain" + "github.com/erigontech/erigon/execution/commitment" + "github.com/erigontech/erigon/execution/protocol/rules" + "github.com/erigontech/erigon/execution/protocol/rules/ethash" + "github.com/erigontech/erigon/execution/protocol/rules/merge" + "github.com/erigontech/erigon/execution/types" + "github.com/erigontech/erigon/execution/types/accounts" +) + +// pbinStatelessState is the plain-state seam the binary engine reads while a +// witness is being built. It stands in for the domain layer: an absent key reads +// as deleted, exactly as a domain read does. +type pbinStatelessState struct { + branches map[string][]byte + accounts map[common.Address]commitment.Update + storage map[string]commitment.Update + code map[common.Address][]byte +} + +func newPBinStatelessState() *pbinStatelessState { + return &pbinStatelessState{ + branches: make(map[string][]byte), + accounts: make(map[common.Address]commitment.Update), + storage: make(map[string]commitment.Update), + code: make(map[common.Address][]byte), + } +} + +func (s *pbinStatelessState) clone() *pbinStatelessState { + c := newPBinStatelessState() + for k, v := range s.branches { + c.branches[k] = bytes.Clone(v) + } + maps.Copy(c.accounts, s.accounts) + maps.Copy(c.storage, s.storage) + for k, v := range s.code { + c.code[k] = bytes.Clone(v) + } + return c +} + +func (s *pbinStatelessState) Branch(prefix []byte) ([]byte, kv.Step, error) { + return s.branches[string(prefix)], 0, nil +} + +func (s *pbinStatelessState) PutBranch(prefix, data, prevData []byte) error { + s.branches[string(prefix)] = bytes.Clone(data) + return nil +} + +func (s *pbinStatelessState) Account(plainKey []byte) (*commitment.Update, error) { + update, ok := s.accounts[common.BytesToAddress(plainKey)] + if !ok { + return &commitment.Update{Flags: commitment.DeleteUpdate}, nil + } + return &update, nil +} + +func (s *pbinStatelessState) Storage(plainKey []byte) (*commitment.Update, error) { + update, ok := s.storage[string(plainKey)] + if !ok { + return &commitment.Update{Flags: commitment.DeleteUpdate}, nil + } + return &update, nil +} + +func (s *pbinStatelessState) Code(plainKey []byte) ([]byte, error) { + return s.code[common.BytesToAddress(plainKey)], nil +} + +func (s *pbinStatelessState) setAccount(addr common.Address, nonce, balance uint64, code []byte) { + update := commitment.Update{ + Flags: commitment.NonceUpdate | commitment.BalanceUpdate | commitment.CodeUpdate, + Nonce: nonce, + CodeHash: crypto.Keccak256Hash(code), + CodeSize: uint64(len(code)), + } + update.Balance.SetUint64(balance) + s.accounts[addr] = update + if len(code) > 0 { + s.code[addr] = bytes.Clone(code) + } +} + +func (s *pbinStatelessState) dropAccount(addr common.Address) { + delete(s.accounts, addr) + delete(s.code, addr) + for key := range s.storage { + if bytes.HasPrefix([]byte(key), addr[:]) { + delete(s.storage, key) + } + } +} + +func (s *pbinStatelessState) setStorage(addr common.Address, slot common.Hash, value uint64) { + key := string(append(bytes.Clone(addr[:]), slot[:]...)) + if value == 0 { + delete(s.storage, key) + return + } + var v uint256.Int + v.SetUint64(value) + trimmed := v.Bytes() + update := commitment.Update{Flags: commitment.StorageUpdate, StorageLen: int8(len(trimmed))} + copy(update.Storage[:], trimmed) + s.storage[key] = update +} + +// pbinStatelessProcess folds the state the way the domain layer does — ModeDirect, +// so every value comes back through the context rather than the touch. +func pbinStatelessProcess(t *testing.T, state *pbinStatelessState, plainKeys [][]byte) []byte { + t.Helper() + trie, updates := commitment.InitializeTrieAndUpdates(commitment.ModeDirect, t.TempDir(), + commitment.TrieConfig{Variant: commitment.VariantBinPatriciaTrie}) + defer trie.Release() + trie.ResetContext(state) + for _, key := range plainKeys { + updates.TouchPlainKeyDirect(string(key), &commitment.Update{}) + } + root, err := trie.Process(context.Background(), updates, "", nil, commitment.WarmupConfig{}) + require.NoError(t, err) + return bytes.Clone(root) +} + +// pbinStatelessWitness captures the witness of the accessed keys and prunes it to +// their proof paths, which is the node set debug_executionWitness returns. +func pbinStatelessWitness(t *testing.T, state *pbinStatelessState, accessed [][]byte) ([][]byte, []byte) { + t.Helper() + return pbinStatelessWitnessRemoving(t, state, accessed, nil) +} + +// pbinStatelessWitnessRemoving is the same capture for a block that removes +// accounts. The pass reads the parent state, where a removed account still +// looks live, so buildWitnessTrie has to name them — see commitment.PBinWitnessBlock. +func pbinStatelessWitnessRemoving(t *testing.T, state *pbinStatelessState, accessed [][]byte, removed []common.Address) ([][]byte, []byte) { + t.Helper() + trie, updates := commitment.InitializeTrieAndUpdates(commitment.ModeDirect, t.TempDir(), + commitment.TrieConfig{Variant: commitment.VariantBinPatriciaTrie}) + defer trie.Release() + trie.ResetContext(state) + for _, key := range accessed { + updates.TouchPlainKeyDirect(string(key), &commitment.Update{}) + } + capturer, ok := trie.(interface { + Witnesses(ctx context.Context, updates *commitment.Updates, produceExclusionProofs bool, logPrefix string) ([][]byte, [][]byte, []byte, error) + }) + require.True(t, ok, "the binary trie captures no witness") + if len(removed) > 0 { + setter, ok := trie.(interface { + SetWitnessBlock(commitment.PBinWitnessBlock) + }) + require.True(t, ok, "the binary trie takes no witness block") + block := commitment.PBinWitnessBlock{Removed: make(map[string]struct{}, len(removed))} + for _, addr := range removed { + block.Removed[string(addr[:])] = struct{}{} + } + setter.SetWitnessBlock(block) + } + + full, provedKeys, root, err := capturer.Witnesses(context.Background(), updates, false, "") + require.NoError(t, err) + lean, err := commitment.PBinWitnessNodesForKeys(full, root, provedKeys) + require.NoError(t, err) + return lean, bytes.Clone(root) +} + +func pbinStatelessAddr(b byte) common.Address { + var addr common.Address + addr[0], addr[19] = b, b + return addr +} + +func pbinStatelessSlot(n uint64) common.Hash { + var v uint256.Int + v.SetUint64(n) + return common.Hash(v.Bytes32()) +} + +func pbinStatelessSlotBytes(n uint64) []byte { + slot := pbinStatelessSlot(n) + return slot[:] +} + +// pbinStatelessCorpus is the pre-state every test in this file reads: an EOA, a +// contract whose code spans a few chunks, a larger contract spanning many, and +// storage in both the account header and the storage zone. +type pbinStatelessCorpus struct { + state *pbinStatelessState + eoa common.Address + contract common.Address + big common.Address + fresh common.Address + code []byte + bigCode []byte +} + +func pbinStatelessNewCorpus() *pbinStatelessCorpus { + c := &pbinStatelessCorpus{ + state: newPBinStatelessState(), + eoa: pbinStatelessAddr(0x11), + contract: pbinStatelessAddr(0x22), + big: pbinStatelessAddr(0x33), + fresh: pbinStatelessAddr(0x44), + code: bytes.Repeat([]byte{0x60, 0x01}, 100), + bigCode: bytes.Repeat([]byte{0x5b}, 5000), + } + c.state.setAccount(c.eoa, 7, 1_000_000, nil) + c.state.setAccount(c.contract, 1, 500, c.code) + c.state.setAccount(c.big, 1, 900, c.bigCode) + for _, slot := range []uint64{1, 63, 64, 1 << 20} { + c.state.setStorage(c.contract, pbinStatelessSlot(slot), slot+1) + } + return c +} + +// accessed is the key set the block touches: reads and writes both, which is what +// buildWitnessTrie folds over. +func (c *pbinStatelessCorpus) accessed() [][]byte { + keys := [][]byte{c.eoa[:], c.contract[:], c.big[:], c.fresh[:]} + for _, slot := range []uint64{1, 63, 64, 1 << 20, 999} { + s := pbinStatelessSlot(slot) + keys = append(keys, append(bytes.Clone(c.contract[:]), s[:]...)) + } + return keys +} + +func (c *pbinStatelessCorpus) verifier(t *testing.T) (*pbinWitnessStateless, [][]byte, common.Hash) { + t.Helper() + pbinStatelessProcess(t, c.state, c.accessed()) + nodes, root := pbinStatelessWitness(t, c.state, c.accessed()) + return pbinStatelessVerifierOver(t, nodes, root), nodes, common.BytesToHash(root) +} + +func pbinStatelessVerifierOver(t *testing.T, nodes [][]byte, root []byte) *pbinWitnessStateless { + t.Helper() + // Codes stays empty on purpose: under bin the chunk leaves are the code + // source, so every code read here has to come out of State alone. + result := &ExecutionWitnessResult{State: make([]hexutil.Bytes, len(nodes))} + for i, node := range nodes { + result.State[i] = node + } + stateless, err := newPBinWitnessStateless(result, common.BytesToHash(root)) + require.NoError(t, err) + return stateless +} + +// TestPBinWitnessStatelessResolvesAccessedState: the witness alone answers every +// account, slot and code read the block made. +func TestPBinWitnessStatelessResolvesAccessedState(t *testing.T) { + t.Parallel() + + c := pbinStatelessNewCorpus() + stateless, _, _ := c.verifier(t) + + eoa, err := stateless.ReadAccountData(accounts.InternAddress(c.eoa)) + require.NoError(t, err) + require.NotNil(t, eoa) + require.Equal(t, uint64(7), eoa.Nonce) + require.Equal(t, uint64(1_000_000), eoa.Balance.Uint64()) + require.Equal(t, crypto.Keccak256Hash(nil), eoa.CodeHash.Value()) + + contract, err := stateless.ReadAccountData(accounts.InternAddress(c.contract)) + require.NoError(t, err) + require.NotNil(t, contract) + require.Equal(t, crypto.Keccak256Hash(c.code), contract.CodeHash.Value()) + + for _, tc := range []struct { + addr common.Address + want []byte + }{ + {c.eoa, []byte{}}, + {c.contract, c.code}, + {c.big, c.bigCode}, + } { + code, err := stateless.ReadAccountCode(accounts.InternAddress(tc.addr)) + require.NoError(t, err) + require.Equal(t, tc.want, code, "code of %x", tc.addr) + size, err := stateless.ReadAccountCodeSize(accounts.InternAddress(tc.addr)) + require.NoError(t, err) + require.Equal(t, len(tc.want), size) + } + + for _, slot := range []uint64{1, 63, 64, 1 << 20} { + value, ok, err := stateless.ReadAccountStorage(accounts.InternAddress(c.contract), + accounts.InternKey(pbinStatelessSlot(slot))) + require.NoError(t, err) + require.True(t, ok, "slot %d is absent", slot) + require.Equal(t, slot+1, value.Uint64()) + } +} + +// TestPBinWitnessStatelessAbsentResolvesWithoutError: absence is proved by the +// nodes on the way, so it resolves rather than erroring — and an absent read is +// not the same answer as an unresolved one. +func TestPBinWitnessStatelessAbsentResolvesWithoutError(t *testing.T) { + t.Parallel() + + c := pbinStatelessNewCorpus() + stateless, _, _ := c.verifier(t) + + acc, err := stateless.ReadAccountData(accounts.InternAddress(c.fresh)) + require.NoError(t, err) + require.Nil(t, acc) + + value, ok, err := stateless.ReadAccountStorage(accounts.InternAddress(c.contract), + accounts.InternKey(pbinStatelessSlot(999))) + require.NoError(t, err) + require.False(t, ok) + require.True(t, value.IsZero()) + + code, err := stateless.ReadAccountCode(accounts.InternAddress(c.fresh)) + require.NoError(t, err) + require.Empty(t, code) +} + +// TestPBinWitnessStatelessMissingNodeErrors: dropping a node has to make the read +// that needs it fail. An empty read there would hash a wrong subtree into the +// post-state root and report success. +func TestPBinWitnessStatelessMissingNodeErrors(t *testing.T) { + t.Parallel() + + c := pbinStatelessNewCorpus() + _, nodes, root := c.verifier(t) + + require.NotEmpty(t, nodes) + broke := 0 + for drop := range nodes { + trimmed := make([]hexutil.Bytes, 0, len(nodes)-1) + for i, node := range nodes { + if i != drop { + trimmed = append(trimmed, node) + } + } + stateless, err := newPBinWitnessStateless(&ExecutionWitnessResult{State: trimmed}, root) + if err != nil { + broke++ // the root node itself: the decode refuses before any read + continue + } + if pbinStatelessReadsAll(t, stateless, c) != nil { + broke++ + } + } + require.Equal(t, len(nodes), broke, "a node can be dropped without any read noticing") +} + +// pbinStatelessReadsAll replays every read the corpus makes and returns the first +// failure. +func pbinStatelessReadsAll(t *testing.T, s *pbinWitnessStateless, c *pbinStatelessCorpus) error { + t.Helper() + for _, addr := range []common.Address{c.eoa, c.contract, c.big, c.fresh} { + if _, err := s.ReadAccountData(accounts.InternAddress(addr)); err != nil { + return err + } + if _, err := s.ReadAccountCode(accounts.InternAddress(addr)); err != nil { + return err + } + } + for _, slot := range []uint64{1, 63, 64, 1 << 20, 999} { + if _, _, err := s.ReadAccountStorage(accounts.InternAddress(c.contract), + accounts.InternKey(pbinStatelessSlot(slot))); err != nil { + return err + } + } + return nil +} + +// TestPBinWitnessStatelessPostStateRoot is the gate the whole verifier exists +// for: the block's writes replayed over the witness reach the root the same +// writes reach over full state. +func TestPBinWitnessStatelessPostStateRoot(t *testing.T) { + t.Parallel() + + c := pbinStatelessNewCorpus() + stateless, _, parentRoot := c.verifier(t) + + deployed := bytes.Repeat([]byte{0x60, 0x02}, 40) + writes := func(t *testing.T, s *pbinWitnessStateless) { + t.Helper() + eoa := accounts.InternAddress(c.eoa) + acc, err := s.ReadAccountData(eoa) + require.NoError(t, err) + acc.Nonce, acc.Balance = 8, *uint256.NewInt(900_000) + require.NoError(t, s.UpdateAccountData(eoa, nil, acc)) + + contract := accounts.InternAddress(c.contract) + contractAcc, err := s.ReadAccountData(contract) + require.NoError(t, err) + contractAcc.Balance = *uint256.NewInt(600) + require.NoError(t, s.UpdateAccountData(contract, nil, contractAcc)) + require.NoError(t, s.WriteAccountStorage(contract, 0, accounts.InternKey(pbinStatelessSlot(1)), + uint256.Int{}, *uint256.NewInt(0xAB))) + // Zeroing a slot the witness holds removes its leaf; one the witness + // proves absent must not gain a leaf. + require.NoError(t, s.WriteAccountStorage(contract, 0, accounts.InternKey(pbinStatelessSlot(64)), + uint256.Int{}, uint256.Int{})) + require.NoError(t, s.WriteAccountStorage(contract, 0, accounts.InternKey(pbinStatelessSlot(999)), + uint256.Int{}, uint256.Int{})) + + fresh := accounts.InternAddress(c.fresh) + require.NoError(t, s.CreateContract(fresh)) + require.NoError(t, s.UpdateAccountCode(fresh, 0, accounts.InternCodeHash(crypto.Keccak256Hash(deployed)), deployed)) + freshAcc := &accounts.Account{Nonce: 1, CodeHash: accounts.InternCodeHash(crypto.Keccak256Hash(deployed))} + freshAcc.Balance.SetUint64(42) + require.NoError(t, s.UpdateAccountData(fresh, nil, freshAcc)) + } + writes(t, stateless) + + got, err := stateless.Finalize(context.Background()) + require.NoError(t, err) + + full := c.state.clone() + full.setAccount(c.eoa, 8, 900_000, nil) + full.setAccount(c.contract, 1, 600, c.code) + full.setStorage(c.contract, pbinStatelessSlot(1), 0xAB) + full.setStorage(c.contract, pbinStatelessSlot(64), 0) + full.setAccount(c.fresh, 1, 42, deployed) + want := pbinStatelessProcess(t, full, [][]byte{ + c.eoa[:], c.contract[:], c.fresh[:], + append(bytes.Clone(c.contract[:]), pbinStatelessSlotBytes(1)...), + append(bytes.Clone(c.contract[:]), pbinStatelessSlotBytes(64)...), + append(bytes.Clone(c.contract[:]), pbinStatelessSlotBytes(999)...), + }) + + require.Equal(t, common.BytesToHash(want), got) + require.NotEqual(t, parentRoot, got, "the writes do not move the root, so the test proves nothing") +} + +// TestPBinWitnessStatelessRemovesOnTreeAccount: a block that clears an account +// the parent state holds reaches, over the witness alone, the root the domain +// fold reaches over full state. +func TestPBinWitnessStatelessRemovesOnTreeAccount(t *testing.T) { + t.Parallel() + + c := pbinStatelessNewCorpus() + pbinStatelessProcess(t, c.state, c.accessed()) + nodes, root := pbinStatelessWitnessRemoving(t, c.state, c.accessed(), []common.Address{c.contract}) + stateless := pbinStatelessVerifierOver(t, nodes, root) + + require.NoError(t, stateless.DeleteAccount(accounts.InternAddress(c.contract), nil)) + + got, err := stateless.Finalize(context.Background()) + require.NoError(t, err) + + full := c.state.clone() + full.dropAccount(c.contract) + want := pbinStatelessProcess(t, full, [][]byte{c.contract[:]}) + + require.Equal(t, common.BytesToHash(want), got) + require.NotEqual(t, common.BytesToHash(root), got, "the removal does not move the root, so the test proves nothing") +} + +// TestPBinWitnessStatelessHasStorageZoneProbe: the probe slot is what the +// builder touches to bring an account's storage zone into the witness, so a +// zone slot the block never read still answers the CREATE-collision predicate — +// and the probe's own key is not the slot that holds the value. +func TestPBinWitnessStatelessHasStorageZoneProbe(t *testing.T) { + t.Parallel() + + state := newPBinStatelessState() + zoneSlot := pbinStatelessAddr(0x52) + bare := pbinStatelessAddr(0x53) + for _, addr := range []common.Address{zoneSlot, bare} { + state.setAccount(addr, 0, 1, nil) + } + state.setStorage(zoneSlot, pbinStatelessSlot(1<<20), 9) + + both := [][]byte{zoneSlot[:], bare[:]} + pbinStatelessProcess(t, state, append(slices.Clone(both), + append(bytes.Clone(zoneSlot[:]), pbinStatelessSlotBytes(1<<20)...))) + + probe := commitment.PBinStorageZoneProbeSlot() + nodes, root := pbinStatelessWitness(t, state, append(slices.Clone(both), + append(bytes.Clone(zoneSlot[:]), probe[:]...), + append(bytes.Clone(bare[:]), probe[:]...))) + stateless := pbinStatelessVerifierOver(t, nodes, root) + + has, err := stateless.HasStorage(accounts.InternAddress(zoneSlot)) + require.NoError(t, err) + require.True(t, has, "the probe proves the zone occupied even though it names another slot") + + has, err = stateless.HasStorage(accounts.InternAddress(bare)) + require.NoError(t, err) + require.False(t, has, "the probe proves an empty zone empty") +} + +// TestPBinWitnessStatelessHasStorage: EIP-7610's CREATE-collision predicate is +// answered from the leaves, so a pre-state slot the block never wrote still +// counts. The header slots resolve off the account's own proof path; the storage +// zone answers for a witness whose keys walked into it, which is what the +// builder's probe is for, and never reports a neighbour's zone as this +// account's. +func TestPBinWitnessStatelessHasStorage(t *testing.T) { + t.Parallel() + + state := newPBinStatelessState() + headerSlot := pbinStatelessAddr(0x51) + zoneSlot := pbinStatelessAddr(0x52) + bare := pbinStatelessAddr(0x53) + for _, addr := range []common.Address{headerSlot, zoneSlot, bare} { + state.setAccount(addr, 0, 1, nil) + } + state.setStorage(headerSlot, pbinStatelessSlot(3), 9) + state.setStorage(zoneSlot, pbinStatelessSlot(1<<20), 9) + + accounts3 := [][]byte{headerSlot[:], zoneSlot[:], bare[:]} + pbinStatelessProcess(t, state, append(slices.Clone(accounts3), + append(bytes.Clone(headerSlot[:]), pbinStatelessSlotBytes(3)...), + append(bytes.Clone(zoneSlot[:]), pbinStatelessSlotBytes(1<<20)...))) + + // The block reads the three accounts and no slot, which is what a CREATE + // colliding on an address touches. + nodes, root := pbinStatelessWitness(t, state, accounts3) + stateless := pbinStatelessVerifierOver(t, nodes, root) + + has, err := stateless.HasStorage(accounts.InternAddress(headerSlot)) + require.NoError(t, err) + require.True(t, has, "a header slot sits on the account's own proof path") + + has, err = stateless.HasStorage(accounts.InternAddress(bare)) + require.NoError(t, err) + require.False(t, has) + + require.NoError(t, stateless.WriteAccountStorage(accounts.InternAddress(bare), 0, + accounts.InternKey(pbinStatelessSlot(1<<20)), uint256.Int{}, *uint256.NewInt(1))) + has, err = stateless.HasStorage(accounts.InternAddress(bare)) + require.NoError(t, err) + require.True(t, has, "the block's own write counts") + + // A block that does touch the slot puts the storage zone in the witness, and + // the zone answers for the account that owns it and no other. + zoneNodes, zoneRoot := pbinStatelessWitness(t, state, append(slices.Clone(accounts3), + append(bytes.Clone(zoneSlot[:]), pbinStatelessSlotBytes(1<<20)...))) + withZone := pbinStatelessVerifierOver(t, zoneNodes, zoneRoot) + + has, err = withZone.HasStorage(accounts.InternAddress(zoneSlot)) + require.NoError(t, err) + require.True(t, has) + + has, err = withZone.HasStorage(accounts.InternAddress(bare)) + require.NoError(t, err) + require.False(t, has, "a neighbour's zone leaf is not this account's storage") +} + +// TestPBinWitnessStatelessCreateOverStoredAccountRefused: the chain drops an +// address's whole storage prefix on CREATE, which no plain-key update here can +// express. A create over storage the witness proves is refused rather than +// answered with a root that keeps the leaves the chain removed. +func TestPBinWitnessStatelessCreateOverStoredAccountRefused(t *testing.T) { + t.Parallel() + + state := newPBinStatelessState() + stored := pbinStatelessAddr(0x61) + bare := pbinStatelessAddr(0x62) + for _, addr := range []common.Address{stored, bare} { + state.setAccount(addr, 0, 1, nil) + } + state.setStorage(stored, pbinStatelessSlot(3), 9) + + both := [][]byte{stored[:], bare[:]} + pbinStatelessProcess(t, state, append(slices.Clone(both), + append(bytes.Clone(stored[:]), pbinStatelessSlotBytes(3)...))) + + nodes, root := pbinStatelessWitness(t, state, both) + stateless := pbinStatelessVerifierOver(t, nodes, root) + + require.NoError(t, stateless.CreateContract(accounts.InternAddress(bare)), + "a create over an account with no storage is the ordinary case") + + require.NoError(t, stateless.DeleteAccount(accounts.InternAddress(stored), nil)) + require.Error(t, stateless.CreateContract(accounts.InternAddress(stored)), + "an in-block removal does not make the pre-state leaves go away") +} + +// TestPBinWitnessStatelessRemovesAccountCreatedInBlock: an account the witness +// proves absent was created and dropped inside the block, so it leaves no leaf +// behind and the root must not move. +func TestPBinWitnessStatelessRemovesAccountCreatedInBlock(t *testing.T) { + t.Parallel() + + c := pbinStatelessNewCorpus() + stateless, _, parentRoot := c.verifier(t) + + require.NoError(t, stateless.DeleteAccount(accounts.InternAddress(c.fresh), nil)) + + got, err := stateless.Finalize(context.Background()) + require.NoError(t, err) + require.Equal(t, parentRoot, got) +} + +// TestPBinWitnessStatelessRefundsRemovedAccount: FinalizeTx runs per transaction, +// so an account emptied under EIP-161 in one transaction and funded again in a +// later one reaches the writer as DeleteAccount then UpdateAccountData, with no +// CreateContract between them. The later write wins, as it does when the domain +// layer merges the same pair. +func TestPBinWitnessStatelessRefundsRemovedAccount(t *testing.T) { + t.Parallel() + + c := pbinStatelessNewCorpus() + stateless, _, parentRoot := c.verifier(t) + + eoa := accounts.InternAddress(c.eoa) + acc, err := stateless.ReadAccountData(eoa) + require.NoError(t, err) + require.NoError(t, stateless.DeleteAccount(eoa, nil)) + + acc.Nonce, acc.Balance = 0, *uint256.NewInt(555) + require.NoError(t, stateless.UpdateAccountData(eoa, nil, acc)) + + got, err := stateless.Finalize(context.Background()) + require.NoError(t, err) + + full := c.state.clone() + full.setAccount(c.eoa, 0, 555, nil) + want := pbinStatelessProcess(t, full, [][]byte{c.eoa[:]}) + + require.Equal(t, common.BytesToHash(want), got) + require.NotEqual(t, parentRoot, got, "the writes do not move the root, so the test proves nothing") +} + +// pbinVerifyWithdrawalGwei is the only state the gate's test block moves. A +// withdrawal keeps the expected post-state root arithmetic instead of gas +// accounting, while still running the full replay. +const pbinVerifyWithdrawalGwei = 3 + +// pbinVerifyChainConfig is post-merge Shanghai: a PoS header pays no block +// reward, and neither the Cancun beacon-root contract nor the Prague blockhash +// contract exists to be called out of a witness that does not carry it. +func pbinVerifyChainConfig() *chain.Config { + return &chain.Config{ + ChainID: uint256.NewInt(1337), + Rules: chain.EtHashRules, + HomesteadBlock: common.NewUint64(0), + TangerineWhistleBlock: common.NewUint64(0), + SpuriousDragonBlock: common.NewUint64(0), + ByzantiumBlock: common.NewUint64(0), + ConstantinopleBlock: common.NewUint64(0), + PetersburgBlock: common.NewUint64(0), + IstanbulBlock: common.NewUint64(0), + BerlinBlock: common.NewUint64(0), + LondonBlock: common.NewUint64(0), + TerminalTotalDifficulty: uint256.NewInt(0), + TerminalTotalDifficultyPassed: true, + ShanghaiTime: common.NewUint64(0), + Ethash: new(chain.EthashConfig), + } +} + +func pbinVerifyEngine() rules.Engine { return merge.New(ethash.NewFaker()) } + +func pbinVerifyBlock(t *testing.T, postRoot common.Hash, to common.Address) *types.Block { + t.Helper() + header := &types.Header{ + Root: postRoot, + Number: *uint256.NewInt(1), + Difficulty: uint256.Int{}, // PoS: no block reward + GasLimit: 30_000_000, + Time: 1, + BaseFee: uint256.NewInt(7), + } + withdrawals := []*types.Withdrawal{{Index: 0, Validator: 0, Address: to, Amount: pbinVerifyWithdrawalGwei}} + return types.NewBlock(header, nil, nil, nil, withdrawals) +} + +// pbinVerifyGateCase is the corpus of the gate tests: the witness is pruned to +// the one account the block credits, so every node in it is on that account's +// path and no removal can go unnoticed. +type pbinVerifyGateCase struct { + corpus *pbinStatelessCorpus + nodes [][]byte + parentRoot common.Hash + block *types.Block +} + +func pbinVerifyNewGateCase(t *testing.T) *pbinVerifyGateCase { + t.Helper() + c := pbinStatelessNewCorpus() + pbinStatelessProcess(t, c.state, c.accessed()) + nodes, parentRoot := pbinStatelessWitness(t, c.state, [][]byte{c.eoa[:]}) + + credited := c.state.clone() + credited.setAccount(c.eoa, 7, 1_000_000+pbinVerifyWithdrawalGwei*uint64(common.GWei), nil) + postRoot := pbinStatelessProcess(t, credited, [][]byte{c.eoa[:]}) + require.NotEqual(t, parentRoot, postRoot, "the withdrawal does not move the root, so the gate proves nothing") + + return &pbinVerifyGateCase{ + corpus: c, + nodes: nodes, + parentRoot: common.BytesToHash(parentRoot), + block: pbinVerifyBlock(t, common.BytesToHash(postRoot), c.eoa), + } +} + +func (g *pbinVerifyGateCase) result(nodes [][]byte) *ExecutionWitnessResult { + result := &ExecutionWitnessResult{ + State: make([]hexutil.Bytes, len(nodes)), + Keys: []hexutil.Bytes{g.corpus.eoa[:]}, + } + for i, node := range nodes { + result.State[i] = node + } + return result +} + +func (g *pbinVerifyGateCase) verify(result *ExecutionWitnessResult, block *types.Block) error { + return verifyWitnessAgainstBlock(context.Background(), result, block, g.parentRoot, + pbinVerifyChainConfig(), pbinVerifyEngine(), true /* binTrie */) +} + +// TestPBinWitnessVerifyGateAcceptsGoodWitness: the block replayed from the +// witness alone reaches the header's post-state root, so the gate lets it +// through. +func TestPBinWitnessVerifyGateAcceptsGoodWitness(t *testing.T) { + t.Parallel() + + g := pbinVerifyNewGateCase(t) + require.NoError(t, g.verify(g.result(g.nodes), g.block)) +} + +// TestPBinWitnessVerifyGateRejectsWrongRoot: a witness that replays to another +// root is refused, which is what stops it from being returned. +func TestPBinWitnessVerifyGateRejectsWrongRoot(t *testing.T) { + t.Parallel() + + g := pbinVerifyNewGateCase(t) + wrongRoot := pbinVerifyBlock(t, common.HexToHash("0xdead"), g.corpus.eoa) + require.ErrorContains(t, g.verify(g.result(g.nodes), wrongRoot), "state root mismatch") +} + +// TestPBinWitnessVerifyGateRejectsTruncatedWitness: a node the replay reads is +// load-bearing, so dropping it has to fail the gate rather than replay to a root +// that happens to match. The pruner also keeps the sibling hanging off each +// branch on the path, which a block that removes nothing never reads; those are +// the only drops the gate may tolerate, and a binary branch has one of them per +// level, so they cannot outnumber the path itself. +func TestPBinWitnessVerifyGateRejectsTruncatedWitness(t *testing.T) { + t.Parallel() + + g := pbinVerifyNewGateCase(t) + require.NotEmpty(t, g.nodes) + var tolerated, rejected int + for drop := range g.nodes { + trimmed := make([][]byte, 0, len(g.nodes)-1) + for i, node := range g.nodes { + if i != drop { + trimmed = append(trimmed, node) + } + } + if g.verify(g.result(trimmed), g.block) != nil { + rejected++ + continue + } + tolerated++ + } + require.Positive(t, rejected) + require.Less(t, tolerated, rejected, "the gate tolerated more drops than the path has siblings") +} + +// TestPBinWitnessVerifyGateChecksKeys: the gate still refuses a witness whose +// keys[] omits a leaf the re-execution resolved. +func TestPBinWitnessVerifyGateChecksKeys(t *testing.T) { + t.Parallel() + + g := pbinVerifyNewGateCase(t) + result := g.result(g.nodes) + result.Keys = nil + require.ErrorContains(t, g.verify(result, g.block), g.corpus.eoa.Hex()) +} + +// TestWitnessVerifySkippedOnlyUnderHex: ERIGON_WITNESS_NO_VERIFY buys back hex's +// doubled execution cost. Under bin the gate is the only correctness evidence +// there is, so the same variable must not turn it off. +func TestWitnessVerifySkippedOnlyUnderHex(t *testing.T) { + require.False(t, witnessVerifySkipped(false /* binTrie */), "hex verification is off by default") + require.False(t, witnessVerifySkipped(true /* binTrie */), "bin verification is off by default") + + t.Setenv("ERIGON_WITNESS_NO_VERIFY", "true") + require.True(t, witnessVerifySkipped(false /* binTrie */)) + require.False(t, witnessVerifySkipped(true /* binTrie */)) +} diff --git a/rpc/jsonrpc/pbin_witness_whale_test.go b/rpc/jsonrpc/pbin_witness_whale_test.go new file mode 100644 index 00000000000..2b3a5639a02 --- /dev/null +++ b/rpc/jsonrpc/pbin_witness_whale_test.go @@ -0,0 +1,214 @@ +package jsonrpc + +// A 1,000-slot contract read at three depths, hex against bin. +// +// Storage layout, not slot count, is what moves a binary witness: slots below 64 +// sit in the account header under 34-byte keys sharing the account's stem, while +// everything above lands in the storage zone under 66-byte keys, one stem per +// 256-slot group. Mapping slots are keccak images, so they scatter one per group. +// +// The contract SLOADs a countdown of slots so one transaction touches N of them. + +import ( + "fmt" + "math/big" + "testing" + + "github.com/holiman/uint256" + "github.com/jinzhu/copier" + "github.com/stretchr/testify/require" + + "github.com/erigontech/erigon/common" + "github.com/erigontech/erigon/common/crypto" + "github.com/erigontech/erigon/execution/chain" + "github.com/erigontech/erigon/execution/execmodule/execmoduletester" + "github.com/erigontech/erigon/execution/tests/blockgen" + "github.com/erigontech/erigon/execution/types" + "github.com/erigontech/erigon/rpc/rpccfg" +) + +const pbinWhaleSlots = 1000 + +// pbinWhaleReader builds runtime that reads `n` slots. Sequential walks slot +// numbers directly; mapping hashes each index first, which is what puts every +// slot in its own storage-zone group. +func pbinWhaleReader(n int, mapping bool) []byte { + code := []byte{0x61, byte(n >> 8), byte(n), 0x5b, 0x80} // PUSH2 n; JUMPDEST; DUP1 + if mapping { + code = append(code, + 0x60, 0x00, 0x52, // PUSH1 0; MSTORE -> mem[0] = i + 0x60, 0x20, 0x60, 0x00, 0x20, // PUSH1 32; PUSH1 0; SHA3 -> keccak(i) + ) + } + return append(code, + 0x54, 0x50, // SLOAD; POP + 0x60, 0x01, 0x90, 0x03, // PUSH1 1; SWAP1; SUB -> i-1 + 0x80, 0x60, 0x03, 0x57, // DUP1; PUSH1 3; JUMPI + 0x00, // STOP + ) +} + +func pbinWhaleSlotKey(i int, mapping bool) common.Hash { + if !mapping { + return common.BigToHash(big.NewInt(int64(i))) + } + var buf [32]byte + big.NewInt(int64(i)).FillBytes(buf[:]) + return crypto.Keccak256Hash(buf[:]) +} + +type pbinWhaleRow struct { + layout string + touched int + hexNodes, hexState, hexTotal int + binNodes, binTotal int + binLeaf, binBranch int + binHdr, binZone int +} + +var pbinWhaleCases = []struct { + layout string + mapping bool + touch int +}{ + {"sequential", false, 8}, + {"sequential", false, 64}, + {"sequential", false, pbinWhaleSlots}, + {"mapping", true, 8}, + {"mapping", true, 64}, + {"mapping", true, pbinWhaleSlots}, +} + +// pbinWhaleChain allocates both contracts with 1,000 slots at genesis, then reads +// each depth in its own block so every measured witness is a pure read. +func pbinWhaleChain(t *testing.T) *pbinWitnessChain { + t.Helper() + bankKey, err := crypto.HexToECDSA("b71c71a67e1177ad4e901695e1b4b9ee17ae16c6668d313eac2f96dbcda3f291") + require.NoError(t, err) + bank := crypto.PubkeyToAddress(bankKey.PublicKey) + funds, ok := new(big.Int).SetString("100000000000000000000", 10) + require.True(t, ok) + + alloc := types.GenesisAlloc{bank: {Balance: funds}} + addrs := map[bool]map[int]common.Address{false: {}, true: {}} + for _, mapping := range []bool{false, true} { + storage := make(map[common.Hash]common.Hash, pbinWhaleSlots) + for i := 1; i <= pbinWhaleSlots; i++ { + storage[pbinWhaleSlotKey(i, mapping)] = common.BigToHash(big.NewInt(int64(i))) + } + for _, c := range pbinWhaleCases { + if c.mapping != mapping { + continue + } + a := common.BigToAddress(big.NewInt(int64(0x9000 + len(alloc)))) + alloc[a] = types.GenesisAccount{ + Balance: big.NewInt(1), + Code: pbinWhaleReader(c.touch, mapping), + Storage: storage, + } + addrs[mapping][c.touch] = a + } + } + + chainConfig := new(chain.Config) + require.NoError(t, copier.CopyWithOption(chainConfig, chain.TestChainBerlinConfig, copier.Option{DeepCopy: true})) + m := execmoduletester.New(t, + execmoduletester.WithGenesisSpec(&types.Genesis{ + Config: chainConfig, Alloc: alloc, GasLimit: 60_000_000, + }), + execmoduletester.WithKey(bankKey)) + + signer := types.LatestSignerForChainID(nil) + gasPrice := uint256.NewInt(1_000_000_000) + pack, err := blockgen.GenerateChain(m.ChainConfig, m.Genesis, m.Engine, m.DB, len(pbinWhaleCases), + func(i int, b *blockgen.BlockGen) { + c := pbinWhaleCases[i] + to := addrs[c.mapping][c.touch] + txn := &types.LegacyTx{CommonTx: types.CommonTx{ + Nonce: b.TxNonce(bank), To: &to, GasLimit: 30_000_000, + }} + txn.GasPrice = *gasPrice + signed, err := types.SignTx(txn, *signer, bankKey) + require.NoError(t, err) + b.AddTx(signed) + }) + require.NoError(t, err) + require.NoError(t, m.InsertChain(pack)) + return &pbinWitnessChain{m: m, pack: pack} +} + +func TestPBinWhaleWitness(t *testing.T) { + withCommitmentHistory(t) + rows := make([]pbinWhaleRow, len(pbinWhaleCases)) + for i, c := range pbinWhaleCases { + rows[i].layout, rows[i].touched = c.layout, c.touch + } + + t.Run("hex", func(t *testing.T) { + c := pbinWhaleChain(t) + enableCommitmentHistoryFlag(t, c.m.DB) + api := NewPrivateDebugAPI(newBaseApiForTest(c.m), c.m.DB, nil, &rpccfg.DebugApiConfig{}) + for i := range pbinWhaleCases { + w := pbinWitnessOf(t, api, uint64(i+1)) + rows[i].hexNodes = len(w.State) + rows[i].hexState = sumBytes(w.State) + rows[i].hexTotal = rows[i].hexState + sumBytes(w.Codes) + sumBytes(w.Headers) + } + }) + + t.Run("bin", func(t *testing.T) { + withBinCommitmentDatadir(t) + c := pbinWhaleChain(t) + enableCommitmentHistoryFlag(t, c.m.DB) + api := NewPrivateDebugAPI(newBaseApiForTest(c.m), c.m.DB, nil, &rpccfg.DebugApiConfig{}) + for i := range pbinWhaleCases { + w := pbinWitnessOf(t, api, uint64(i+1)) + r := &rows[i] + r.binNodes = len(w.State) + for _, n := range w.State { + r.binTotal += len(n) + k := pbinLeafKeyOf(n) + if k == nil { + r.binBranch += len(n) + continue + } + r.binLeaf += len(n) + switch sub := k[len(k)-1]; { + case k[0] == 0xFF: + r.binZone++ + case k[0] == 0x00 && sub >= 64 && sub < 128: + r.binHdr++ + } + } + r.binTotal += sumBytes(w.Headers) + } + // The property the table exists to show: a slot's number, not its count, + // decides the zone. Slots under 64 sit in the account's header window; + // everything else, and every keccak-mapped slot, gets its own storage-zone + // group. + for i, c := range pbinWhaleCases { + r := &rows[i] + if c.mapping { + require.Zero(t, r.binHdr, "%s/%d: a mapped slot cannot reach the header window", c.layout, c.touch) + require.NotZero(t, r.binZone, "%s/%d: mapped slots must land in the storage zone", c.layout, c.touch) + continue + } + require.NotZero(t, r.binHdr, "%s/%d: slots under 64 must land in the header window", c.layout, c.touch) + if c.touch < 64 { + require.Zero(t, r.binZone, "%s/%d: no slot reaches the storage zone", c.layout, c.touch) + } else { + require.NotZero(t, r.binZone, "%s/%d: slots from 64 up must land in the storage zone", c.layout, c.touch) + } + } + }) + + out := fmt.Sprintf("%d slots stored; one block per read depth\n", pbinWhaleSlots) + out += fmt.Sprintf("%-11s %6s | %7s %9s | %7s %9s %8s | %9s %9s %6s %6s\n", + "layout", "touch", "hexNod", "hex tot", "binNod", "bin tot", "bin/hex", "binLeafB", "binBrB", "hdr", "zone") + for _, r := range rows { + out += fmt.Sprintf("%-11s %6d | %7d %9d | %7d %9d %7.2fx | %9d %9d %6d %6d\n", + r.layout, r.touched, r.hexNodes, r.hexTotal, r.binNodes, r.binTotal, + float64(r.binTotal)/float64(max(r.hexTotal, 1)), r.binLeaf, r.binBranch, r.binHdr, r.binZone) + } + t.Log(out) +} diff --git a/rpc/jsonrpc/testdata/hex_witness_baseline.json b/rpc/jsonrpc/testdata/hex_witness_baseline.json new file mode 100644 index 00000000000..304c3b5c138 --- /dev/null +++ b/rpc/jsonrpc/testdata/hex_witness_baseline.json @@ -0,0 +1,72 @@ +[ + { + "block": 1, + "shape": "plain transfer", + "nodes": 2, + "stateBytes": 118, + "codes": 1, + "codeBytes": 0, + "headers": 1, + "headerBytes": 502 + }, + { + "block": 2, + "shape": "deploy within one code-zone group", + "nodes": 4, + "stateBytes": 347, + "codes": 2, + "codeBytes": 8, + "headers": 1, + "headerBytes": 504 + }, + { + "block": 3, + "shape": "deploy crossing a group boundary", + "nodes": 4, + "stateBytes": 379, + "codes": 2, + "codeBytes": 8184, + "headers": 1, + "headerBytes": 504 + }, + { + "block": 4, + "shape": "storage write", + "nodes": 5, + "stateBytes": 518, + "codes": 2, + "codeBytes": 8, + "headers": 1, + "headerBytes": 505 + }, + { + "block": 5, + "shape": "SSTORE to zero", + "nodes": 6, + "stateBytes": 554, + "codes": 2, + "codeBytes": 8, + "headers": 1, + "headerBytes": 504 + }, + { + "block": 6, + "shape": "code read across a group boundary", + "nodes": 5, + "stateBytes": 518, + "codes": 2, + "codeBytes": 8184, + "headers": 1, + "headerBytes": 504 + }, + { + "block": 7, + "shape": "no transactions", + "nodes": 3, + "stateBytes": 295, + "codes": 1, + "codeBytes": 0, + "headers": 1, + "headerBytes": 504 + } +] From 40c25bcc46bbf88190aa32f4462a61d88e5ce4b3 Mon Sep 17 00:00:00 2001 From: awskii Date: Mon, 10 Aug 2026 21:16:03 +0700 Subject: [PATCH 51/56] execution: scope EIP-8038's revised gas schedule to the config that opts in The revised state-access schedule was applied globally, so every Amsterdam fixture in the pinned spec-test corpora diverged on gas used and, through it, on the state and BAL roots: 13073 of 24041 zkevm sub-tests, all six devnet spec shards, and the execution/vm gas tests. Keep the pre-revision constants as the default and put the revised set behind Rules.EIP8038Revised, which only Forks[BinaryTree] sets: its fixtures track head-of-spec, the pinned corpora do not. No scheduled network reaches it. CreateAccessEIP2780 now derives from CreateAccessEIP8038 instead of repeating the number; the two had already drifted apart. tests-zkevm@v0.6.2: 24041 ran, 0 failed. --- execution/chain/chain_config.go | 6 ++++ execution/protocol/mdgas/intrinsic_gas.go | 11 +++++-- execution/protocol/params/protocol.go | 35 +++++++++++++------- execution/protocol/txn_executor.go | 10 ++++-- execution/tests/testforks/forks.go | 7 ++-- execution/vm/eips.go | 6 ++++ execution/vm/evmtypes/rules.go | 1 + execution/vm/gas_table.go | 22 +++++++++++++ execution/vm/gas_table_test.go | 39 +++++++++++++++++++++++ execution/vm/interpreter.go | 2 ++ execution/vm/jump_table.go | 8 +++++ execution/vm/operations_acl.go | 6 ++-- 12 files changed, 133 insertions(+), 20 deletions(-) diff --git a/execution/chain/chain_config.go b/execution/chain/chain_config.go index 23b5d2834a4..8bb16d7cfe7 100644 --- a/execution/chain/chain_config.go +++ b/execution/chain/chain_config.go @@ -84,6 +84,11 @@ type Config struct { OsakaTime *uint64 `json:"osakaTime,omitempty"` AmsterdamTime *uint64 `json:"amsterdamTime,omitempty"` + // EIP8038Revised charges EIP-8038's revised state-access schedule instead of the + // one the pinned spec-test corpora were generated against. Experimental forks that + // track head-of-spec set it; no scheduled network does. + EIP8038Revised bool `json:"eip8038Revised,omitempty"` + // Optional EIP-4844 parameters (see also EIP-7691, EIP-7840, EIP-7892) MinBlobGasPrice *uint64 `json:"minBlobGasPrice,omitempty"` BlobSchedule map[string]*params.BlobConfig `json:"blobSchedule,omitempty"` @@ -871,6 +876,7 @@ type Rules struct { IsIstanbul, IsBerlin, IsLondon, IsShanghai bool IsCancun, IsNapoli, IsAhmedabad, IsBhilai bool IsPrague, IsOsaka, IsAmsterdam bool + EIP8038Revised bool DisabledEIPs []int IsAura bool diff --git a/execution/protocol/mdgas/intrinsic_gas.go b/execution/protocol/mdgas/intrinsic_gas.go index 8ffa6a6baf5..050082b68ae 100644 --- a/execution/protocol/mdgas/intrinsic_gas.go +++ b/execution/protocol/mdgas/intrinsic_gas.go @@ -41,6 +41,7 @@ type IntrinsicGasCalcArgs struct { IsEIP7981 bool IsEIP2780 bool IsAATxn bool + IsEIP8038Revised bool } type IntrinsicGasCalcResult struct { @@ -145,6 +146,9 @@ func CalcIntrinsicGas(args IntrinsicGasCalcArgs) (IntrinsicGasCalcResult, bool) if args.IsEIP2780 { addressGas = params.TxAccessListAddressGasEIP8038 storageKeyGas = params.TxAccessListStorageKeyGasEIP8038 + if args.IsEIP8038Revised { + storageKeyGas = params.TxAccessListStorageKeyGasEIP8038Revised + } } else { addressGas = params.TxAccessListAddressGas storageKeyGas = params.TxAccessListStorageKeyGas @@ -268,9 +272,12 @@ func CalcIntrinsicGas(args IntrinsicGasCalcArgs) (IntrinsicGasCalcResult, bool) // Add the cost of authorizations var perAuthCost uint64 if args.IsEIP2780 { - if args.IsAATxn { + switch { + case args.IsAATxn && args.IsEIP8038Revised: + perAuthCost = params.PerAuthExecutionCostEIP8038Revised + case args.IsAATxn: perAuthCost = params.PerAuthExecutionCostEIP8038 - } else { + default: perAuthCost = params.ExecutionPerAuthBaseCostEIP8038 } } else { diff --git a/execution/protocol/params/protocol.go b/execution/protocol/params/protocol.go index 35c71c2a135..94da5a359ee 100644 --- a/execution/protocol/params/protocol.go +++ b/execution/protocol/params/protocol.go @@ -237,27 +237,40 @@ const ( // costs and adds the execution-gas write components (ACCOUNT_WRITE, STORAGE_WRITE) // that the EIP-8037 state-gas model is charged alongside. ColdAccountAccessCostEIP8038 = uint64(3000) // COLD_ACCOUNT_ACCESS (EIP-2929: 2600) - ColdStorageAccessCostEIP8038 = uint64(2100) // COLD_STORAGE_ACCESS (EIP-2929 cold SLOAD: 2100) - AccountWriteCostEIP8038 = uint64(9000) // ACCOUNT_WRITE: account balance-leaf write + ColdStorageAccessCostEIP8038 = uint64(3000) // COLD_STORAGE_ACCESS (EIP-2929 cold SLOAD: 2100) + AccountWriteCostEIP8038 = uint64(8000) // ACCOUNT_WRITE: account balance-leaf write StorageWriteCostEIP8038 = uint64(10000) // STORAGE_WRITE: first write to a slot in the txn - CallValueTransferGasEIP8038 = AccountWriteCostEIP8038 + CallStipend // CALL_VALUE = 11300 - CreateAccessEIP8038 = AccountWriteCostEIP8038 + ColdAccountAccessCostEIP8038 // CREATE_ACCESS = 12000 - SstoreClearsScheduleRefundEIP8038 = uint64(11616) // REFUND_STORAGE_CLEAR = (STORAGE_WRITE+COLD_STORAGE_ACCESS)*4800/5000 + CallValueTransferGasEIP8038 = AccountWriteCostEIP8038 + CallStipend // CALL_VALUE = 10300 + CreateAccessEIP8038 = AccountWriteCostEIP8038 + ColdStorageAccessCostEIP8038 // CREATE_ACCESS = 11000 + SstoreClearsScheduleRefundEIP8038 = uint64(12480) // REFUND_STORAGE_CLEAR = (STORAGE_WRITE+COLD_STORAGE_ACCESS)*4800/5000 TxAccessListAddressGasEIP8038 = ColdAccountAccessCostEIP8038 // ACCESS_LIST_ADDRESS_COST TxAccessListStorageKeyGasEIP8038 = ColdStorageAccessCostEIP8038 // ACCESS_LIST_STORAGE_KEY_COST ExtCodeWarmAccessGasEIP8038 = 2 * WarmStorageReadCostEIP2929 // EXTCODESIZE/EXTCODECOPY: account access + second read for the code // EXECUTION_PER_AUTH_BASE_COST = 101 auth-tuple bytes * 16 + ECRECOVER + COLD_ACCOUNT_ACCESS + 2*WARM_ACCESS = 7816 ExecutionPerAuthBaseCostEIP8038 = 101*TxDataNonZeroGasEIP2028 + EcrecoverGas + ColdAccountAccessCostEIP8038 + 2*WarmStorageReadCostEIP2929 - // PER_AUTH execution intrinsic = ACCOUNT_WRITE + EXECUTION_PER_AUTH_BASE_COST = 15816 + // PER_AUTH execution intrinsic = ACCOUNT_WRITE + EXECUTION_PER_AUTH_BASE_COST = 14816 PerAuthExecutionCostEIP8038 = AccountWriteCostEIP8038 + ExecutionPerAuthBaseCostEIP8038 + // Revised EIP-8038 schedule, selected by Rules.EIP8038Revised. The constants + // above stay on the values the pinned spec-test corpora were generated against, + // so only a chain config that opts in charges the revised ones. COLD_ACCOUNT_ACCESS, + // STORAGE_WRITE, ACCESS_LIST_ADDRESS_COST and EXTCODE warm access are unchanged + // by the revision and have no counterpart here. + ColdStorageAccessCostEIP8038Revised = uint64(2100) // COLD_STORAGE_ACCESS + AccountWriteCostEIP8038Revised = uint64(9000) // ACCOUNT_WRITE + CallValueTransferGasEIP8038Revised = AccountWriteCostEIP8038Revised + CallStipend // CALL_VALUE = 11300 + CreateAccessEIP8038Revised = AccountWriteCostEIP8038Revised + ColdAccountAccessCostEIP8038 // CREATE_ACCESS = 12000 + SstoreClearsScheduleRefundEIP8038Revised = (StorageWriteCostEIP8038 + ColdStorageAccessCostEIP8038Revised) * 4800 / 5000 // REFUND_STORAGE_CLEAR = 11616 + TxAccessListStorageKeyGasEIP8038Revised = ColdStorageAccessCostEIP8038Revised // ACCESS_LIST_STORAGE_KEY_COST + PerAuthExecutionCostEIP8038Revised = AccountWriteCostEIP8038Revised + ExecutionPerAuthBaseCostEIP8038 + // EIP-2780: Reduce intrinsic transaction gas (resource-based decomposition). // COLD_ACCOUNT_ACCESS and CREATE_ACCESS take their values from EIP-8038. - TxBaseEIP2780 uint64 = 12_000 // TX_BASE: sender ECDSA recovery plus access and write - TxValueCostEIP2780 uint64 = 4_244 // TX_VALUE_COST: recipient balance write for value transfers - TransferLogCostEIP2780 uint64 = 1_756 // TRANSFER_LOG_COST: EIP-7708 transfer log - ColdAccountAccessEIP2780 uint64 = 3_000 // COLD_ACCOUNT_ACCESS: recipient account touch - CreateAccessEIP2780 uint64 = 11_000 // CREATE_ACCESS: ACCOUNT_WRITE(8000) + COLD_STORAGE_ACCESS(3000) + TxBaseEIP2780 uint64 = 12_000 // TX_BASE: sender ECDSA recovery plus access and write + TxValueCostEIP2780 uint64 = 4_244 // TX_VALUE_COST: recipient balance write for value transfers + TransferLogCostEIP2780 uint64 = 1_756 // TRANSFER_LOG_COST: EIP-7708 transfer log + ColdAccountAccessEIP2780 uint64 = 3_000 // COLD_ACCOUNT_ACCESS: recipient account touch + CreateAccessEIP2780 uint64 = CreateAccessEIP8038 // CREATE_ACCESS shares EIP-8038's value; deriving keeps the two from drifting ) // EIP-7702: Set EOA account code diff --git a/execution/protocol/txn_executor.go b/execution/protocol/txn_executor.go index b2d485ee680..81b334e9ec2 100644 --- a/execution/protocol/txn_executor.go +++ b/execution/protocol/txn_executor.go @@ -856,7 +856,8 @@ func (st *TxnExecutor) verifyAuthorities(auths []types.Authorization, chainID *u if auths == nil { return gasRemaining, gasUsed, nil } - isAmsterdam := st.evm.ChainRules().IsAmsterdam + rules := st.evm.ChainRules() + isAmsterdam := rules.IsAmsterdam writtenAccounts := map[accounts.Address]struct{}{st.msg.From(): {}} if !st.msg.Value().IsZero() { writtenAccounts[st.msg.To()] = struct{}{} @@ -920,7 +921,11 @@ func (st *TxnExecutor) verifyAuthorities(auths []types.Authorization, chainID *u return gasRemaining, gasUsed, vm.ErrRuntimeOutOfGas } if _, written := writtenAccounts[authority]; !written { - if !mdgas.Consume(&gasRemaining, &gasUsed, params.AccountWriteCostEIP8038, mdgas.ExecutionGas) { + accountWrite := params.AccountWriteCostEIP8038 + if rules.EIP8038Revised { + accountWrite = params.AccountWriteCostEIP8038Revised + } + if !mdgas.Consume(&gasRemaining, &gasUsed, accountWrite, mdgas.ExecutionGas) { return gasRemaining, gasUsed, vm.ErrRuntimeOutOfGas } writtenAccounts[authority] = struct{}{} @@ -989,5 +994,6 @@ func (st *TxnExecutor) calcIntrinsicGas(contractCreation bool, auths []types.Aut IsEIP7976: rules.IsAmsterdam, IsEIP7981: rules.IsAmsterdam, IsEIP2780: rules.IsAmsterdam, + IsEIP8038Revised: rules.EIP8038Revised, }) } diff --git a/execution/tests/testforks/forks.go b/execution/tests/testforks/forks.go index c66a3173cb5..b266b10befb 100644 --- a/execution/tests/testforks/forks.go +++ b/execution/tests/testforks/forks.go @@ -225,9 +225,12 @@ func init() { Forks["Amsterdam"] = cAms // BinaryTree is Amsterdam with state committed through EIP-8297's binary tree - // instead of the MPT. The fork rules are identical; only the commitment engine - // differs, and the runner selects it from the network name. + // instead of the MPT, and the runner selects it from the network name. Its + // fixtures are generated from head-of-spec rather than a pinned release, so it + // also charges EIP-8038's revised state-access schedule; Amsterdam stays on the + // pre-revision one its pinned corpora were generated against. Forks[BinaryTree] = configCopy(cAms) + Forks[BinaryTree].EIP8038Revised = true // BPO3/BPO4 continue from BPO2 as a separate chain c = configCopy(c) diff --git a/execution/vm/eips.go b/execution/vm/eips.go index 4d61cbc99f0..f14f0f3c634 100644 --- a/execution/vm/eips.go +++ b/execution/vm/eips.go @@ -391,3 +391,9 @@ func enable8038(jt *JumpTable) { jt[CREATE].constantGas = params.CreateAccessEIP8038 jt[CREATE2].constantGas = params.CreateAccessEIP8038 } + +// enable8038Revised repoints the opcodes whose EIP-8038 cost the revision moved. +func enable8038Revised(jt *JumpTable) { + jt[CREATE].constantGas = params.CreateAccessEIP8038Revised + jt[CREATE2].constantGas = params.CreateAccessEIP8038Revised +} diff --git a/execution/vm/evmtypes/rules.go b/execution/vm/evmtypes/rules.go index adb095c30f6..94f38a57249 100644 --- a/execution/vm/evmtypes/rules.go +++ b/execution/vm/evmtypes/rules.go @@ -48,6 +48,7 @@ func (bc *BlockContext) Rules(c *chain.Config) *chain.Rules { IsPrague: c.IsPrague(bc.Time) || c.IsBhilai(bc.BlockNumber), IsOsaka: c.IsOsaka(bc.Time), IsAmsterdam: c.IsAmsterdam(bc.Time), + EIP8038Revised: c.EIP8038Revised, DisabledEIPs: c.DisabledEIPs, IsAura: c.Aura != nil, } diff --git a/execution/vm/gas_table.go b/execution/vm/gas_table.go index f43d0b0e681..dae4afc3d02 100644 --- a/execution/vm/gas_table.go +++ b/execution/vm/gas_table.go @@ -34,6 +34,9 @@ import ( func callValueTransferGas(rules *chain.Rules) uint64 { if rules.IsAmsterdam { + if rules.EIP8038Revised { + return params.CallValueTransferGasEIP8038Revised + } return params.CallValueTransferGasEIP8038 } return params.CallValueTransferGas @@ -46,8 +49,27 @@ func coldAccountAccessCost(rules *chain.Rules) uint64 { return params.ColdAccountAccessCostEIP2929 } +// accountWriteCost and sstoreClearsRefund are only reached under Amsterdam rules; +// the pre-Amsterdam values have no EIP-8038 counterpart to fall back to. +func accountWriteCost(rules *chain.Rules) uint64 { + if rules.EIP8038Revised { + return params.AccountWriteCostEIP8038Revised + } + return params.AccountWriteCostEIP8038 +} + +func sstoreClearsRefund(rules *chain.Rules) uint64 { + if rules.EIP8038Revised { + return params.SstoreClearsScheduleRefundEIP8038Revised + } + return params.SstoreClearsScheduleRefundEIP8038 +} + func coldStorageAccessCost(rules *chain.Rules) uint64 { if rules.IsAmsterdam { + if rules.EIP8038Revised { + return params.ColdStorageAccessCostEIP8038Revised + } return params.ColdStorageAccessCostEIP8038 } return params.ColdSloadCostEIP2929 diff --git a/execution/vm/gas_table_test.go b/execution/vm/gas_table_test.go index f363ed17456..9631862cf9e 100644 --- a/execution/vm/gas_table_test.go +++ b/execution/vm/gas_table_test.go @@ -42,6 +42,7 @@ import ( "github.com/erigontech/erigon/execution/protocol/mdgas" "github.com/erigontech/erigon/execution/protocol/params" "github.com/erigontech/erigon/execution/state" + "github.com/erigontech/erigon/execution/tests/testforks" "github.com/erigontech/erigon/execution/tests/testutil" "github.com/erigontech/erigon/execution/tracing" "github.com/erigontech/erigon/execution/types" @@ -254,6 +255,44 @@ func TestEIP8038SStore(t *testing.T) { } } +// TestEIP8038RevisedScheduleSelectedByConfig pins the revised state-access schedule +// as a chain-config property rather than a global: the same SSTORE pays the +// pre-revision cold access under a plain Amsterdam config and the revised one when +// the config selects it. +func TestEIP8038RevisedScheduleSelectedByConfig(t *testing.T) { + sstoreExecutionGas := func(t *testing.T, config *chain.Config) uint64 { + t.Helper() + tx, sd := testTemporalTxSD(t) + txNum, _, err := sd.SeekCommitment(t.Context(), tx) + require.NoError(t, err) + r, w := state.NewReaderV3(sd.AsGetter(tx)), state.NewWriter(sd.AsPutDel(tx), nil, txNum) + s := state.New(r) + defer s.Close() + address := accounts.InternAddress(common.BytesToAddress([]byte("contract"))) + require.NoError(t, s.CreateAccount(address, true)) + require.NoError(t, s.SetCode(address, hexutil.MustDecode("0x6001600055"), tracing.CodeChangeUnspecified)) + vmctx := evmtypes.BlockContext{ + CanTransfer: func(evmtypes.IntraBlockState, accounts.Address, uint256.Int) (bool, error) { return true, nil }, + Transfer: func(evmtypes.IntraBlockState, accounts.Address, accounts.Address, uint256.Int, bool, *chain.Rules) error { + return nil + }, + } + _ = s.CommitBlock(vmctx.Rules(config), w) + vmenv := vm.NewEVM(vmctx, evmtypes.TxContext{}, s, config, vm.Config{}) + pool := mdgas.MdGas{Execution: 10_000_000, State: 10_000_000} + _, gas, _, err := vmenv.Call(accounts.ZeroAddress, address, nil, pool, uint256.Int{}, false /* bailout */) + require.NoError(t, err) + return pool.Execution - gas.Execution + } + + pinned := sstoreExecutionGas(t, testforks.Forks["Amsterdam"]) + revised := sstoreExecutionGas(t, testforks.Forks[testforks.BinaryTree]) + require.Equal(t, + params.ColdStorageAccessCostEIP8038-params.ColdStorageAccessCostEIP8038Revised, + pinned-revised, + "revised schedule must lower the cold storage access by the repricing delta") +} + func TestEIP7928SStoreReadRequiresAffordableAccess(t *testing.T) { tests := []struct { name string diff --git a/execution/vm/interpreter.go b/execution/vm/interpreter.go index c9218b082fd..6c339a766d0 100644 --- a/execution/vm/interpreter.go +++ b/execution/vm/interpreter.go @@ -301,6 +301,8 @@ func copyJumpTable(jt *JumpTable) *JumpTable { func jumpTable(chainRules *chain.Rules, cfg Config) *JumpTable { var jt *JumpTable switch { + case chainRules.IsAmsterdam && chainRules.EIP8038Revised: + jt = &amsterdamEIP8038RevisedSet case chainRules.IsAmsterdam: jt = &amsterdamInstructionSet case chainRules.IsOsaka: diff --git a/execution/vm/jump_table.go b/execution/vm/jump_table.go index 88e80db7e4f..f384a23203b 100644 --- a/execution/vm/jump_table.go +++ b/execution/vm/jump_table.go @@ -71,6 +71,7 @@ var ( pragueInstructionSet = newPragueInstructionSet() osakaInstructionSet = newOsakaInstructionSet() amsterdamInstructionSet = newAmsterdamInstructionSet() + amsterdamEIP8038RevisedSet = newAmsterdamEIP8038RevisedInstructionSet() ) // JumpTable contains the EVM opcodes supported at a given fork. @@ -109,6 +110,13 @@ func newAmsterdamInstructionSet() JumpTable { return instructionSet } +func newAmsterdamEIP8038RevisedInstructionSet() JumpTable { + instructionSet := newAmsterdamInstructionSet() + enable8038Revised(&instructionSet) + validateAndFillMaxStack(&instructionSet) + return instructionSet +} + func newOsakaInstructionSet() JumpTable { instructionSet := newPragueInstructionSet() enable7939(&instructionSet) // EIP-7939 (CLZ opcode) diff --git a/execution/vm/operations_acl.go b/execution/vm/operations_acl.go index 25f979b3786..9113f0959da 100644 --- a/execution/vm/operations_acl.go +++ b/execution/vm/operations_acl.go @@ -45,10 +45,10 @@ func makeGasSStoreFunc(clearingRefund uint64) gasFunc { } var coldAccess, writeCreate, writeExisting, clearRefund, stateCreate uint64 if rules.IsAmsterdam { - coldAccess = params.ColdStorageAccessCostEIP8038 + coldAccess = coldStorageAccessCost(rules) writeCreate = params.StorageWriteCostEIP8038 writeExisting = params.StorageWriteCostEIP8038 - clearRefund = params.SstoreClearsScheduleRefundEIP8038 + clearRefund = sstoreClearsRefund(rules) stateCreate = params.StateGasPerStorageSet } else { coldAccess = params.SstoreColdAccessEIP2929 @@ -268,7 +268,7 @@ func makeSelfdestructGasFn(refundsEnabled bool) gasFunc { evm.IntraBlockState().MarkAddressAccess(address, false) if empty && !balance.IsZero() { if evm.chainRules.IsAmsterdam { - gas.Execution += params.AccountWriteCostEIP8038 + gas.Execution += accountWriteCost(evm.chainRules) gas.State = params.StateGasNewAccount } else { gas.Execution += params.CreateBySelfdestructGas From 231d77591d7a036f8746565c9c33db7c542c0f92 Mon Sep 17 00:00:00 2001 From: awskii Date: Mon, 10 Aug 2026 17:23:03 +0700 Subject: [PATCH 52/56] execution/commitment, rpc/jsonrpc: satisfy gocritic sloppyReassign and deferInLoop Six err re-assignments become declarations, and the storage-layout test releases its trie per iteration rather than deferring inside the loop. --- execution/commitment/pbin_branch.go | 2 +- execution/commitment/pbin_patricia_hashed.go | 2 +- execution/commitment/pbin_storage_layout_test.go | 10 ++++++++-- execution/commitment/pbin_update_stream.go | 4 ++-- node/eth/backend.go | 2 +- rpc/jsonrpc/debug_execution_witness.go | 2 +- rpc/jsonrpc/pbin_witness_stateless.go | 2 +- 7 files changed, 15 insertions(+), 9 deletions(-) diff --git a/execution/commitment/pbin_branch.go b/execution/commitment/pbin_branch.go index aa4987454b6..d8008850b86 100644 --- a/execution/commitment/pbin_branch.go +++ b/execution/commitment/pbin_branch.go @@ -141,7 +141,7 @@ func pbinDecodeBranch(data []byte, cells *[2]pbinCell) (touchMap, afterMap uint1 return 0, 0, fmt.Errorf("%w: %d bytes is shorter than the header", errPBinMalformedBranch, len(data)) } touchMap, afterMap = binary.BigEndian.Uint16(data), binary.BigEndian.Uint16(data[2:]) - if err = pbinCheckCellMaps(touchMap, afterMap); err != nil { + if err := pbinCheckCellMaps(touchMap, afterMap); err != nil { return 0, 0, err } diff --git a/execution/commitment/pbin_patricia_hashed.go b/execution/commitment/pbin_patricia_hashed.go index 8b559ee110e..0fe512cde80 100644 --- a/execution/commitment/pbin_patricia_hashed.go +++ b/execution/commitment/pbin_patricia_hashed.go @@ -160,7 +160,7 @@ func (pph *PBinPatriciaHashed) Process(ctx context.Context, updates *Updates, lo return nil, fmt.Errorf("pbin: final fold: %w", err) } } - if err = pph.storeRoot(); err != nil { + if err := pph.storeRoot(); err != nil { return nil, err } if onProgress != nil { diff --git a/execution/commitment/pbin_storage_layout_test.go b/execution/commitment/pbin_storage_layout_test.go index 355bccb99d7..3d6b4720094 100644 --- a/execution/commitment/pbin_storage_layout_test.go +++ b/execution/commitment/pbin_storage_layout_test.go @@ -55,7 +55,10 @@ func TestPBinStorageLayoutCost(t *testing.T) { } rows := make([]row, 0, len(patterns)) - for _, p := range patterns { + buildRow := func(p struct { + name string + slot func(i int) uint64 + }) row { c := new(pbinTestCorpus).account(addr, 1, 100, pbinTestCodeHash(0)) for i := range slots { c = c.storage(addr, pbinSlotAt(p.slot(i)), byte(i+1)) @@ -80,7 +83,10 @@ func TestPBinStorageLayoutCost(t *testing.T) { t.Fatalf("unknown tag %#x", n[0]) } } - rows = append(rows, r) + return r + } + for _, p := range patterns { + rows = append(rows, buildRow(p)) } t.Logf("%d storage slots on one account, by where the embedding puts them:", slots) diff --git a/execution/commitment/pbin_update_stream.go b/execution/commitment/pbin_update_stream.go index ae967650f7a..1f1c3b4369a 100644 --- a/execution/commitment/pbin_update_stream.go +++ b/execution/commitment/pbin_update_stream.go @@ -73,10 +73,10 @@ func (s *pbinUpdateStream) process(ctx context.Context, updates *Updates, state if err != nil { return processed, err } - if err = s.flushCodeChunks(); err != nil { + if err := s.flushCodeChunks(); err != nil { return processed, err } - if err = s.flushRemovals(nil); err != nil { + if err := s.flushRemovals(nil); err != nil { return processed, err } return processed, nil diff --git a/node/eth/backend.go b/node/eth/backend.go index 20662857385..7d487de040f 100644 --- a/node/eth/backend.go +++ b/node/eth/backend.go @@ -317,7 +317,7 @@ func New(ctx context.Context, stack *node.Node, config *ethconfig.Config, logger statecfg.ExperimentalBinCommitment = true } if config.BinCommitmentHash != "" { - if err = commitment.SetPBinHashSuite(config.BinCommitmentHash); err != nil { + if err := commitment.SetPBinHashSuite(config.BinCommitmentHash); err != nil { return err } statecfg.BinCommitmentHash = config.BinCommitmentHash diff --git a/rpc/jsonrpc/debug_execution_witness.go b/rpc/jsonrpc/debug_execution_witness.go index 43ec717f967..02f42f97148 100644 --- a/rpc/jsonrpc/debug_execution_witness.go +++ b/rpc/jsonrpc/debug_execution_witness.go @@ -2209,7 +2209,7 @@ func execBlockStatelessly(result *ExecutionWitnessResult, block *types.Block, ch // common.HexToAddress("0x8863786beBE8eB9659DF00b49f8f1eeEc7e2C8c1"), }) - if err = replayBlockOverWitness(result, block, chainConfig, engine, stateless); err != nil { + if err := replayBlockOverWitness(result, block, chainConfig, engine, stateless); err != nil { return common.Hash{}, stateless, err } diff --git a/rpc/jsonrpc/pbin_witness_stateless.go b/rpc/jsonrpc/pbin_witness_stateless.go index 22b173d8d38..86bc2f891a6 100644 --- a/rpc/jsonrpc/pbin_witness_stateless.go +++ b/rpc/jsonrpc/pbin_witness_stateless.go @@ -77,7 +77,7 @@ func pbinExecBlockStatelessly( if err != nil { return common.Hash{}, nil, err } - if err = replayBlockOverWitness(result, block, chainConfig, engine, stateless); err != nil { + if err := replayBlockOverWitness(result, block, chainConfig, engine, stateless); err != nil { return common.Hash{}, stateless, err } From 15737330f002411b853728eaf3b1252dc8e86de7 Mon Sep 17 00:00:00 2001 From: awskii Date: Mon, 10 Aug 2026 17:23:09 +0700 Subject: [PATCH 53/56] tests: pin the bin commitment tests to the sequential trie The bin trie is sequential-only and settings resolution refuses it combined with parallel or streaming commitment, so these tests force both off and restore the process-wide values on cleanup. --- execution/state/genesiswrite/pbin_genesis_test.go | 13 ++++++++++++- rpc/jsonrpc/pbin_witness_reachable_test.go | 7 +++++++ rpc/rpchelper/pbin_commitment_test.go | 11 ++++++++++- 3 files changed, 29 insertions(+), 2 deletions(-) diff --git a/execution/state/genesiswrite/pbin_genesis_test.go b/execution/state/genesiswrite/pbin_genesis_test.go index 029ee5a0ea6..9a2a8b3f105 100644 --- a/execution/state/genesiswrite/pbin_genesis_test.go +++ b/execution/state/genesiswrite/pbin_genesis_test.go @@ -37,8 +37,19 @@ import ( func withBinCommitment(t *testing.T, on bool) { t.Helper() orig := statecfg.ExperimentalBinCommitment - t.Cleanup(func() { statecfg.ExperimentalBinCommitment = orig }) + origParallel, origStreaming := statecfg.ExperimentalParallelCommitment, statecfg.ExperimentalStreamingCommitment + t.Cleanup(func() { + statecfg.ExperimentalBinCommitment = orig + statecfg.ExperimentalParallelCommitment = origParallel + statecfg.ExperimentalStreamingCommitment = origStreaming + }) statecfg.ExperimentalBinCommitment = on + if on { + // erigondb.toml resolution refuses bin combined with either: the bin trie + // is sequential-only, regardless of a process-wide parallel/streaming default. + statecfg.ExperimentalParallelCommitment = false + statecfg.ExperimentalStreamingCommitment = false + } } func pbinTestGenesis() *types.Genesis { diff --git a/rpc/jsonrpc/pbin_witness_reachable_test.go b/rpc/jsonrpc/pbin_witness_reachable_test.go index c9f99c3c563..bd1cc558b8e 100644 --- a/rpc/jsonrpc/pbin_witness_reachable_test.go +++ b/rpc/jsonrpc/pbin_witness_reachable_test.go @@ -43,14 +43,21 @@ func withBinCommitmentDatadir(t *testing.T) { t.Helper() origBin, origHash, origSuite := statecfg.ExperimentalBinCommitment, statecfg.BinCommitmentHash, commitment.PBinHashSuiteName() + origParallel, origStreaming := statecfg.ExperimentalParallelCommitment, statecfg.ExperimentalStreamingCommitment t.Cleanup(func() { statecfg.ExperimentalBinCommitment = origBin statecfg.BinCommitmentHash = origHash require.NoError(t, commitment.SetPBinHashSuite(origSuite)) + statecfg.ExperimentalParallelCommitment = origParallel + statecfg.ExperimentalStreamingCommitment = origStreaming }) statecfg.ExperimentalBinCommitment = true statecfg.BinCommitmentHash = commitment.PBinHashBlake3 require.NoError(t, commitment.SetPBinHashSuite(commitment.PBinHashBlake3)) + // erigondb.toml resolution refuses bin combined with either: the bin trie is + // sequential-only, regardless of a process-wide parallel/streaming default. + statecfg.ExperimentalParallelCommitment = false + statecfg.ExperimentalStreamingCommitment = false } func withCommitmentHistory(t *testing.T) { diff --git a/rpc/rpchelper/pbin_commitment_test.go b/rpc/rpchelper/pbin_commitment_test.go index a5d6fbdda4f..f6bfbeb7d9d 100644 --- a/rpc/rpchelper/pbin_commitment_test.go +++ b/rpc/rpchelper/pbin_commitment_test.go @@ -39,8 +39,17 @@ func TestPBinCommitmentReplayRefusesBin(t *testing.T) { defer tx.Rollback() orig := statecfg.ExperimentalBinCommitment - t.Cleanup(func() { statecfg.ExperimentalBinCommitment = orig }) + origParallel, origStreaming := statecfg.ExperimentalParallelCommitment, statecfg.ExperimentalStreamingCommitment + t.Cleanup(func() { + statecfg.ExperimentalBinCommitment = orig + statecfg.ExperimentalParallelCommitment = origParallel + statecfg.ExperimentalStreamingCommitment = origStreaming + }) statecfg.ExperimentalBinCommitment = true + // erigondb.toml resolution refuses bin combined with either: the bin trie is + // sequential-only, regardless of a process-wide parallel/streaming default. + statecfg.ExperimentalParallelCommitment = false + statecfg.ExperimentalStreamingCommitment = false // Fresh dirs: the replay resolves erigondb.toml itself, and a hex toml would // be refused there instead of at the SharedDomains this test pins. From 912ecdde414d1454d29f8a09d1528d0f1af28874 Mon Sep 17 00:00:00 2001 From: awskii Date: Mon, 10 Aug 2026 22:34:44 +0700 Subject: [PATCH 54/56] execution/commitment, rpc/jsonrpc: trim narrative comments from the pbin witness code Removes doc comments that restate the signature, the "ok is false when the witness proves it absent" tail repeated at four sites, and a task reference. The canonical statements are kept where they belong: the absence cases at leaf(), the leafCellHash cross-reference, the root-first ordering contract, and the engine rationale in pbin_witness_context.go's file comment. Comments only. --- .../commitment/pbin_storage_layout_test.go | 2 +- .../commitment/pbin_witness_prune_test.go | 22 ++++++++----------- execution/commitment/pbin_witness_state.go | 15 ++++--------- execution/commitment/pbin_witness_test.go | 15 ++++++------- rpc/jsonrpc/pbin_witness_reachable_test.go | 7 +++--- rpc/jsonrpc/pbin_witness_stateless.go | 2 -- rpc/jsonrpc/pbin_witness_stateless_test.go | 5 ++--- 7 files changed, 26 insertions(+), 42 deletions(-) diff --git a/execution/commitment/pbin_storage_layout_test.go b/execution/commitment/pbin_storage_layout_test.go index 3d6b4720094..1948433b1f4 100644 --- a/execution/commitment/pbin_storage_layout_test.go +++ b/execution/commitment/pbin_storage_layout_test.go @@ -112,7 +112,7 @@ func TestPBinStorageLayoutCost(t *testing.T) { // into one group and once spread one-per-group. Both are storage-zone keys of the // same width, so any difference is the shared group stem alone. // -// Two things this has to get right, both of which were got wrong first: +// Two things to get right: // // - Measure the PRUNED witness. Witnesses returns a superset that callers prune // with PBinWitnessNodesForKeys; the superset carries off-path siblings re-hashed diff --git a/execution/commitment/pbin_witness_prune_test.go b/execution/commitment/pbin_witness_prune_test.go index 54321091094..11a8c9bcae4 100644 --- a/execution/commitment/pbin_witness_prune_test.go +++ b/execution/commitment/pbin_witness_prune_test.go @@ -157,8 +157,7 @@ func TestPBinWitnessPruneKeepsProofPaths(t *testing.T) { // TestPBinWitnessPruneDropsOffPathNodes: the capture holds nodes neither a proved // key nor a collapse reaches — whole subtrees hanging two or more levels off a -// path, and branches re-hashed under a shorter prefix earlier in the fold. -// Keeping them is the whole cost the pruner exists to remove. +// path. func TestPBinWitnessPruneDropsOffPathNodes(t *testing.T) { t.Parallel() @@ -203,8 +202,7 @@ func TestPBinWitnessPruneKeepsCodeLeaves(t *testing.T) { } // TestPBinWitnessPruneStopsAtBlindedChild: a key whose path leaves the witness -// keeps what it walked and stops. The key is built from a path the witness is -// known to blind, so the case cannot silently stop being one. +// keeps what it walked and stops. func TestPBinWitnessPruneStopsAtBlindedChild(t *testing.T) { t.Parallel() @@ -291,13 +289,11 @@ func TestPBinWitnessPruneKeepsSubtreePrefix(t *testing.T) { require.Equal(t, f.root, pbinWitnessMerkelized(t, f.prune(t, [][]byte{stem}), f.root)) } -// TestPBinWitnessServesRemoval: a removal collapses the branch above the key and -// re-hashes the surviving sibling under a longer prefix. That needs the -// sibling's own preimage — a branch hash commits to the node under the prefix it -// had, so it cannot be re-prefixed — which the capture has to hash and the -// pruner has to keep. Both sibling shapes are covered: a leaf, which the fold -// hashes on its way past, and a branch, which arrives as a bare hash out of its -// parent's record. +// TestPBinWitnessServesRemoval: collapsing a branch re-hashes the surviving +// sibling under a longer prefix, which needs its own preimage — a branch hash +// commits to the prefix it had and can't be reused as-is. Both sibling shapes +// are covered: a leaf the fold already hashes, and a branch that arrives as a +// bare hash from its parent's record. func TestPBinWitnessServesRemoval(t *testing.T) { t.Parallel() @@ -360,8 +356,8 @@ func TestPBinWitnessServesRemoval(t *testing.T) { } } -// TestPBinWitnessPruneEmptyCapture: no capture, nothing to prune. The empty -// result is what an update set touching nothing produces. +// TestPBinWitnessPruneEmptyCapture: an update set that touches nothing produces +// no nodes to prune. func TestPBinWitnessPruneEmptyCapture(t *testing.T) { t.Parallel() diff --git a/execution/commitment/pbin_witness_state.go b/execution/commitment/pbin_witness_state.go index b46f1c2020a..d214b8f1992 100644 --- a/execution/commitment/pbin_witness_state.go +++ b/execution/commitment/pbin_witness_state.go @@ -49,7 +49,6 @@ type PBinAccount struct { CodeHash common.Hash } -// PBinWitnessState is a decoded binary witness served as pre-state. type PBinWitnessState struct { tree *pbinWitnessTree ctx *pbinWitnessContext @@ -72,8 +71,6 @@ func PBinNewWitnessState(nodes [][]byte, root []byte) (*PBinWitnessState, error) // pre-state chunk leaves. Everything else is read from the leaves. func (s *PBinWitnessState) SetCode(addr, code []byte) { s.ctx.setCode(addr, code) } -// Account resolves an address to the state its account leaves hold. ok is -// false when the witness proves the account absent. func (s *PBinWitnessState) Account(addr []byte) (PBinAccount, bool, error) { // An account holds exactly one of the CODE_HASH and DELEGATION leaves, and // neither is ever zero, so whichever exists marks the account present — @@ -116,8 +113,7 @@ func (s *PBinWitnessState) Account(addr []byte) (PBinAccount, bool, error) { return acc, true, nil } -// Storage resolves one slot. ok is false when the witness proves the slot -// absent, which the tree reads as zero. +// An absent slot resolves to the zero hash, matching SLOAD's default value. func (s *PBinWitnessState) Storage(addr, slot []byte) (common.Hash, bool, error) { value, ok, err := s.tree.leaf(s.keys.storageKey(addr, slot)) if err != nil || !ok { @@ -145,8 +141,7 @@ func (s *PBinWitnessState) HasStorage(addr []byte) bool { } // Code returns the account's bytecode: reassembled from the chunk leaves, or -// read from the DELEGATION leaf for a delegated account. ok is false when the -// witness proves the account absent. +// read from the DELEGATION leaf for a delegated account. func (s *PBinWitnessState) Code(addr []byte) ([]byte, bool, error) { code, err := s.ctx.codeFromLeaves(addr) if err != nil { @@ -156,8 +151,7 @@ func (s *PBinWitnessState) Code(addr []byte) ([]byte, bool, error) { } // Root applies the block's writes over the witness and returns the post-state -// root. The engine runs against the witness alone, so leaf splitting, branch -// creation, BASIC_DATA packing and code chunking are the ones the chain uses. +// root. func (s *PBinWitnessState) Root(ctx context.Context, plainKeys [][]byte, updates []Update) ([]byte, error) { if len(plainKeys) != len(updates) { return nil, fmt.Errorf("pbin: %d plain keys for %d updates", len(plainKeys), len(updates)) @@ -232,8 +226,7 @@ func (c *pbinWitnessContext) codeFromLeaves(addr []byte) ([]byte, error) { // delegationCode reads a delegated account's code: the indicator its DELEGATION // leaf carries. There is nothing to reassemble and no hash to check against — // the root commits the leaf itself — so the leaf's fixed shape is the only thing -// that can be checked, and code_size has to agree with it. A nil result means the -// witness proves the account absent. +// that can be checked, and code_size has to agree with it. func (c *pbinWitnessContext) delegationCode(addr []byte, size uint64) ([]byte, error) { value, ok, err := c.tree.leaf(c.keys.accountKey(addr, pbinDelegationLeafKey)) if err != nil || !ok { diff --git a/execution/commitment/pbin_witness_test.go b/execution/commitment/pbin_witness_test.go index 09a2f4026a9..51cb23cfe99 100644 --- a/execution/commitment/pbin_witness_test.go +++ b/execution/commitment/pbin_witness_test.go @@ -174,7 +174,7 @@ func TestPBinWitnessTracerEmitsEveryNode(t *testing.T) { } // TestPBinWitnessTracerCoversRootLeaf: a one-key tree folds no row, so its only -// node is hashed by RootHash. A tap in foldBranch would emit nothing here. +// node is hashed by RootHash rather than during a fold. func TestPBinWitnessTracerCoversRootLeaf(t *testing.T) { t.Parallel() @@ -210,8 +210,8 @@ func TestPBinWitnessTracerCoversSiblingCells(t *testing.T) { require.Equal(t, pbinWitnessOracleNodes(t, corpus.entries(t)), emitted) } -// TestPBinWitnessTracerDetachedOnReset keeps the tracer off the normal -// commitment path a reset engine goes back to serving. +// TestPBinWitnessTracerDetachedOnReset: Reset detaches the tracer, so the +// process that follows would trip a still-attached rejecting one. func TestPBinWitnessTracerDetachedOnReset(t *testing.T) { t.Parallel() @@ -254,9 +254,8 @@ func pbinWitnessPending() *pbinTestCorpus { return c } -// TestPBinWitnessesReturnsParentRoot: the pass proves the tree as it stands. -// buildWitnessTrie checks the returned root against the parent block's, so an -// applied update would fail there. +// TestPBinWitnessesReturnsParentRoot: the pass proves the tree as it stands, +// not any pending modifications to it. func TestPBinWitnessesReturnsParentRoot(t *testing.T) { t.Parallel() @@ -286,8 +285,8 @@ func pbinWitnessNodeFor(t *testing.T, nodes [][]byte, hash []byte) []byte { return nil } -// TestPBinWitnessesLeavesStateUntouched: the fold writes each branch row back as -// it goes, and this pass folds rows it never modified. +// TestPBinWitnessesLeavesStateUntouched: the witness pass must not write any +// branch row back to state. func TestPBinWitnessesLeavesStateUntouched(t *testing.T) { t.Parallel() diff --git a/rpc/jsonrpc/pbin_witness_reachable_test.go b/rpc/jsonrpc/pbin_witness_reachable_test.go index bd1cc558b8e..e1bf34c73e7 100644 --- a/rpc/jsonrpc/pbin_witness_reachable_test.go +++ b/rpc/jsonrpc/pbin_witness_reachable_test.go @@ -76,10 +76,9 @@ func enableCommitmentHistoryFlag(t *testing.T, db kv.TemporalRwDB) { })) } -// TestPBinExecutionWitnessReachable is what Task 10 unblocks: debug_executionWitness -// no longer declares itself hex-only, so a bin datadir reaches the pipeline instead of -// ErrBinCommitmentUnsupported. Under bin the stateless gate is not skippable, so a -// returned witness is one that re-executed the block to its post-state root. +// TestPBinExecutionWitnessReachable confirms a bin datadir reaches the witness pipeline +// instead of ErrBinCommitmentUnsupported. Under bin the stateless gate is not skippable, so +// a returned witness is one that re-executed the block to its post-state root. func TestPBinExecutionWitnessReachable(t *testing.T) { // No t.Parallel: mutates process-global commitment flags. withCommitmentHistory(t) diff --git a/rpc/jsonrpc/pbin_witness_stateless.go b/rpc/jsonrpc/pbin_witness_stateless.go index 86bc2f891a6..19f419d925b 100644 --- a/rpc/jsonrpc/pbin_witness_stateless.go +++ b/rpc/jsonrpc/pbin_witness_stateless.go @@ -290,8 +290,6 @@ func (s *pbinWitnessStateless) CreateContract(address accounts.Address) error { return nil } -// Finalize turns the block's writes into the plain-key updates the commitment -// layer takes and recomputes the root over the witness. func (s *pbinWitnessStateless) Finalize(ctx context.Context) (common.Hash, error) { plainKeys, updates, err := s.pendingUpdates() if err != nil { diff --git a/rpc/jsonrpc/pbin_witness_stateless_test.go b/rpc/jsonrpc/pbin_witness_stateless_test.go index 1adbf709c75..27207c398df 100644 --- a/rpc/jsonrpc/pbin_witness_stateless_test.go +++ b/rpc/jsonrpc/pbin_witness_stateless_test.go @@ -798,9 +798,8 @@ func TestPBinWitnessVerifyGateChecksKeys(t *testing.T) { require.ErrorContains(t, g.verify(result, g.block), g.corpus.eoa.Hex()) } -// TestWitnessVerifySkippedOnlyUnderHex: ERIGON_WITNESS_NO_VERIFY buys back hex's -// doubled execution cost. Under bin the gate is the only correctness evidence -// there is, so the same variable must not turn it off. +// TestWitnessVerifySkippedOnlyUnderHex: the same env var may skip the gate +// under hex but never under bin — see witnessVerifySkipped for why. func TestWitnessVerifySkippedOnlyUnderHex(t *testing.T) { require.False(t, witnessVerifySkipped(false /* binTrie */), "hex verification is off by default") require.False(t, witnessVerifySkipped(true /* binTrie */), "bin verification is off by default") From 45b1bfb1e12798898a248b1e4eeaaef70ea37b5e Mon Sep 17 00:00:00 2001 From: awskii Date: Mon, 10 Aug 2026 21:41:10 +0700 Subject: [PATCH 55/56] execution/protocol/mdgas: charge the revised create access on creating transactions CreateAccessEIP8038Revised reached only the CREATE and CREATE2 opcodes. A contract-creating transaction prices through CreateAccessEIP2780, which derives from the unrevised constant, so a config opting into the revised schedule charged 12000 for the opcode and 11000 for the transaction. --- execution/protocol/mdgas/intrinsic_gas.go | 6 ++- .../protocol/mdgas/intrinsic_gas_test.go | 38 +++++++++++++++++++ 2 files changed, 43 insertions(+), 1 deletion(-) diff --git a/execution/protocol/mdgas/intrinsic_gas.go b/execution/protocol/mdgas/intrinsic_gas.go index 050082b68ae..53051d765da 100644 --- a/execution/protocol/mdgas/intrinsic_gas.go +++ b/execution/protocol/mdgas/intrinsic_gas.go @@ -79,7 +79,11 @@ func CalcIntrinsicGas(args IntrinsicGasCalcArgs) (IntrinsicGasCalcResult, bool) case args.IsEIP2780: result.ExecutionGas = params.TxBaseEIP2780 if args.IsContractCreation { - result.ExecutionGas += params.CreateAccessEIP2780 + createAccess := params.CreateAccessEIP2780 + if args.IsEIP8038Revised { + createAccess = params.CreateAccessEIP8038Revised + } + result.ExecutionGas += createAccess if args.HasValue { result.ExecutionGas += params.TransferLogCostEIP2780 } diff --git a/execution/protocol/mdgas/intrinsic_gas_test.go b/execution/protocol/mdgas/intrinsic_gas_test.go index c0e39f28de4..7dffbc1df4f 100644 --- a/execution/protocol/mdgas/intrinsic_gas_test.go +++ b/execution/protocol/mdgas/intrinsic_gas_test.go @@ -391,6 +391,44 @@ func TestEIP2780IntrinsicGas(t *testing.T) { } } +// A contract-creating transaction and the CREATE opcode must price the new +// account identically. They read separate constants, so a schedule that moves +// one without the other diverges silently: the opcode charges the revised cost +// while the transaction keeps the base one. +func TestEIP8038RevisedCreateAccess(t *testing.T) { + cases := map[string]struct { + revised bool + createAcces uint64 + }{ + "base schedule": {createAcces: params.CreateAccessEIP8038}, + "revised schedule": {revised: true, createAcces: params.CreateAccessEIP8038Revised}, + } + for name, c := range cases { + t.Run(name, func(t *testing.T) { + args := IntrinsicGasCalcArgs{ + IsContractCreation: true, + IsEIP2: true, + IsEIP2028: true, + IsEIP3860: true, + IsEIP7623: true, + IsEIP7976: true, + IsEIP7981: true, + IsEIP2780: true, + IsEIP8038Revised: c.revised, + } + result, overflow := CalcIntrinsicGas(args) + assert.False(t, overflow) + assert.Equal(t, params.TxBaseEIP2780+c.createAcces, result.ExecutionGas) + + // The flag prices creation only; an ordinary recipient is untouched. + args.IsContractCreation = false + eoa, overflow := CalcIntrinsicGas(args) + assert.False(t, overflow) + assert.Equal(t, params.TxBaseEIP2780+params.ColdAccountAccessEIP2780, eoa.ExecutionGas) + }) + } +} + func TestEIP2780ContractCreationStateGasIsRuntime(t *testing.T) { result, overflow := CalcIntrinsicGas(IntrinsicGasCalcArgs{ IsContractCreation: true, From 2876b42430c42edf8df917fba0c483580100ba0f Mon Sep 17 00:00:00 2001 From: awskii Date: Tue, 11 Aug 2026 21:19:42 +0700 Subject: [PATCH 56/56] execution/commitment: refuse a leaf whose state read comes back absent MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit loadCellState substituted 32 zero bytes for a storage leaf whose plain key the domain no longer held, and committed it. EIP-8297 collapsed zero into absent (spec 7852514), so no entry set produces that root — the value is unrepresentable rather than merely stale. The account arm already refused; the storage arm was the outlier, as was the witness context, which refuses the same read with errPBinWitnessNoState. The condition means the caller left a removal out of its update set. Removal lives on the update path and the grid only walks forward, so the fold cannot repair it and fails instead. --- execution/commitment/pbin_patricia_hashed.go | 12 ++-- execution/commitment/pbin_zerovalue_test.go | 66 +++++++++++++------- 2 files changed, 49 insertions(+), 29 deletions(-) diff --git a/execution/commitment/pbin_patricia_hashed.go b/execution/commitment/pbin_patricia_hashed.go index 0fe512cde80..3d86e88731f 100644 --- a/execution/commitment/pbin_patricia_hashed.go +++ b/execution/commitment/pbin_patricia_hashed.go @@ -129,7 +129,7 @@ func (pph *PBinPatriciaHashed) Release() { var ( errPBinMissingBranch = errors.New("pbin: branch record missing") - errPBinDeleteUnsupported = errors.New("pbin: account record outlived its state") + errPBinDeleteUnsupported = errors.New("pbin: record outlived its state") errPBinVisitOrder = errors.New("pbin: visit order is not ascending") ) @@ -886,9 +886,9 @@ func (pph *PBinPatriciaHashed) cellHash(c *pbinCell, path *pbinBitpath) (common. } // loadCellState fills a leaf cell whose plain key arrived from a record and -// whose value therefore did not. A storage leaf whose state reads absent carries -// the zero it stands for; an account leaf cannot, since removal drops its whole -// header stem before any record can name it. +// whose value therefore did not. A read that comes back absent means the caller +// left a removal out of its update set: the tree cannot hold the zero it stands +// for, and the fold only walks forward, so the leaf can no longer be dropped. func (pph *PBinPatriciaHashed) loadCellState(c *pbinCell) error { if c.accountAddrLen > 0 && !c.loaded.account() { plainKey := c.accountAddr[:c.accountAddrLen] @@ -909,9 +909,7 @@ func (pph *PBinPatriciaHashed) loadCellState(c *pbinCell) error { return fmt.Errorf("pbin: read storage %x: %w", plainKey, err) } if update.Deleted() { - // A stored leaf whose state reads absent: the record outlived the value. - // Carry the zero it stands for; the update path is what removes leaves. - update = &Update{Flags: StorageUpdate} + return fmt.Errorf("%w: %x", errPBinDeleteUnsupported, plainKey) } c.setFromUpdate(update) c.loaded = c.loaded.addFlag(cellLoadStorage) diff --git a/execution/commitment/pbin_zerovalue_test.go b/execution/commitment/pbin_zerovalue_test.go index c1e764afab8..f7d5ba4c7ad 100644 --- a/execution/commitment/pbin_zerovalue_test.go +++ b/execution/commitment/pbin_zerovalue_test.go @@ -18,6 +18,7 @@ package commitment import ( "bytes" + "context" "fmt" "sort" "testing" @@ -72,13 +73,12 @@ func TestPBinStorageZeroWriteRemovesLeaf(t *testing.T) { } } -// TestPBinStorageZeroOnUntouchedSiblingKeepsLeaf pins the fold path, where the -// rule does not yet hold: a slot zeroed without being in the update set is -// rehydrated from its branch record and committed as 32 zero bytes, which under -// the current spec is a state the tree cannot hold. Removal lives on the update -// path only. The domain always carries a zeroed slot in the same block's update -// set, so this is out of reach through ordinary execution. -func TestPBinStorageZeroOnUntouchedSiblingKeepsLeaf(t *testing.T) { +// TestPBinStorageZeroOnUntouchedSiblingRefuses pins the fold boundary. A slot +// zeroed without being in the update set reaches the fold through its branch +// record, and the only value it could carry is the 32 zero bytes the tree cannot +// hold. Removal lives on the update path, and the grid only walks forward, so +// the fold refuses rather than committing a root no entry set produces. +func TestPBinStorageZeroOnUntouchedSiblingRefuses(t *testing.T) { t.Parallel() addr := pbinOracleAddr(42) @@ -97,20 +97,46 @@ func TestPBinStorageZeroOnUntouchedSiblingKeepsLeaf(t *testing.T) { require.NoError(t, ms.applyPlainUpdates(touched.plainKeys, touched.updates)) pph.Reset() - root := pbinTestProcess(t, pph, touched.plainKeys, touched.updates) + upd := WrapKeyUpdates(t, ModeDirect, pbinKeyHasher(), touched.plainKeys, touched.updates) + _, err := pph.Process(context.Background(), upd, "", nil, WarmupConfig{}) + require.ErrorIs(t, err, errPBinDeleteUnsupported) +} - // The zero leaf is not a state entries() can express, since it filters zeros. - survivor := new(pbinTestCorpus).storage(addr, pbinOracleSlot(257), 0x0B) - withZeroLeaf := append(survivor.entries(t), pbinOracleEntry{ - key: pbinTreeKeyStorage(addr, pbinOracleSlot(256)), - value: make([]byte, pbinValueLength), - }) - want := pbinOracleRoot(withZeroLeaf) - require.Equal(t, want[:], root) +// TestPBinStorageZeroOnTouchedSiblingCollapses is the same shape with the +// removal declared: the update path drops the leaf and the root matches the +// entry set without it. +func TestPBinStorageZeroOnTouchedSiblingCollapses(t *testing.T) { + t.Parallel() + + addr := pbinOracleAddr(42) + stored := new(pbinTestCorpus). + storage(addr, pbinOracleSlot(256), 0x01). + storage(addr, pbinOracleSlot(257), 0x02) + + pph, ms := pbinTestEngine(t) + require.NoError(t, ms.applyPlainUpdates(stored.plainKeys, stored.updates)) + pbinTestProcess(t, pph, stored.plainKeys, stored.updates) + + gone := new(pbinTestCorpus).storage(addr, pbinOracleSlot(256)) + require.NoError(t, ms.applyPlainUpdates(gone.plainKeys, []Update{{Flags: DeleteUpdate}})) + + touched := new(pbinTestCorpus).storage(addr, pbinOracleSlot(257), 0x0B) + require.NoError(t, ms.applyPlainUpdates(touched.plainKeys, touched.updates)) + + both := new(pbinTestCorpus). + storage(addr, pbinOracleSlot(256)). + storage(addr, pbinOracleSlot(257), 0x0B) + both.updates[0] = Update{Flags: DeleteUpdate} + + pph.Reset() + root := pbinTestProcess(t, pph, both.plainKeys, both.updates) - require.NotEqual(t, survivor.oracleRoot(t), root, "the leaf survives as a zero") + survivor := new(pbinTestCorpus).storage(addr, pbinOracleSlot(257), 0x0B) + require.Equal(t, survivor.oracleRoot(t), root) } +// TestPBinLoadCellStateAbsentRead: neither arm has a value it may carry for a +// key the state no longer holds, so both refuse. func TestPBinLoadCellStateAbsentRead(t *testing.T) { t.Parallel() @@ -123,11 +149,7 @@ func TestPBinLoadCellStateAbsentRead(t *testing.T) { c.storageAddrLen = length.Addr + length.Hash copy(c.storageAddr[:], append(bytes.Clone(pbinOracleAddr(43)), pbinOracleSlot(1000)...)) - require.NoError(t, pph.loadCellState(&c)) - require.True(t, c.loaded.storage()) - require.False(t, c.Update.Deleted()) - value := pbinEncodeStorageValue(c.Update.Storage[:c.Update.StorageLen]) - require.Equal(t, make([]byte, length.Hash), value[:]) + require.ErrorIs(t, pph.loadCellState(&c), errPBinDeleteUnsupported) }) t.Run("account", func(t *testing.T) {