go-ethereum/trie/triecompare/stategen.go
weiihann d61dd875d8 nomt: add triecompare package and fix sort.SliceStable bug in stem grouping
Add trie/triecompare/ package with realistic state generation and cross-
validation tests proving NOMT produces identical roots as bintrie at scale
(10K+ accounts, PowerLaw/Uniform/Exponential distributions, multi-block).

Fix a subtle bug in groupAndHashStems: sort.Slice was used instead of
sort.SliceStable, causing non-deterministic results when the same account
is mutated twice in a single block (duplicate stem+suffix entries need
last-writer-wins ordering preserved).

Tests: 5 correctness tests + 4 benchmarks + storage footprint comparison.
All pass with race detector clean.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-02-13 00:07:04 +08:00

263 lines
7.2 KiB
Go

// Package triecompare provides realistic Ethereum state generation and
// comparison tests between bintrie and NOMT trie implementations.
//
// The state generation logic is ported from the state-actor repository's
// generator patterns, using PowerLaw/Uniform/Exponential distributions
// to mimic mainnet-like storage slot distributions.
package triecompare
import (
"bytes"
"math"
mrand "math/rand"
"sort"
"github.com/ethereum/go-ethereum/common"
"github.com/ethereum/go-ethereum/core/types"
"github.com/holiman/uint256"
)
// Distribution represents the storage slot distribution strategy.
type Distribution int
const (
// PowerLaw distribution — most contracts have few slots, few have many.
// Mimics real Ethereum where contracts like Uniswap have millions of
// slots while most have very few. Uses Pareto inverse CDF (alpha=1.5).
PowerLaw Distribution = iota
// Uniform distribution — all contracts have similar slot counts.
Uniform
// Exponential distribution — exponential decay in slot counts.
Exponential
)
// StateGenConfig configures synthetic state generation.
type StateGenConfig struct {
NumAccounts int // Number of EOA accounts
NumContracts int // Number of contract accounts
MinSlots int // Minimum storage slots per contract
MaxSlots int // Maximum storage slots per contract
CodeSize int // Average contract code size in bytes
Distribution Distribution // Slot distribution strategy
Seed int64 // Deterministic random seed
}
// OpKind discriminates between state operation types.
type OpKind int
const (
OpUpdateAccount OpKind = iota
OpUpdateStorage
OpUpdateCode
)
// StateOp represents a single state operation to apply to a trie.
type StateOp struct {
Kind OpKind
Address common.Address
Account *types.StateAccount // populated for OpUpdateAccount
CodeLen int // code length for OpUpdateAccount
Code []byte // populated for OpUpdateCode
Slot []byte // 32-byte key for OpUpdateStorage
Value []byte // raw value for OpUpdateStorage
}
// GenerateBlocks produces deterministic blocks of state operations.
// Block 0 = initial state creation (all accounts, storage, code).
// Block 1 = incremental mutations (nonce bumps, balance changes, storage mods).
func GenerateBlocks(cfg StateGenConfig) [][]StateOp {
rng := mrand.New(mrand.NewSource(cfg.Seed))
// Block 0: initial state.
block0 := generateInitialState(rng, cfg)
// Block 1: incremental mutations on a subset of addresses.
block1 := generateMutations(rng, cfg, block0)
return [][]StateOp{block0, block1}
}
// generateInitialState creates the full initial state: EOAs, contracts
// with storage and code.
func generateInitialState(rng *mrand.Rand, cfg StateGenConfig) []StateOp {
estimatedOps := cfg.NumAccounts*1 + cfg.NumContracts*3
ops := make([]StateOp, 0, estimatedOps)
emptyCodeHash := common.HexToHash(
"c5d2460186f7233c927e7db2dcc703c0e500b653ca82273b7bfad8045d85a470",
)
// EOA accounts.
for range cfg.NumAccounts {
var addr common.Address
rng.Read(addr[:])
acc := &types.StateAccount{
Nonce: uint64(rng.Intn(1000)),
Balance: new(uint256.Int).Mul(uint256.NewInt(uint64(rng.Intn(1000))), uint256.NewInt(1e18)),
CodeHash: emptyCodeHash[:],
}
ops = append(ops, StateOp{
Kind: OpUpdateAccount,
Address: addr,
Account: acc,
CodeLen: 0,
})
}
// Contract accounts with storage and code.
slotDist := generateSlotDistribution(rng, cfg)
for i := range cfg.NumContracts {
var addr common.Address
rng.Read(addr[:])
// Generate code.
codeSize := cfg.CodeSize + rng.Intn(max(cfg.CodeSize, 1))
code := make([]byte, codeSize)
rng.Read(code)
acc := &types.StateAccount{
Nonce: uint64(rng.Intn(1000)),
Balance: new(uint256.Int).Mul(uint256.NewInt(uint64(rng.Intn(100))), uint256.NewInt(1e18)),
CodeHash: emptyCodeHash[:],
}
// Account update (with code length for basicData encoding).
ops = append(ops, StateOp{
Kind: OpUpdateAccount,
Address: addr,
Account: acc,
CodeLen: codeSize,
})
// Code update.
ops = append(ops, StateOp{
Kind: OpUpdateCode,
Address: addr,
Code: code,
})
// Storage slots.
numSlots := slotDist[i]
for range numSlots {
slot := make([]byte, 32)
rng.Read(slot)
val := make([]byte, 32)
rng.Read(val)
// Ensure non-zero value (matches state-actor behavior).
if val[0] == 0 && val[31] == 0 {
val[0] = 0x01
}
ops = append(ops, StateOp{
Kind: OpUpdateStorage,
Address: addr,
Slot: slot,
Value: val,
})
}
}
return ops
}
// generateMutations creates incremental state changes on a subset of
// addresses from block 0. Modifies ~10% of accounts with nonce bumps,
// balance changes, and new storage slots.
func generateMutations(rng *mrand.Rand, cfg StateGenConfig, block0 []StateOp) []StateOp {
// Collect unique addresses from block 0.
addrSet := make(map[common.Address]bool, cfg.NumAccounts+cfg.NumContracts)
for i := range block0 {
if block0[i].Kind == OpUpdateAccount {
addrSet[block0[i].Address] = true
}
}
addrs := make([]common.Address, 0, len(addrSet))
for addr := range addrSet {
addrs = append(addrs, addr)
}
// Sort for deterministic iteration (map order is random in Go).
sort.Slice(addrs, func(i, j int) bool {
return bytes.Compare(addrs[i][:], addrs[j][:]) < 0
})
// Mutate ~10% of addresses.
numMutations := max(len(addrs)/10, 1)
ops := make([]StateOp, 0, numMutations*2)
emptyCodeHash := common.HexToHash(
"c5d2460186f7233c927e7db2dcc703c0e500b653ca82273b7bfad8045d85a470",
)
for range numMutations {
addr := addrs[rng.Intn(len(addrs))]
// Nonce bump + balance change.
acc := &types.StateAccount{
Nonce: uint64(1000 + rng.Intn(1000)),
Balance: new(uint256.Int).Mul(uint256.NewInt(uint64(rng.Intn(500))), uint256.NewInt(1e18)),
CodeHash: emptyCodeHash[:],
}
ops = append(ops, StateOp{
Kind: OpUpdateAccount,
Address: addr,
Account: acc,
CodeLen: 0,
})
// Add a new storage slot.
slot := make([]byte, 32)
rng.Read(slot)
val := make([]byte, 32)
rng.Read(val)
if val[0] == 0 && val[31] == 0 {
val[0] = 0x01
}
ops = append(ops, StateOp{
Kind: OpUpdateStorage,
Address: addr,
Slot: slot,
Value: val,
})
}
return ops
}
// generateSlotDistribution returns the number of storage slots for each
// contract based on the configured distribution strategy.
// Ported from state-actor/generator/generator.go:1056-1092.
func generateSlotDistribution(rng *mrand.Rand, cfg StateGenConfig) []int {
dist := make([]int, cfg.NumContracts)
switch cfg.Distribution {
case PowerLaw:
alpha := 1.5
for i := range dist {
u := rng.Float64()
slots := float64(cfg.MinSlots) / math.Pow(1-u, 1/alpha)
if slots > float64(cfg.MaxSlots) {
slots = float64(cfg.MaxSlots)
}
dist[i] = int(slots)
}
case Exponential:
lambda := math.Log(2) / float64(cfg.MaxSlots/4)
for i := range dist {
u := rng.Float64()
slots := -math.Log(1-u) / lambda
slots = math.Max(float64(cfg.MinSlots), math.Min(slots, float64(cfg.MaxSlots)))
dist[i] = int(slots)
}
case Uniform:
for i := range dist {
dist[i] = cfg.MinSlots + rng.Intn(cfg.MaxSlots-cfg.MinSlots+1)
}
}
return dist
}