Skip to content

Conversation

@gballet
Copy link
Member

@gballet gballet commented Aug 14, 2025

This is broken off of #31730 to only focus on testing networks that start with verkle at genesis.

The PR has seen a lot of work since its creation, and it now targets creating and re-executing tests for a binary tree testnet without the transition (so it starts at genesis). The transition tree has been moved to its own package. It also replaces verkle with the binary tree for this specific application.

@gballet gballet added the verkle label Aug 14, 2025
gballet pushed a commit to gballet/go-ethereum that referenced this pull request Aug 19, 2025
* fix tests

* fix MakePreState not respecting verkle flag
@gballet gballet force-pushed the evm-t8n-stateless-at-genesis branch from a56b754 to 76046f3 Compare August 20, 2025 19:32
gballet pushed a commit to gballet/go-ethereum that referenced this pull request Aug 20, 2025
* fix tests

* fix MakePreState not respecting verkle flag
gballet pushed a commit to gballet/go-ethereum that referenced this pull request Sep 8, 2025
* fix tests

* fix MakePreState not respecting verkle flag
@gballet gballet force-pushed the evm-t8n-stateless-at-genesis branch from 5130362 to 553fea0 Compare September 8, 2025 08:11
Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@CPerezz drop this file

func MakePreState(db ethdb.Database, accounts types.GenesisAlloc) *state.StateDB {
tdb := triedb.NewDatabase(db, &triedb.Config{Preimages: true})
func MakePreState(db ethdb.Database, chainConfig *params.ChainConfig, pre *Prestate, verkle bool) *state.StateDB {
tdb := triedb.NewDatabase(db, &triedb.Config{Preimages: true, IsVerkle: verkle})
Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@CPerezz don't change that name yet

}
// If verkle mode started, establish the conversion
if verkle {
if _, ok := statedb.GetTrie().(*trie.VerkleTrie); ok {
Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@CPerezz please replace all instances of trie.VerkleTrie with bintrie.BinaryTrie

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

verkle-specific ci pipeline, remove

gballet added a commit to gballet/go-ethereum that referenced this pull request Oct 30, 2025
Co-authored-by: Ignacio Hagopian <[email protected]>

fix tests in ethereum#32445 (#548)

* fix tests

* fix MakePreState not respecting verkle flag

bring in verkle iterator and verkle subcommand (#549)

push missing verkle iterator files (#550)

fix rebase issue

cmd/evm/internal/t8n: Binary at genesis support - Verkle <-> Binary swap (#552)

* remove: Verkle-related files with no codebase deps

* fixup

* refactor: update Prestate and transition logic for binary trie support

- Introduced a new field `BT` in the `Prestate` struct to accommodate binary trie data.
- Updated the `MakePreState` function to replace the `verkle` flag with `isBintrie` for better clarity.
- Renamed functions and variables related to Verkle to their binary trie counterparts, including `BinKey`, `BinKeys`, and `BinTrieRoot`.
- Added TODO comments for further updates and clarifications regarding the transition from Verkle to binary trie structures.

* feat: add ChunkifyCode function for EVM bytecode chunking

- Introduced the `ChunkedCode` type to represent 32-byte chunks of EVM bytecode.
- Implemented the `ChunkifyCode` function to convert a byte array of EVM bytecode into chunked format.
- Updated the `UpdateContractCode` method to utilize the new `ChunkifyCode` function.

* refactor: rename Verkle commands to Binary Trie equivalents

We leave the command name equal to keep support with https://github.com/ethereum/execution-spec-tests/tree/verkle/main

* refactor: update flags and transition logic for Binary Trie support

- Renamed `OutputVKTFlag` to `OutputBTFlag`.
- Renamed `InputVKTFlag` to `InputBTFlag` and updated its usage description for prestate input.
- Adjusted `Transition` function to utilize the new `BT` field for Binary Trie data instead of Verkle.
- Updated `dispatchOutput` function to handle Binary Trie output correctly.

* refactor: update DumpVKTLeaves to DumpBinTrieLeaves and adjust trie usage

* refactor: update functions and comments for Binary Trie implementation

- Replaced references to Verkle Trie with Binary Trie in various functions.
- Updated `genBinTrieFromAlloc` to return a Binary Trie instead of a Verkle Trie.
- Adjusted output functions to utilize Binary Trie methods and updated associated comments.

* refactor: rename flags for Binary Trie integration

* feat: add BinaryCodeChunkKey and BinaryCodeChunkCode functions for code chunking

- Introduced `BinaryCodeChunkKey` to compute the tree key of a code-chunk for a given address.
- Added `BinaryCodeChunkCode` to return the chunkification of bytecode.

* feat: introduce TransitionTrie for Binary Trie integration

- Added a new `TransitionTrie` type to facilitate the integration of the Binary Trie with the existing MPT trie.
- Updated the `newTrieReader` function to utilize the new `TransitionTrie` implementation.
- The transition tree has been moved to its own package
 `core/state/transitiontrie`, resolving the import cycle issue we get into when including `BinaryTrie` within `database.go`/

* refactor: update references to TransitionTrie and BinaryTrie integration

- Replaced instances of `bintrie.BinaryTrie` with `transitiontrie.TransitionTrie` in the `execution.go`, `dump.go`, and `reader.go`.
- Adjusted the `newTrieReader` function with atemporary workaround for import cycles.

* trie/bintrie: fix StemNode serialization to only return actual data

This fixes the "invalid serialized node length" error that occurred when
Binary Trie nodes were persisted and later reloaded from the database.

Issue Discovery:
The error manifested when running execution-spec-tests with Binary Trie mode.
After committing state changes, attempting to create a new StateDB with the
committed root would fail with "invalid serialized node length" when
deserializing the root node.

Root Cause Analysis:
Through debug logging, discovered that:
1. StemNodes were being serialized with 97 bytes of actual data
2. But the SerializeNode function was returning the entire 8224-byte buffer
3. When the node was later loaded and deserialized, it received 97 bytes
4. The deserializer expected at least 128 bytes for the bitmap and values
5. This mismatch caused the deserialization to fail

The Fix:
Changed SerializeNode to return only the actual data (serialized[:offset])
instead of the entire pre-allocated buffer (serialized[:]). This ensures
that only the meaningful bytes are persisted to the database.

This aligns the serialization with the deserialization logic, which
correctly handles variable-length StemNode data based on which values
are actually present in the node.

* Fix Binary Trie StemNode serialization array size calculation

This fixes a critical off-by-one error in the StemNode serialization buffer
that was causing "invalid serialized node length" errors during execution-spec-tests.

The error was discovered when running execution-spec-tests with Binary Trie mode:
```
cd ../execution-spec-tests && uv run fill --fork Verkle -v -m blockchain_test \
  -k test_contract_creation -n 1 --evm-bin=PATH_TO/go-ethereum/evm
```

The tests were failing with:
```
ERROR: error getting prestate: invalid serialized node length
```

Through systematic debugging with hex dumps and bitmap analysis, we discovered
that the serialized array was incorrectly sized:

**Incorrect**: `var serialized [32 + 32 + 256*32]byte`
- This allocated: 32 + 32 + 8192 = 8256 bytes
- Missing 1 byte for the node type prefix

**Correct**: `var serialized [1 + 31 + 32 + 256*32]byte`
- This allocates: 1 + 31 + 32 + 8192 = 8256 bytes
- Properly accounts for all fields per EIP-7864

The layout should be:
- 1 byte: node type (nodeTypeStem)
- 31 bytes: stem (path prefix)
- 32 bytes: bitmap indicating which of 256 values are present
- Up to 256*32 bytes: the actual values (32 bytes each)

1. Corrected array size from `[32 + 32 + 256*32]` to `[1 + 31 + 32 + 256*32]`
2. Cleaned up type assertions to use `n` from the type switch instead of `node.(*StemNode)`
3. This ensures proper alignment of all fields during serialization

The misalignment was causing the deserializer to interpret value data as bitmap data,
leading to impossible bitmap patterns (e.g., 122 bits set requiring 3904 bytes when
only 97 bytes were available).

After this fix, the "invalid serialized node length" errors are completely resolved.
The execution-spec-tests now progress past the serialization stage, confirming that
Binary Trie nodes are being correctly serialized and deserialized.

* various tweaks

1. move the transition trie package to trie, as this will be requested
by the geth team and it's where it belongs.
2. panic if state creation has failed in execution.go, to catch issues
early.
3. use a BinaryTrie and not a TransitionTrie, as evm t8n doesn't support
state trie transitions at this stage.

* more replacements of TransitionTrie with VerkleTrie

1. TransitionTrie doesn't work at this stage, we are only testing the
binary trees at genesis, so let's make sure TransitionTrie isn't
activated anywhere.
2. There was a superfluous check for Transitioned(), which actually made
the code take the wrong turn: if the verkle fork has occured, and if the
transition isn't in progress, it means that the conversion has ended*

* to be complete, it could be that this is in the period before the
start of the iterator sweep, but in that case the transition tree also
needs to be returned. So there is a missing granularity in this code,
but it's ok at this stage since the transition isn't supported yet.

* fix(state): Use BinaryTrie instead of VerkleTrie when IsVerkle is set

The codebase uses IsVerkle flag to indicate Binary Trie mode (not renamed
to avoid large diff). However, OpenTrie was incorrectly creating a VerkleTrie
which expects Verkle node format, while we store Binary Trie nodes.

This mismatch caused "invalid serialized node length" errors when VerkleTrie
tried to deserialize Binary Trie nodes.

We left some instantiation of `VerkleTree` arround which we have found
and changed too here.

* fix(bintrie): Remove incorrect type assertion in BinaryTrie.Commit

The Commit function incorrectly assumed that the root node was always
an InternalNode, causing a panic when the root was a StemNode:
"interface conversion: bintrie.BinaryNode is *bintrie.StemNode, not *bintrie.InternalNode"

Changes:
- Remove type assertion `root := t.root.(*InternalNode)`
- Call CollectNodes directly on t.root which works for any BinaryNode type
- Add comment explaining the root can be any BinaryNode type

This fix allows BinaryTrie to properly commit trees where the root is
a StemNode, which can happen in small tries or during initial setup.

* fix(t8ntool): Add error handling and debug logging to MakePreState

Previously, state.New errors were silently ignored, leading to nil pointer
panics later when statedb was accessed. This made debugging difficult as
the actual error was hidden.

Changes:
- Add explicit error checking when creating initial statedb
- Add explicit error checking when re-opening statedb after commit
- Include meaningful error messages with context (e.g., root hash)

* fix(bintrie): Fix iterator to properly handle StemNode leaf values

This commit fixes two in the BinaryTrie iterator:

1. Fixed Leaf() method to only return true when positioned at a specific
   non-nil value within a StemNode, not just at the StemNode itself.
   The iterator tracks position using an Index that points to the NEXT
   position after the current value.

2. Fixed stack underflow in Next() method by checking if we're at the
   root before popping from the stack. This prevents index out of range
   errors when iterating through the tree.

These fixes resolve panics in DumpBinTrieLeaves when generating VKT
output for Binary Tries in the t8n tool.

* fix: handle nil child nodes in BinaryTrie InternalNode

When inserting values into an InternalNode with nil children, the code
was attempting to dereference nil pointers, causing panics. This fix
ensures that nil children are initialized to Empty{} nodes before
attempting to call methods on them.

This resolves crashes when building the Binary Trie from an empty or
sparse initial state.

* fix: preserve Binary Trie in Apply after commit

When in Binary Trie mode (isEIP4762), the state.New() call after
commit was creating a new StateDB with MPT trie, losing the Binary
Trie. This resulted in nil trie when attempting to dump Binary Trie
leaves.

Fix: Skip state.New() for Binary Trie mode since the trie is already
correct after commit. Only reopen state for MPT mode.

* fix(t8n): preserve Binary Trie after commit and use correct JSON field name

This commit fixes two issues that prevented VKT (Binary Trie) data from
being correctly generated and passed to execution-spec-tests:

1. Binary Trie Preservation (execution.go):
   After statedb.Commit(), the code was calling state.New() which
   recreated the StateDB with an MPT trie, losing the Binary Trie.
   For Binary Trie mode (EIP-4762), we now skip state.New() since
   the trie is already correct after commit.

2. JSON Field Name (transition.go):
   The dispatch function was using name="bt" which created a JSON
   output field called "bt", but execution-spec-tests expects the
   field to be called "vkt" for compatibility with the upstream
   implementation. Changed to use name="vkt" instead. So translating,
   I'm dumb and I shouldn't have changed them..

These fixes allow execution-spec-tests to successfully extract Binary
Trie leaf data from t8n output via stdout JSON parsing.

Fixes test failures in:
- tests/verkle/eip6800_genesis_verkle_tree/test_contract_creation.py

* fix(t8n): revert change to avoid hardcoded MPT creation.

Prior, the code was working such that we were getting `nil` when re-opening. We weren't using `TransitionTree` thus reader and other interfaces were working hardcoded to MPT. This was fixed in prev. commits. Thus, this was no longer needed.

* fix(t8ntool, bintrie): address PR review comments

* fix(t8ntool): update JSON field name for Binary Trie in Prestate

There was a leftover as we modified already in `transition.go` flagnames to be `vkt` again and prevent errors for now.

* refactor: use named constants for binary node serialization sizes

Replace magic numbers with named constants for better code clarity:
- NodeTypeBytes = 1 (size of node type prefix)
- HashSize = 32 (size of a hash)
- BitmapSize = 32 (size of the bitmap in stem nodes)

This makes the serialization format more self-documenting and easier
to maintain.

* refactor: replace magic numbers with named constants in Binary Trie

Replace hardcoded values (31, 32, 256) with named constants throughout
the Binary Trie implementation for improved code maintainability.

- Add NodeWidth (256), StemSize (31), HashSize (32), BitmapSize (32), NodeTypeBytes (1) constants
- Update all serialization, deserialization, and node operations to use constants
- Revert error wrapping to maintain test compatibility

* feat: add GetBinaryTreeKeyBasicData function for Binary Trie

Implement GetBinaryTreeKeyBasicData function similar to GetBinaryTreeKeyCodeHash
but with offset 0 (BasicDataLeafKey) instead of 1, as per EIP-7864 tree embedding spec.

* fix(t8ntool, bintrie): leftovers overlooked from review addressed

* fix(bintrie): rename NodeWidth to StemNodeWidth throughout package

The constant NodeWidth was renamed to StemNodeWidth to better reflect
its purpose as the number of children per stem node (256 values).
This change ensures consistency across the Binary Trie implementation
and aligns with the EIP-7864 specification for Binary Merkle Tree format.

Changes made:
- Updated constant definition in binary_node.go from NodeWidth to StemNodeWidth
- Replaced all references across the bintrie package files
- Ensures compilation succeeds and all tests pass

This fix was necessary after recent refactoring of the Binary Trie code
that replaced the Verkle tree implementation at genesis.

---------

Co-authored-by: Guillaume Ballet <[email protected]>

fix: implement InsertValuesAtStem for HashedNode in Binary Trie (#554)

This fixes the "insertValuesAtStem not implemented for hashed node" error
that was blocking contract creation tests in Binary Trie mode.

The implementation follows the pattern established in InternalNode.GetValuesAtStem:
1. Generate the path for the node's position in the tree
2. Resolve the hashed node to get actual node data from the database
3. Deserialize the resolved data into a concrete node type
4. Call InsertValuesAtStem on the resolved node

This allows the Binary Trie to handle cases where parts of the tree are not
in memory but need to be modified during state transitions, maintaining the
lazy-loading design.
@gballet gballet force-pushed the evm-t8n-stateless-at-genesis branch from 50ba20c to ea17332 Compare October 30, 2025 13:43
@gballet gballet marked this pull request as ready for review October 30, 2025 13:43
@gballet gballet changed the title cmd/evm/internal/t8ntool: support for verkle-at-genesis cmd/evm/internal/t8ntool, trie: support for verkle-at-genesis, use UBT, and move the transition tree to its own package Oct 30, 2025
Co-authored-by: Ignacio Hagopian <[email protected]>

fix tests in ethereum#32445 (#548)

* fix tests

* fix MakePreState not respecting verkle flag

bring in verkle iterator and verkle subcommand (#549)

push missing verkle iterator files (#550)

fix rebase issue

cmd/evm/internal/t8n: Binary at genesis support - Verkle <-> Binary swap (#552)

* remove: Verkle-related files with no codebase deps

* fixup

* refactor: update Prestate and transition logic for binary trie support

- Introduced a new field `BT` in the `Prestate` struct to accommodate binary trie data.
- Updated the `MakePreState` function to replace the `verkle` flag with `isBintrie` for better clarity.
- Renamed functions and variables related to Verkle to their binary trie counterparts, including `BinKey`, `BinKeys`, and `BinTrieRoot`.
- Added TODO comments for further updates and clarifications regarding the transition from Verkle to binary trie structures.

* feat: add ChunkifyCode function for EVM bytecode chunking

- Introduced the `ChunkedCode` type to represent 32-byte chunks of EVM bytecode.
- Implemented the `ChunkifyCode` function to convert a byte array of EVM bytecode into chunked format.
- Updated the `UpdateContractCode` method to utilize the new `ChunkifyCode` function.

* refactor: rename Verkle commands to Binary Trie equivalents

We leave the command name equal to keep support with https://github.com/ethereum/execution-spec-tests/tree/verkle/main

* refactor: update flags and transition logic for Binary Trie support

- Renamed `OutputVKTFlag` to `OutputBTFlag`.
- Renamed `InputVKTFlag` to `InputBTFlag` and updated its usage description for prestate input.
- Adjusted `Transition` function to utilize the new `BT` field for Binary Trie data instead of Verkle.
- Updated `dispatchOutput` function to handle Binary Trie output correctly.

* refactor: update DumpVKTLeaves to DumpBinTrieLeaves and adjust trie usage

* refactor: update functions and comments for Binary Trie implementation

- Replaced references to Verkle Trie with Binary Trie in various functions.
- Updated `genBinTrieFromAlloc` to return a Binary Trie instead of a Verkle Trie.
- Adjusted output functions to utilize Binary Trie methods and updated associated comments.

* refactor: rename flags for Binary Trie integration

* feat: add BinaryCodeChunkKey and BinaryCodeChunkCode functions for code chunking

- Introduced `BinaryCodeChunkKey` to compute the tree key of a code-chunk for a given address.
- Added `BinaryCodeChunkCode` to return the chunkification of bytecode.

* feat: introduce TransitionTrie for Binary Trie integration

- Added a new `TransitionTrie` type to facilitate the integration of the Binary Trie with the existing MPT trie.
- Updated the `newTrieReader` function to utilize the new `TransitionTrie` implementation.
- The transition tree has been moved to its own package
 `core/state/transitiontrie`, resolving the import cycle issue we get into when including `BinaryTrie` within `database.go`/

* refactor: update references to TransitionTrie and BinaryTrie integration

- Replaced instances of `bintrie.BinaryTrie` with `transitiontrie.TransitionTrie` in the `execution.go`, `dump.go`, and `reader.go`.
- Adjusted the `newTrieReader` function with atemporary workaround for import cycles.

* trie/bintrie: fix StemNode serialization to only return actual data

This fixes the "invalid serialized node length" error that occurred when
Binary Trie nodes were persisted and later reloaded from the database.

Issue Discovery:
The error manifested when running execution-spec-tests with Binary Trie mode.
After committing state changes, attempting to create a new StateDB with the
committed root would fail with "invalid serialized node length" when
deserializing the root node.

Root Cause Analysis:
Through debug logging, discovered that:
1. StemNodes were being serialized with 97 bytes of actual data
2. But the SerializeNode function was returning the entire 8224-byte buffer
3. When the node was later loaded and deserialized, it received 97 bytes
4. The deserializer expected at least 128 bytes for the bitmap and values
5. This mismatch caused the deserialization to fail

The Fix:
Changed SerializeNode to return only the actual data (serialized[:offset])
instead of the entire pre-allocated buffer (serialized[:]). This ensures
that only the meaningful bytes are persisted to the database.

This aligns the serialization with the deserialization logic, which
correctly handles variable-length StemNode data based on which values
are actually present in the node.

* Fix Binary Trie StemNode serialization array size calculation

This fixes a critical off-by-one error in the StemNode serialization buffer
that was causing "invalid serialized node length" errors during execution-spec-tests.

The error was discovered when running execution-spec-tests with Binary Trie mode:
```
cd ../execution-spec-tests && uv run fill --fork Verkle -v -m blockchain_test \
  -k test_contract_creation -n 1 --evm-bin=PATH_TO/go-ethereum/evm
```

The tests were failing with:
```
ERROR: error getting prestate: invalid serialized node length
```

Through systematic debugging with hex dumps and bitmap analysis, we discovered
that the serialized array was incorrectly sized:

**Incorrect**: `var serialized [32 + 32 + 256*32]byte`
- This allocated: 32 + 32 + 8192 = 8256 bytes
- Missing 1 byte for the node type prefix

**Correct**: `var serialized [1 + 31 + 32 + 256*32]byte`
- This allocates: 1 + 31 + 32 + 8192 = 8256 bytes
- Properly accounts for all fields per EIP-7864

The layout should be:
- 1 byte: node type (nodeTypeStem)
- 31 bytes: stem (path prefix)
- 32 bytes: bitmap indicating which of 256 values are present
- Up to 256*32 bytes: the actual values (32 bytes each)

1. Corrected array size from `[32 + 32 + 256*32]` to `[1 + 31 + 32 + 256*32]`
2. Cleaned up type assertions to use `n` from the type switch instead of `node.(*StemNode)`
3. This ensures proper alignment of all fields during serialization

The misalignment was causing the deserializer to interpret value data as bitmap data,
leading to impossible bitmap patterns (e.g., 122 bits set requiring 3904 bytes when
only 97 bytes were available).

After this fix, the "invalid serialized node length" errors are completely resolved.
The execution-spec-tests now progress past the serialization stage, confirming that
Binary Trie nodes are being correctly serialized and deserialized.

* various tweaks

1. move the transition trie package to trie, as this will be requested
by the geth team and it's where it belongs.
2. panic if state creation has failed in execution.go, to catch issues
early.
3. use a BinaryTrie and not a TransitionTrie, as evm t8n doesn't support
state trie transitions at this stage.

* more replacements of TransitionTrie with VerkleTrie

1. TransitionTrie doesn't work at this stage, we are only testing the
binary trees at genesis, so let's make sure TransitionTrie isn't
activated anywhere.
2. There was a superfluous check for Transitioned(), which actually made
the code take the wrong turn: if the verkle fork has occured, and if the
transition isn't in progress, it means that the conversion has ended*

* to be complete, it could be that this is in the period before the
start of the iterator sweep, but in that case the transition tree also
needs to be returned. So there is a missing granularity in this code,
but it's ok at this stage since the transition isn't supported yet.

* fix(state): Use BinaryTrie instead of VerkleTrie when IsVerkle is set

The codebase uses IsVerkle flag to indicate Binary Trie mode (not renamed
to avoid large diff). However, OpenTrie was incorrectly creating a VerkleTrie
which expects Verkle node format, while we store Binary Trie nodes.

This mismatch caused "invalid serialized node length" errors when VerkleTrie
tried to deserialize Binary Trie nodes.

We left some instantiation of `VerkleTree` arround which we have found
and changed too here.

* fix(bintrie): Remove incorrect type assertion in BinaryTrie.Commit

The Commit function incorrectly assumed that the root node was always
an InternalNode, causing a panic when the root was a StemNode:
"interface conversion: bintrie.BinaryNode is *bintrie.StemNode, not *bintrie.InternalNode"

Changes:
- Remove type assertion `root := t.root.(*InternalNode)`
- Call CollectNodes directly on t.root which works for any BinaryNode type
- Add comment explaining the root can be any BinaryNode type

This fix allows BinaryTrie to properly commit trees where the root is
a StemNode, which can happen in small tries or during initial setup.

* fix(t8ntool): Add error handling and debug logging to MakePreState

Previously, state.New errors were silently ignored, leading to nil pointer
panics later when statedb was accessed. This made debugging difficult as
the actual error was hidden.

Changes:
- Add explicit error checking when creating initial statedb
- Add explicit error checking when re-opening statedb after commit
- Include meaningful error messages with context (e.g., root hash)

* fix(bintrie): Fix iterator to properly handle StemNode leaf values

This commit fixes two in the BinaryTrie iterator:

1. Fixed Leaf() method to only return true when positioned at a specific
   non-nil value within a StemNode, not just at the StemNode itself.
   The iterator tracks position using an Index that points to the NEXT
   position after the current value.

2. Fixed stack underflow in Next() method by checking if we're at the
   root before popping from the stack. This prevents index out of range
   errors when iterating through the tree.

These fixes resolve panics in DumpBinTrieLeaves when generating VKT
output for Binary Tries in the t8n tool.

* fix: handle nil child nodes in BinaryTrie InternalNode

When inserting values into an InternalNode with nil children, the code
was attempting to dereference nil pointers, causing panics. This fix
ensures that nil children are initialized to Empty{} nodes before
attempting to call methods on them.

This resolves crashes when building the Binary Trie from an empty or
sparse initial state.

* fix: preserve Binary Trie in Apply after commit

When in Binary Trie mode (isEIP4762), the state.New() call after
commit was creating a new StateDB with MPT trie, losing the Binary
Trie. This resulted in nil trie when attempting to dump Binary Trie
leaves.

Fix: Skip state.New() for Binary Trie mode since the trie is already
correct after commit. Only reopen state for MPT mode.

* fix(t8n): preserve Binary Trie after commit and use correct JSON field name

This commit fixes two issues that prevented VKT (Binary Trie) data from
being correctly generated and passed to execution-spec-tests:

1. Binary Trie Preservation (execution.go):
   After statedb.Commit(), the code was calling state.New() which
   recreated the StateDB with an MPT trie, losing the Binary Trie.
   For Binary Trie mode (EIP-4762), we now skip state.New() since
   the trie is already correct after commit.

2. JSON Field Name (transition.go):
   The dispatch function was using name="bt" which created a JSON
   output field called "bt", but execution-spec-tests expects the
   field to be called "vkt" for compatibility with the upstream
   implementation. Changed to use name="vkt" instead. So translating,
   I'm dumb and I shouldn't have changed them..

These fixes allow execution-spec-tests to successfully extract Binary
Trie leaf data from t8n output via stdout JSON parsing.

Fixes test failures in:
- tests/verkle/eip6800_genesis_verkle_tree/test_contract_creation.py

* fix(t8n): revert change to avoid hardcoded MPT creation.

Prior, the code was working such that we were getting `nil` when re-opening. We weren't using `TransitionTree` thus reader and other interfaces were working hardcoded to MPT. This was fixed in prev. commits. Thus, this was no longer needed.

* fix(t8ntool, bintrie): address PR review comments

* fix(t8ntool): update JSON field name for Binary Trie in Prestate

There was a leftover as we modified already in `transition.go` flagnames to be `vkt` again and prevent errors for now.

* refactor: use named constants for binary node serialization sizes

Replace magic numbers with named constants for better code clarity:
- NodeTypeBytes = 1 (size of node type prefix)
- HashSize = 32 (size of a hash)
- BitmapSize = 32 (size of the bitmap in stem nodes)

This makes the serialization format more self-documenting and easier
to maintain.

* refactor: replace magic numbers with named constants in Binary Trie

Replace hardcoded values (31, 32, 256) with named constants throughout
the Binary Trie implementation for improved code maintainability.

- Add NodeWidth (256), StemSize (31), HashSize (32), BitmapSize (32), NodeTypeBytes (1) constants
- Update all serialization, deserialization, and node operations to use constants
- Revert error wrapping to maintain test compatibility

* feat: add GetBinaryTreeKeyBasicData function for Binary Trie

Implement GetBinaryTreeKeyBasicData function similar to GetBinaryTreeKeyCodeHash
but with offset 0 (BasicDataLeafKey) instead of 1, as per EIP-7864 tree embedding spec.

* fix(t8ntool, bintrie): leftovers overlooked from review addressed

* fix(bintrie): rename NodeWidth to StemNodeWidth throughout package

The constant NodeWidth was renamed to StemNodeWidth to better reflect
its purpose as the number of children per stem node (256 values).
This change ensures consistency across the Binary Trie implementation
and aligns with the EIP-7864 specification for Binary Merkle Tree format.

Changes made:
- Updated constant definition in binary_node.go from NodeWidth to StemNodeWidth
- Replaced all references across the bintrie package files
- Ensures compilation succeeds and all tests pass

This fix was necessary after recent refactoring of the Binary Trie code
that replaced the Verkle tree implementation at genesis.

---------

Co-authored-by: Guillaume Ballet <[email protected]>

fix: implement InsertValuesAtStem for HashedNode in Binary Trie (#554)

This fixes the "insertValuesAtStem not implemented for hashed node" error
that was blocking contract creation tests in Binary Trie mode.

The implementation follows the pattern established in InternalNode.GetValuesAtStem:
1. Generate the path for the node's position in the tree
2. Resolve the hashed node to get actual node data from the database
3. Deserialize the resolved data into a concrete node type
4. Call InsertValuesAtStem on the resolved node

This allows the Binary Trie to handle cases where parts of the tree are not
in memory but need to be modified during state transitions, maintaining the
lazy-loading design.
@gballet gballet force-pushed the evm-t8n-stateless-at-genesis branch from ea17332 to a0e6f53 Compare October 30, 2025 13:49
log.Crit("Both 'hash' and 'path' mode are configured")
}
if config.PathDB != nil {
if config.PathDB != nil || config.IsVerkle {
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Any particular reason to add this condition?

I would prefer to always explicitly specify the setting for path database for verkle cases.

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The Binary Trie implementation requires the pathdb backend (not hashdb). Adding || config.IsVerkle provides automatic pathdb selection when Binary Trie is enabled, even if PathDB config isn't explicitly provided.

You prefer to embed this within some Default behaviour or similar?

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I still think we shouldn't automatically select path mode when Verkle is true. It’s a minor point, but it feels odd to force the use of the path database for the binary tree.

BinaryTree should be totally compatible with hash mode.

MergeNetsplitBlock: big.NewInt(0),
TerminalTotalDifficulty: big.NewInt(0),
ShanghaiTime: u64(0),
VerkleTime: u64(0),
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Just to confirm it's intentional, for verkle, the tests are based on the Shanghai fork right? Specifically the Cancun, Prague and Osaka will all be deactivated

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

yes, because our testing branch is an offshoot of the execution spec-tests rooted at shanghai. When they complete the "MELD" we will rebase our branch and re-root the thing at prague/osaka

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Yes indeed! That is the case for now!

Comment on lines 227 to 236
if s.trie == nil {
trie, err := s.db.OpenTrie(s.originalRoot)
if err != nil {
return err
}
s.trie = trie
}

it, err := s.trie.(*bintrie.BinaryTrie).NodeIterator(nil)
if err != nil {
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Please don't populate the trie referenced by the stateDB.

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

You can create a standalone binary tree with trie, err := s.db.OpenTrie(s.originalRoot) and operate with it

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

That's my bad! Thanks for the catch! Still new to Go and Geth so it's taking me some time to get up to speed with best practices!

collector := make(Alloc)
s.DumpToCollector(collector, nil)
return dispatchOutput(ctx, baseDir, result, collector, body)
var btleaves map[common.Hash]hexutil.Bytes
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

originally we dump out the states (accounts and storage slots) with s.DumpToCollector(collector, nil), i guess it's no longer possible with binarytrie right?

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Correct. Binary Trie fundamentally cannot enumerate storage slots (it only supports key-based access). That's why we use DumpBinTrieLeaves to output leaf nodes in VKT format instead of the traditional alloc format.

This honestly is a pain. And one of the reasons for my previous comment (#32445 (comment))

This will need some investigation as the issue is not just doing it, is doing it on a performant way..
Help is always welcome 😛

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

it could be done by keeping some metadata in the tree, that is only used for tests and not hashed. It'd be ok as long as the state isn't too large. But that brings the question of what would then be tested, the production code, or the tests themselves? I want to think more about this.

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

by which I mean, let's not hold merging this PR for this.

"\t<file> - into the file <file> ",
Value: "vkt.json",
}
OutputWitnessFlag = &cli.StringFlag{
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

It's not used, maybe remove it from this PR?

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

True! Plus we aren't even testing witgen at all when comparing against Besu.

Pre types.GenesisAlloc `json:"pre"`
Env stEnv `json:"env"`
Pre types.GenesisAlloc `json:"pre"`
BT map[common.Hash]hexutil.Bytes `json:"vkt,omitempty"`
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Please elaborate the use case of using the specific binaryTrie data as the pre-state.

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I thought mostly formatting and ease of use/test.

But can definitely change my mind here.

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

same thing as the other comment: it's impossible to go from a tree leaf to an account, so you need to rebuild the tree from scratch at each new block.

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

How about we use a more general name? TreeLeaves ?

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I did the renaming, but I also looked into the possibility of caching the accounts/slots and I don't think it makes sense to do that: I would have to cache the data at some global variable since the tree is opened independently multiple times (e.g. in system contracts), which means I would have to pass that global cache all over the code and that is a very invasive change for something we hope can disappear.

@rjl493456442
Copy link
Member

rjl493456442 commented Nov 3, 2025

Please apply these nitpick diffs

diff --git a/cmd/evm/internal/t8ntool/transition.go b/cmd/evm/internal/t8ntool/transition.go
index c47561e57b..3f148cd1e4 100644
--- a/cmd/evm/internal/t8ntool/transition.go
+++ b/cmd/evm/internal/t8ntool/transition.go
@@ -42,6 +42,7 @@ import (
 	"github.com/ethereum/go-ethereum/tests"
 	"github.com/ethereum/go-ethereum/trie/bintrie"
 	"github.com/ethereum/go-ethereum/triedb"
+	"github.com/ethereum/go-ethereum/triedb/database"
 	"github.com/holiman/uint256"
 	"github.com/urfave/cli/v2"
 )
@@ -118,13 +119,14 @@ func Transition(ctx *cli.Context) error {
 		}
 	}
 	prestate.Pre = inputData.Alloc
+
 	if btStr != stdinSelector && btStr != "" {
 		if err := readFile(btStr, "BT", &inputData.BT); err != nil {
 			return err
 		}
 	}
-
 	prestate.BT = inputData.BT
+
 	// Set the block environment
 	if envStr != stdinSelector {
 		var env stEnv
@@ -195,8 +197,10 @@ func Transition(ctx *cli.Context) error {
 		return err
 	}
 	// Dump the execution result
-	collector := make(Alloc)
-	var btleaves map[common.Hash]hexutil.Bytes
+	var (
+		collector = make(Alloc)
+		btleaves  map[common.Hash]hexutil.Bytes
+	)
 	isBinary := chainConfig.IsVerkle(big.NewInt(int64(prestate.Env.Number)), prestate.Env.Timestamp)
 	if !isBinary {
 		s.DumpToCollector(collector, nil)
@@ -382,9 +386,7 @@ func dispatchOutput(ctx *cli.Context, baseDir string, result *ExecutionResult, a
 	return nil
 }
 
-// The logic for tree key
-// BinKey computes the tree key given an address and an optional
-// slot number.
+// BinKey computes the tree key given an address and an optional slot number.
 func BinKey(ctx *cli.Context) error {
 	if ctx.Args().Len() == 0 || ctx.Args().Len() > 2 {
 		return errors.New("invalid number of arguments: expecting an address and an optional slot number")
@@ -423,8 +425,10 @@ func BinKeys(ctx *cli.Context) error {
 			return err
 		}
 	}
+	db := triedb.NewDatabase(rawdb.NewMemoryDatabase(), triedb.VerkleDefaults)
+	defer db.Close()
 
-	bt, err := genBinTrieFromAlloc(alloc)
+	bt, err := genBinTrieFromAlloc(alloc, db)
 	if err != nil {
 		return fmt.Errorf("error generating bt: %w", err)
 	}
@@ -465,8 +469,10 @@ func BinTrieRoot(ctx *cli.Context) error {
 			return err
 		}
 	}
+	db := triedb.NewDatabase(rawdb.NewMemoryDatabase(), triedb.VerkleDefaults)
+	defer db.Close()
 
-	bt, err := genBinTrieFromAlloc(alloc)
+	bt, err := genBinTrieFromAlloc(alloc, db)
 	if err != nil {
 		return fmt.Errorf("error generating bt: %w", err)
 	}
@@ -476,15 +482,11 @@ func BinTrieRoot(ctx *cli.Context) error {
 }
 
 // TODO(@CPerezz): Should this go to `bintrie` module?
-func genBinTrieFromAlloc(alloc core.GenesisAlloc) (*bintrie.BinaryTrie, error) {
-	bt, err := bintrie.NewBinaryTrie(types.EmptyBinaryHash, triedb.NewDatabase(rawdb.NewMemoryDatabase(),
-		&triedb.Config{
-			IsVerkle: true,
-		}))
+func genBinTrieFromAlloc(alloc core.GenesisAlloc, db database.NodeDatabase) (*bintrie.BinaryTrie, error) {
+	bt, err := bintrie.NewBinaryTrie(types.EmptyBinaryHash, db)
 	if err != nil {
 		return nil, err
 	}
-
 	for addr, acc := range alloc {
 		for slot, value := range acc.Storage {
 			err := bt.UpdateStorage(addr, slot.Bytes(), value.Big().Bytes())
@@ -492,7 +494,6 @@ func genBinTrieFromAlloc(alloc core.GenesisAlloc) (*bintrie.BinaryTrie, error) {
 				return nil, fmt.Errorf("error inserting storage: %w", err)
 			}
 		}
-
 		account := &types.StateAccount{
 			Balance:  uint256.MustFromBig(acc.Balance),
 			Nonce:    acc.Nonce,
@@ -503,7 +504,6 @@ func genBinTrieFromAlloc(alloc core.GenesisAlloc) (*bintrie.BinaryTrie, error) {
 		if err != nil {
 			return nil, fmt.Errorf("error inserting account: %w", err)
 		}
-
 		err = bt.UpdateContractCode(addr, common.BytesToHash(account.CodeHash), acc.Code)
 		if err != nil {
 			return nil, fmt.Errorf("error inserting code: %w", err)
diff --git a/core/state/dump.go b/core/state/dump.go
index 4a17d1adde..829d106ed3 100644
--- a/core/state/dump.go
+++ b/core/state/dump.go
@@ -18,6 +18,7 @@ package state
 
 import (
 	"encoding/json"
+	"errors"
 	"fmt"
 	"time"
 
@@ -224,17 +225,17 @@ func (s *StateDB) DumpToCollector(c DumpCollector, conf *DumpConfig) (nextKey []
 
 // DumpBinTrieLeaves collects all binary trie leaf nodes into the provided map.
 func (s *StateDB) DumpBinTrieLeaves(collector map[common.Hash]hexutil.Bytes) error {
-	if s.trie == nil {
-		trie, err := s.db.OpenTrie(s.originalRoot)
-		if err != nil {
-			return err
-		}
-		s.trie = trie
+	tr, err := s.db.OpenTrie(s.originalRoot)
+	if err != nil {
+		return err
 	}
-
-	it, err := s.trie.(*bintrie.BinaryTrie).NodeIterator(nil)
+	btr, ok := tr.(*bintrie.BinaryTrie)
+	if !ok {
+		return errors.New("trie is not a binary trie")
+	}
+	it, err := btr.NodeIterator(nil)
 	if err != nil {
-		panic(err)
+		return err
 	}
 	for it.Next(true) {
 		if it.Leaf() {
diff --git a/core/state/reader.go b/core/state/reader.go
index fb23c84837..93083c8ae2 100644
--- a/core/state/reader.go
+++ b/core/state/reader.go
@@ -276,7 +276,6 @@ func newTrieReader(root common.Hash, db *triedb.Database, cache *utils.PointCach
 			// by using TransitionTrie when there's no actual transition happening.
 			tr = transitiontrie.NewTransitionTrie(nil, binTrie, false)
 		}
-		err = binErr
 	}
 	if err != nil {
 		return nil, err
diff --git a/tests/block_test_util.go b/tests/block_test_util.go
index 99ce051573..2ced18787a 100644
--- a/tests/block_test_util.go
+++ b/tests/block_test_util.go
@@ -118,8 +118,8 @@ func (t *BlockTest) Run(snapshotter bool, scheme string, witness bool, tracer *t
 	}
 	// import pre accounts & construct test genesis block & state root
 	// Commit genesis state
-	gspec := t.genesis(config)
 	var (
+		gspec = t.genesis(config)
 		db    = rawdb.NewMemoryDatabase()
 		tconf = &triedb.Config{
 			Preimages: true,

@gballet gballet force-pushed the evm-t8n-stateless-at-genesis branch from 05600fc to e94d14c Compare November 3, 2025 19:42
core/genesis.go Outdated
}
// Commit newly generated states into disk if it's not empty.
if root != types.EmptyRootHash {
if root != types.EmptyRootHash && root != types.EmptyVerkleHash {
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

if root != emptyRoot {

}

}
// Commit and re-open to start with a clean state.
root, err := statedb.Commit(0, false, false)
mptRoot, err := statedb.Commit(0, false, false)
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

why change the var name here?

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

to differentiate between the MPT root and a verkle root, this is not apparent here but for the transition there will be two concurrent roots. But I will revert for now, since there's not going to be any "rebase".

}
statedb, err = state.New(root, sdb)
// If bintrie mode started, check if conversion happened
if isBintrie {
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

In the MPT, the statedb re-init is essential, as after the commit, the mutated nodes are all converted as the hash and de-reference from the trie.

The only way to access the them is re-open the trie with new root hash.

I am not sure how it's handled in the binaryTrie. I guess it somehow still reference the new nodes after the commit.

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

After Commit(), all mutated nodes are converted to hash nodes (line 763 in trie.go: t.root = newCommitter(...).Commit(...)).

  1. The committer replaces all dirty nodes with their hash representations
  2. The trie becomes unusable because nodes are now just hashes, not full nodes
    That's why we need to re-open the trie with state.New(root, sdb) to get a fresh trie that can load nodes from the database.

In Binary Trie (Verkle/Binary):

  • The ``Commit()` method collects nodes but doesn't convert them to hashes
  • The trie maintains its in-memory node structure after commit
  • The trie remains usable after commit because nodes are still accessible

So you are right, we should check if we're in Bintrie EIP. And if so, do this, otherwise, if we are using MPT we should avoid doing that.

Does that make sense @gballet ?

Value: "block.json",
}
OutputBTFlag = &cli.StringFlag{
Name: "output.vkt",
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

output.vkt is confusing, it essentially refers to the file storing the trie leaves right? It doesn't matter it's verkle/bintree/mpt

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

agreed, but the reason why we keep this is because we are dependent on a branch of execution spec tests that is no longer maintained and it has all these fields hardcoded.

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

so I'm banking on the fact this code can be removed soon.

Pre types.GenesisAlloc `json:"pre"`
Env stEnv `json:"env"`
Pre types.GenesisAlloc `json:"pre"`
BT map[common.Hash]hexutil.Bytes `json:"vkt,omitempty"`
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

How about we use a more general name? TreeLeaves ?

log.Crit("Both 'hash' and 'path' mode are configured")
}
if config.PathDB != nil {
if config.PathDB != nil || config.IsVerkle {
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I still think we shouldn't automatically select path mode when Verkle is true. It’s a minor point, but it feels odd to force the use of the path database for the binary tree.

BinaryTree should be totally compatible with hash mode.

@gballet gballet added this to the 1.16.8 milestone Nov 14, 2025
@gballet gballet merged commit 2a2f106 into ethereum:master Nov 14, 2025
10 of 13 checks passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants