Skip to content

core/state: fix codeLen=0 corruption of bintrie BasicData#34677

Open
CPerezz wants to merge 1 commit intoethereum:masterfrom
CPerezz:fix/bintrie-hasher-codelen-master
Open

core/state: fix codeLen=0 corruption of bintrie BasicData#34677
CPerezz wants to merge 1 commit intoethereum:masterfrom
CPerezz:fix/bintrie-hasher-codelen-master

Conversation

@CPerezz
Copy link
Copy Markdown
Contributor

@CPerezz CPerezz commented Apr 7, 2026

Summary

updateStateObject passed len(obj.code) as the codeLen argument to trie.UpdateAccount. obj.code is only populated when the contract code was modified in the current block — for plain balance or nonce updates the field is left empty and len(obj.code) is 0.

For MPT, StateTrie.UpdateAccount ignores its codeLen parameter (the parameter is named _ int), so the zero was harmless. For the binary trie the code size is packed into the BasicData leaf blob at bytes 4-7: passing 0 overwrites the previously-stored code size with zero every time the account is touched, corrupting the state root silently whenever a contract's balance or nonce changes without a code write.

This fix uses obj.CodeSize() instead. It returns the cached code length if obj.code is loaded, otherwise falls back to a code-size lookup via the state reader, so it always reflects the account's current total code size.

Test plan

  • TestVerkleCodeSizePreserved — regression test that verifies the state root produced by create contract → commit → reload → modify balance → commit matches the root of a single-step construction of the same final state. Before the fix, the two roots diverge:
    • path A (reload + balance): 1a675599...
    • path B (fresh, same state): de0cfb03...
  • go test ./core/state/ -count=1 — all existing tests pass
  • go test ./trie/bintrie/ -count=1 — all bintrie tests pass

Impact

  • MPT path: no behavior changeStateTrie.UpdateAccount discards codeLen.
  • Binary trie path: state root now correctly preserves code size across balance/nonce updates. This is a latent consensus-adjacent issue for any node actually running the binary trie in production (dev mode or tests today); once the binary trie is enabled more broadly, this would have been a client-divergence bug.

updateStateObject passed len(obj.code) as the codeLen argument to
trie.UpdateAccount. obj.code is only populated when the contract code
was modified in the current block — for plain balance or nonce updates
the field is left empty and len(obj.code) is 0.

For MPT, StateTrie.UpdateAccount ignores its codeLen parameter (the
parameter is named `_ int`), so the zero was harmless. For the binary
trie the code size is packed into the BasicData leaf blob at bytes 4-7:
passing 0 overwrites the previously-stored code size with zero every
time the account is touched, corrupting the state root silently
whenever a contract's balance or nonce changes without a code write.

Use obj.CodeSize() instead. It returns the cached code length if
obj.code is loaded, otherwise falls back to a code-size lookup via the
state reader, so it always reflects the account's current total code
size.

Regression test TestVerkleCodeSizePreserved verifies that the state
root produced by "create contract, commit, reload, modify balance,
commit" matches the root of a single-step construction of the same
final state. Before the fix the two roots diverge:

  path A (reload + balance): 1a675599...
  path B (fresh, same state): de0cfb03...
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.

1 participant