tests(binary_tree): witness cost of reading chunked code - #3286
Conversation
The suite pins that chunking is invisible to execution semantics. It is not invisible to a witness: executing one byte of a contract proves every chunk that contract occupies, because code is reassembled from the tree rather than shipped alongside it. Each case executes the same 6 bytes and varies only the dead padding behind the STOP, so work, gas and state touched are identical across the parametrisation and only the chunk count moves: 1, 128, 129, 256, 793. Two supporting cases. The deploy/read pair shows the asymmetry — creating a contract proves no chunk pre-state, reading it back proves all of them. EXTCODESIZE and EXTCODEHASH against a maximum-size contract are the control: both answer from the account header, so neither reads a chunk. No fixture format for this fork carries a witness, so nothing here asserts a size. The post states are ordinary; the cost shows up when a client fills or proves them.
|
A conventional Merkle proof ships only the sibling and lets the verifier reconstruct the parent's preimage from the child it computed. That alone takes a branch from 68 → ~35 B, so per-chunk 135 → ~102, i.e. 4.35x → ~3.3x. But that is nowhere close to where we want to be. This is relevant as this was inherited from original Verkle & UBT EIPs. And it didn't got much questioning when we setup the EIP assuming it was 100% worth it. (And I'm not claiming is not for the avg. case nor that it is) I have mixed feelings here: Why I like the code in header
Why I don't like code in header
One more thing to highlight here is that within the stateless-consensus team, we already discussed removing this back 1 year ago aprox (when we started working back on UBT). We brought it to SIC but never ended up reaching consensus. Thus, it wasn't done. See: https://stateless.fyi/development/sic-calls/history.html#codechunks-inclusion-in-the-binary-tree-leaf for more info about the proposal and date. My conclusion is the same as I had back when I proposed this. And is to remove it. Back then I did not have the data to back it up. Right now I do. @awskii confirmed state size is 2x (was expected to be bigger. But not that much and I don't think intermediate nodes are the issue only, I think this duplicated 4kb pages are a big one too). |
Header code chunks are proposed for removal (ethereum#3286), so name and describe the case by the zone that outlives that: the zero chunk in the content-addressed code zone. The state and its root are unchanged.
|
@awskii I think we can close this as is stale according to my optimizations in Geth. See: CPerezz/go-ethereum#2 Please LMK if you don't agree |
Codecov Report✅ All modified and coverable lines are covered by tests. Additional details and impacted files@@ Coverage Diff @@
## projects/binary-trie #3286 +/- ##
========================================================
- Coverage 93.17% 90.94% -2.24%
========================================================
Files 628 628
Lines 37292 37292
Branches 3414 3414
========================================================
- Hits 34748 33915 -833
- Misses 1788 2920 +1132
+ Partials 756 457 -299
Flags with carried forward coverage won't be shown. Click here to find out more. ☔ View full report in Codecov by Harness. 🚀 New features to boost your workflow:
|
|
Closing, agreed. The five cases are parametrised on Re-measuring against the current layout — every chunk content-addressed, so a clone shares its chunk set rather than proving its own copy. If the numbers still say something worth pinning I'll open it fresh. |
The existing tests pin that chunking is invisible to execution semantics. These measure what it costs a witness.
The test
Every case calls a contract that executes the same 6 bytes —
SSTORE(slot, value)thenSTOP. Gas, work and state touched are identical. The only variable is deadINVALIDpadding behind theSTOP, which changes how many chunk leaves the contract occupies:single_chunkheader_fullfirst_overflow_chunkdeep_overflowmax_code_sizePadding is
INVALID, so a client that mis-executes past theSTOPfails loudly instead of passing for the wrong reason.Two supporting cases:
test_deploy_then_read_asymmetry— deploy in one block, call in the next. Block 1 writes 256 chunk leaves and proves none; block 2 executes 6 bytes and proves all 256.test_code_introspection_needs_no_chunk—EXTCODESIZEandEXTCODEHASHon a maximum-size contract. Both answer from the account header, so neither reads a chunk.No fixture format for this fork carries a witness, so nothing here asserts a size. The post states are ordinary; the cost appears when a client fills or proves them.
Measured on erigon
Context, not asserted by the tests. Erigon's EIP-8297 engine under BLAKE3,
debug_executionWitnesson the call block, against the same chain built on the Merkle-Patricia trie:single_chunkheader_fullfirst_overflowdeep_overflowmax_code_sizeBinary witness bytes at
max_code_size, attributed by reading each leaf's own key:BASIC_DATA+CODE_HASH(3 accounts)Branches cost as much as the leaves they bind, because each carries two 32-byte child hashes. The 402 B of account data is constant across all five sizes.
A contract past 3,968 bytes does not only add chunk leaves. Overflow chunks are keyed
zone ‖ H(code_hash ‖ tree_index) ‖ sub_index, so each run of 256 opens a separate code-zone stem with its own path from the root — 3 of them atmax_code_size, holding 256, 256 and 153 leaves.MPT costs one times the code plus a flat 5-node proof at every size. The binary tree converges on
(67 + 68) / 31 = 4.35x: a 67-byte chunk leaf plus a ~68-byte branch per 31 bytes of code.These numbers assume a witness carries every chunk of a contract whose code is read. That is erigon's format, not a requirement stated by the EIP. A client that proves only the executed chunks will see the parametrisation flatten, which is worth knowing either way.