Skip to content

trie/bintrie: InternalNode is not goroutine-safe; concurrent storage updates race on shared trie #34708

Description

@CPerezz

Summary

trie/bintrie.InternalNode.InsertValuesAtStem mutates bt.left, bt.right, and bt.mustRecompute without synchronization. When core/state.IntermediateRoot parallelizes per-account updateTrie() calls via errgroup and the underlying hasher wraps a single shared *BinaryTrie (EIP-7864 unified layout), multiple goroutines call BinaryTrie.UpdateStorage concurrently on the same internal nodes — producing a data race.

Current state on master

Master's IntermediateRoot has an if s.db.TrieDB().IsVerkle() bypass that serializes all bintrie storage updates in a single sequential loop. This prevents the race today.

However, the bypass is a runtime workaround — InternalNode itself has no documentation or compile-time guard marking it as goroutine-unsafe. Any future refactor that drops or restructures the bypass (as happened on the bintrie-flat-state feature branch during a hasher-routed refactor in #34706 ) silently reintroduces the race.

Race trace (observed on bintrie-flat-state where the bypass was accidentally dropped)

WARNING: DATA RACE
Read at 0x... by goroutine 58:
  trie/bintrie.(*InternalNode).InsertValuesAtStem()
      trie/bintrie/internal_node.go:194

Previous write at 0x... by goroutine 59:
  trie/bintrie.(*InternalNode).InsertValuesAtStem()
      trie/bintrie/internal_node.go:211

Both goroutines are spawned by errgroup.Group.Go inside core/state.(*StateDB).IntermediateRoot at statedb.go:762.

Reproduced deterministically with go test ./core/state/ -race -run TestBintrieFlatStateConsistencyOracle (every run, not intermittent).

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Type

    No type
    No fields configured for issues without a type.

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions