Skip to content

tests(binary_tree): witness cost of reading chunked code - #3286

Closed
awskii wants to merge 1 commit into
ethereum:projects/binary-triefrom
awskii:tests/binary-tree-witness-growth
Closed

tests(binary_tree): witness cost of reading chunked code#3286
awskii wants to merge 1 commit into
ethereum:projects/binary-triefrom
awskii:tests/binary-tree-witness-growth

Conversation

@awskii

@awskii awskii commented Aug 3, 2026

Copy link
Copy Markdown

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) then STOP. Gas, work and state touched are identical. The only variable is dead INVALID padding behind the STOP, which changes how many chunk leaves the contract occupies:

case code chunks header chunks overflow chunks code-zone stems
single_chunk 31 B 1 1 0 0
header_full 3,968 B 128 128 0 0
first_overflow_chunk 3,969 B 129 128 1 1
deep_overflow 7,936 B 256 128 128 1
max_code_size 24,576 B 793 128 665 3

Padding is INVALID, so a client that mis-executes past the STOP fails 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_chunkEXTCODESIZE and EXTCODEHASH on 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_executionWitness on the call block, against the same chain built on the Merkle-Patricia trie:

case MPT binary ratio
single_chunk 1,119 1,815 1.62x
header_full 5,055 18,764 3.71x
first_overflow 5,086 18,965 3.73x
deep_overflow 9,023 35,880 3.98x
max_code_size 25,663 108,108 4.21x

Binary witness bytes at max_code_size, attributed by reading each leaf's own key:

bytes
header chunk leaves (128) 8,576
overflow chunk leaves (665) 44,555
branches 54,070
BASIC_DATA + CODE_HASH (3 accounts) 402

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 at max_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.

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.
@CPerezz

CPerezz commented Aug 3, 2026

Copy link
Copy Markdown
Contributor

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

  • We can read "for free" the code yielding an optimization.
  • We can have a single branch in a proof if the code is under 4KB (or if the executed code it's the initial 4KB of the totality when we prove things about bigger ones)

Why I don't like code in header

  • As @awskii and Wei Han reported (see https://ethereum-magicians.org/t/not-all-state-is-equal/25508) arround 60% of the code is duplicated. We de-duplicate in the code tree(zone) but we don't in the account header (4kb for each duplicated contract deployed are indeed not de-duplicated).
  • Makes the VOPS state bigger than it needs to be. As we need to hold duplicated bytecode.
  • Adds more "complexity" al;though isn't really that complex. Is just more stuff to take into account.
  • It blows up the statedb size with a lot of useless duplicated code
  • Makes (as just showcased by @awskii and now me with my geth prototype) that the proof size explodes (for this particular worst case, but also in general (as it just pulls more hashes into the account header even if logarithmically).

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).
Also, we see proof sizes are balooning in part due to this. So is something we should also consider as a highly-important issue worth removing the code in header.

awskii added a commit to awskii/execution-specs that referenced this pull request Aug 5, 2026
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.
@CPerezz

CPerezz commented Aug 5, 2026

Copy link
Copy Markdown
Contributor

@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

@CPerezz CPerezz self-assigned this Aug 5, 2026
@codecov

codecov Bot commented Aug 6, 2026

Copy link
Copy Markdown

Codecov Report

✅ All modified and coverable lines are covered by tests.
✅ Project coverage is 90.94%. Comparing base (4a22435) to head (b2382c5).
⚠️ Report is 25 commits behind head on projects/binary-trie.

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     
Flag Coverage Δ
unittests 90.94% <ø> (-2.24%) ⬇️

Flags with carried forward coverage won't be shown. Click here to find out more.

☔ View full report in Codecov by Harness.
📢 Have feedback on the report? Share it here.

🚀 New features to boost your workflow:
  • ❄️ Test Analytics: Detect flaky tests, report on failures, and find test suite problems.

@awskii

awskii commented Aug 7, 2026

Copy link
Copy Markdown
Author

Closing, agreed.

The five cases are parametrised on Spec.CODE_OFFSET and the header/overflow split that #3310 removed, so the file no longer imports against projects/binary-trie. The measurement table is stale for the same reason.

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.

@awskii awskii closed this Aug 7, 2026
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants