mirror of
https://github.com/ethereum/go-ethereum.git
synced 2026-07-11 07:24:27 +00:00
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>
263 lines
7.2 KiB
Go
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
|
|
}
|