diff --git a/.github/workflows/parallel_benchmark.yml b/.github/workflows/parallel_benchmark.yml new file mode 100644 index 0000000000..b92c2d1dc4 --- /dev/null +++ b/.github/workflows/parallel_benchmark.yml @@ -0,0 +1,36 @@ +name: Parallel Execution Benchmark + +on: + pull_request: + branches: + - master + +jobs: + benchmark: + name: Run Parallel Benchmarks + runs-on: ubuntu-latest + permissions: + contents: write # Results will be pushed back to the PR + steps: + - uses: actions/checkout@v4 + with: + submodules: false + ref: ${{ github.event.pull_request.head.ref }} + + - name: Set up Go + uses: actions/setup-go@v5 + with: + go-version: '1.24' + cache: true + + - name: Run benchmark + run: | + # The benchmark outputs results to BENCHMARK_OUTPUT_FILE + export BENCHMARK_OUTPUT_FILE=$GITHUB_WORKSPACE/tests/benchmarks/parallel_results.txt + go test -v ./tests -run TestParallelBenchmarkOutput -timeout 15m + + - name: Commit benchmark results + uses: stefanzweifel/git-auto-commit-action@v5 + with: + commit_message: "chore: update parallel execution benchmark results" + file_pattern: "tests/benchmarks/parallel_results.txt" diff --git a/core/parallel_flags.go b/core/parallel_flags.go new file mode 100644 index 0000000000..07c9fe3618 --- /dev/null +++ b/core/parallel_flags.go @@ -0,0 +1,5 @@ +package core + +// ParallelTxGroupingByStorageOverlap controls whether the state processor +// runs transaction execution in parallel using storage overlap grouping. +var ParallelTxGroupingByStorageOverlap = false diff --git a/tests/benchmarks/parallel_results.txt b/tests/benchmarks/parallel_results.txt new file mode 100644 index 0000000000..e69de29bb2 diff --git a/tests/parallel_execution_bench_test.go b/tests/parallel_execution_bench_test.go new file mode 100644 index 0000000000..e9d3e4f86b --- /dev/null +++ b/tests/parallel_execution_bench_test.go @@ -0,0 +1,340 @@ +package tests + +import ( + "fmt" + "math/big" + "os" + "path/filepath" + "testing" + "time" + + "github.com/ethereum/go-ethereum/common" + "github.com/ethereum/go-ethereum/consensus/ethash" + "github.com/ethereum/go-ethereum/core" + "github.com/ethereum/go-ethereum/core/rawdb" + "github.com/ethereum/go-ethereum/core/types" + "github.com/ethereum/go-ethereum/crypto" + "github.com/ethereum/go-ethereum/params" +) + +func TestParallelBenchmarkOutput(t *testing.T) { + outPath := os.Getenv("BENCHMARK_OUTPUT_FILE") + if outPath == "" { + t.Skip("BENCHMARK_OUTPUT_FILE not set, skipping benchmark tracking output") + } + + txCounts := []int{50, 150, 300, 450, 600} + txDepsTypes := []string{"Isolated", "Contended", "Mixed"} + + var results []string + dateStr := time.Now().Format("2006-01-02") + + for _, n := range txCounts { + for _, dep := range txDepsTypes { + var blocks []*types.Block + var genesis *core.Genesis + var engine *ethash.Ethash + + switch dep { + case "Isolated": + blocks, genesis, engine = createIsolatedBlock(t, n) + case "Contended": + blocks, genesis, engine = createContendedBlock(t, n) + case "Mixed": + blocks, genesis, engine = createMixedBlock(t, n) + } + + // Sequential Run + core.ParallelTxGroupingByStorageOverlap = false + seqTime := timeInsert(t, blocks, genesis, engine) + + // Parallel Run + core.ParallelTxGroupingByStorageOverlap = true + parTime := timeInsert(t, blocks, genesis, engine) + + speedup := float64(seqTime) / float64(parTime) + seqAvgMs := (seqTime.Seconds() / float64(n)) * 1000 + parAvgMs := (parTime.Seconds() / float64(n)) * 1000 + resLine := fmt.Sprintf("[%s][%d][%s] - Sequential: %.3fs (%.2fms/tx), Parallel: %.3fs (%.2fms/tx), Speedup: %.2fx", + dateStr, n, dep, seqTime.Seconds(), seqAvgMs, parTime.Seconds(), parAvgMs, speedup) + t.Log(resLine) + results = append(results, resLine) + } + } + + err := os.MkdirAll(filepath.Dir(outPath), 0755) + if err != nil { + t.Fatalf("failed to create dir: %v", err) + } + f, err := os.OpenFile(outPath, os.O_APPEND|os.O_CREATE|os.O_WRONLY, 0644) + if err != nil { + t.Fatalf("failed to open output file: %v", err) + } + defer f.Close() + + for _, res := range results { + if _, err := f.WriteString(res + "\n"); err != nil { + t.Fatalf("failed to write result: %v", err) + } + } +} + +func timeInsert(t *testing.T, blocks []*types.Block, genesis *core.Genesis, engine *ethash.Ethash) time.Duration { + options := &core.BlockChainConfig{ + TrieCleanLimit: 256, + TrieDirtyLimit: 256, + TrieTimeLimit: 5 * time.Minute, + SnapshotLimit: 0, + Preimages: true, + ArchiveMode: true, + } + + chain, err := core.NewBlockChain(rawdb.NewMemoryDatabase(), genesis, engine, options) + if err != nil { + t.Fatalf("create chain: %v", err) + } + defer chain.Stop() + + // Warm up + if n, err := chain.InsertChain(blocks[:1]); err != nil { + t.Fatalf("warmup block %d failed: %v", n, err) + } + + start := time.Now() + // Insert the actual block with transactions + if n, err := chain.InsertChain(blocks[1:]); err != nil { + t.Fatalf("benchmark block %d failed: %v", n, err) + } + return time.Since(start) +} + +func createIsolatedBlock(t *testing.T, n int) ([]*types.Block, *core.Genesis, *ethash.Ethash) { + keys, addrs := newTestAccounts(t, n+1) + deployKey := keys[0] + deployFrom := addrs[0] + + contractABI := mustReadContractABI(t) + contractBin := mustReadContractBinBlockTest(t) + + contractAddrs := make([]common.Address, n) + for i := 0; i < n; i++ { + contractAddrs[i] = crypto.CreateAddress(deployFrom, uint64(i)) + } + + genesis := &core.Genesis{ + Config: params.AllEthashProtocolChanges, + Alloc: genesisAllocEther(addrs[:]...), + GasLimit: uint64(n+1) * 20_000_000, + BaseFee: big.NewInt(params.InitialBaseFee), + } + + engine := ethash.NewFaker() + + // Create chain sequentially first to avoid race conditions during test setup + core.ParallelTxGroupingByStorageOverlap = false + + _, blocks, _ := core.GenerateChainWithGenesis(genesis, engine, 2, func(i int, b *core.BlockGen) { + switch i { + case 0: + for nonce := uint64(0); nonce < uint64(n); nonce++ { + deployTx := mustSignAccessListTx(t, params.AllEthashProtocolChanges, deployKey, &types.AccessListTx{ + ChainID: params.AllEthashProtocolChanges.ChainID, + Nonce: nonce, + Gas: 12_000_000, + GasPrice: b.BaseFee(), + Data: contractBin, + }) + b.AddTx(deployTx) + } + case 1: + const rounds int64 = 1000 + for i := 0; i < n; i++ { + lane := int64(i + 1) + senderKey := keys[i+1] + contract := contractAddrs[i] + data, _ := contractABI.Pack("isolatedJob", big.NewInt(lane), big.NewInt(1), big.NewInt(rounds)) + tx := mustSignAccessListTx(t, params.AllEthashProtocolChanges, senderKey, &types.AccessListTx{ + ChainID: params.AllEthashProtocolChanges.ChainID, + Nonce: 0, + To: &contract, + Gas: 18_000_000, + GasPrice: b.BaseFee(), + Data: data, + AccessList: isolatedAccessList(contract, uint64(lane)), + }) + b.AddTx(tx) + } + } + }) + return blocks, genesis, engine +} + +func createContendedBlock(t *testing.T, n int) ([]*types.Block, *core.Genesis, *ethash.Ethash) { + keys, addrs := newTestAccounts(t, n+1) + deployKey := keys[0] + deployFrom := addrs[0] + + contractABI := mustReadContractABI(t) + contractBin := mustReadContractBinBlockTest(t) + + contractAddr := crypto.CreateAddress(deployFrom, 0) + + genesis := &core.Genesis{ + Config: params.AllEthashProtocolChanges, + Alloc: genesisAllocEther(addrs[:]...), + GasLimit: uint64(n+1) * 20_000_000, + BaseFee: big.NewInt(params.InitialBaseFee), + } + + engine := ethash.NewFaker() + + core.ParallelTxGroupingByStorageOverlap = false + + _, blocks, _ := core.GenerateChainWithGenesis(genesis, engine, 2, func(i int, b *core.BlockGen) { + switch i { + case 0: + deployTx := mustSignAccessListTx(t, params.AllEthashProtocolChanges, deployKey, &types.AccessListTx{ + ChainID: params.AllEthashProtocolChanges.ChainID, + Nonce: 0, + Gas: 12_000_000, + GasPrice: b.BaseFee(), + Data: contractBin, + }) + b.AddTx(deployTx) + case 1: + const rounds int64 = 1000 + contendedAccess := types.AccessList{{ + Address: contractAddr, + StorageKeys: []common.Hash{ + common.BigToHash(big.NewInt(2)), + common.BigToHash(big.NewInt(3)), + }, + }} + for i := 0; i < n; i++ { + senderKey := keys[i+1] + data, _ := contractABI.Pack("contendedJob", big.NewInt(1), big.NewInt(rounds)) + tx := mustSignAccessListTx(t, params.AllEthashProtocolChanges, senderKey, &types.AccessListTx{ + ChainID: params.AllEthashProtocolChanges.ChainID, + Nonce: 0, + To: &contractAddr, + Gas: 18_000_000, + GasPrice: b.BaseFee(), + Data: data, + AccessList: contendedAccess, + }) + b.AddTx(tx) + } + } + }) + return blocks, genesis, engine +} + +func createMixedBlock(t *testing.T, n int) ([]*types.Block, *core.Genesis, *ethash.Ethash) { + keys, addrs := newTestAccounts(t, n+1) + deployKey := keys[0] + deployFrom := addrs[0] + + contractABI := mustReadContractABI(t) + contractBin := mustReadContractBinBlockTest(t) + + cShared := crypto.CreateAddress(deployFrom, 0) + + // Half shared, half isolated. The shared contract is deployed at nonce 0, + // the rest of the isolated contracts at nonces 1.. n/2. + numIsolated := n / 2 + numContended := n - numIsolated + + cIsolatedAddrs := make([]common.Address, numIsolated) + for i := 0; i < numIsolated; i++ { + cIsolatedAddrs[i] = crypto.CreateAddress(deployFrom, uint64(i+1)) + } + + genesis := &core.Genesis{ + Config: params.AllEthashProtocolChanges, + Alloc: genesisAllocEther(addrs[:]...), + GasLimit: uint64(n+1) * 20_000_000, + BaseFee: big.NewInt(params.InitialBaseFee), + } + + engine := ethash.NewFaker() + + core.ParallelTxGroupingByStorageOverlap = false + + _, blocks, _ := core.GenerateChainWithGenesis(genesis, engine, 2, func(i int, b *core.BlockGen) { + switch i { + case 0: + // Deploy shared contract + deployTx0 := mustSignAccessListTx(t, params.AllEthashProtocolChanges, deployKey, &types.AccessListTx{ + ChainID: params.AllEthashProtocolChanges.ChainID, + Nonce: 0, + Gas: 12_000_000, + GasPrice: b.BaseFee(), + Data: contractBin, + }) + b.AddTx(deployTx0) + + // Deploy isolated contracts + for i := 0; i < numIsolated; i++ { + deployTx := mustSignAccessListTx(t, params.AllEthashProtocolChanges, deployKey, &types.AccessListTx{ + ChainID: params.AllEthashProtocolChanges.ChainID, + Nonce: uint64(i + 1), + Gas: 12_000_000, + GasPrice: b.BaseFee(), + Data: contractBin, + }) + b.AddTx(deployTx) + } + case 1: + const rounds int64 = 1000 + + mixedAccess := func(caddr common.Address, lane uint64) types.AccessList { + return append( + isolatedAccessList(caddr, lane), + types.AccessTuple{ + Address: caddr, + StorageKeys: []common.Hash{ + common.BigToHash(big.NewInt(2)), + common.BigToHash(big.NewInt(3)), + }, + }, + ) + } + + // Add contended transactions + for i := 0; i < numContended; i++ { + senderKey := keys[i+1] + lane := int64(i + 1) + data, _ := contractABI.Pack("mixedJob", big.NewInt(lane), big.NewInt(lane+1), big.NewInt(rounds)) + tx := mustSignAccessListTx(t, params.AllEthashProtocolChanges, senderKey, &types.AccessListTx{ + ChainID: params.AllEthashProtocolChanges.ChainID, + Nonce: 0, + To: &cShared, + Gas: 18_000_000, + GasPrice: b.BaseFee(), + Data: data, + AccessList: mixedAccess(cShared, uint64(lane)), + }) + b.AddTx(tx) + } + + // Add isolated transactions + for i := 0; i < numIsolated; i++ { + senderKey := keys[numContended+1+i] + lane := int64(i + 1) + contract := cIsolatedAddrs[i] + data, _ := contractABI.Pack("isolatedJob", big.NewInt(lane), big.NewInt(1), big.NewInt(rounds)) + tx := mustSignAccessListTx(t, params.AllEthashProtocolChanges, senderKey, &types.AccessListTx{ + ChainID: params.AllEthashProtocolChanges.ChainID, + Nonce: 0, + To: &contract, + Gas: 18_000_000, + GasPrice: b.BaseFee(), + Data: data, + AccessList: isolatedAccessList(contract, uint64(lane)), + }) + b.AddTx(tx) + } + } + }) + return blocks, genesis, engine +} diff --git a/tests/parallel_vm_block_test.go b/tests/parallel_vm_block_test.go index f019746552..071ac18a27 100644 --- a/tests/parallel_vm_block_test.go +++ b/tests/parallel_vm_block_test.go @@ -1,28 +1,19 @@ package tests import ( - "bytes" "crypto/ecdsa" - "errors" "math/big" - "os" - "path/filepath" - "strings" "testing" "time" - "github.com/ethereum/go-ethereum/accounts/abi" "github.com/ethereum/go-ethereum/common" "github.com/ethereum/go-ethereum/consensus/ethash" "github.com/ethereum/go-ethereum/core" "github.com/ethereum/go-ethereum/core/rawdb" "github.com/ethereum/go-ethereum/core/state" - "github.com/ethereum/go-ethereum/core/tracing" "github.com/ethereum/go-ethereum/core/types" "github.com/ethereum/go-ethereum/crypto" "github.com/ethereum/go-ethereum/params" - "github.com/ethereum/go-ethereum/triedb" - "github.com/holiman/uint256" ) // Gas budget for TestParallelVMBlockAccessListIsolated: block limit must exceed the @@ -344,178 +335,4 @@ func TestParallelVMBlockAccessListMixed(t *testing.T) { }) } -func runAndCheckChain(t *testing.T, genesis *core.Genesis, engine *ethash.Ethash, blocks []*types.Block, check func(*state.StateDB)) { - options := &core.BlockChainConfig{ - TrieCleanLimit: 256, - TrieDirtyLimit: 256, - TrieTimeLimit: 5 * time.Minute, - SnapshotLimit: 0, - Preimages: true, - ArchiveMode: true, - } - chain, err := core.NewBlockChain(rawdb.NewMemoryDatabase(), genesis, engine, options) - if err != nil { - t.Fatalf("create chain: %v", err) - } - defer chain.Stop() - - if n, err := chain.InsertChain(blocks); err != nil { - t.Fatalf("block %d failed: %s", n, formatInsertChainErrorForDebug(err)) - } - - statedb := mustStateAtCurrentHead(t, chain) - check(statedb) -} - -// formatInsertChainErrorForDebug prints the full errors.Unwrap chain (outer → inner) -// and adds a file hint for known core errors so test failures are easier to trace. -func formatInsertChainErrorForDebug(err error) string { - var b strings.Builder - b.WriteString(err.Error()) - for u := errors.Unwrap(err); u != nil; u = errors.Unwrap(u) { - b.WriteString("\n ← unwrap: ") - b.WriteString(u.Error()) - } - switch { - case errors.Is(err, core.ErrNonceTooHigh): - b.WriteString("\n [origin] core/state_transition.go — (*stateTransition).preCheck when !msg.SkipNonceChecks (tx nonce > state nonce)") - case errors.Is(err, core.ErrNonceTooLow): - b.WriteString("\n [origin] core/state_transition.go — (*stateTransition).preCheck when !msg.SkipNonceChecks") - case errors.Is(err, core.ErrInsufficientFunds): - b.WriteString("\n [origin] core/state_transition.go — (*stateTransition).buyGas or balance checks in preCheck") - } - return b.String() -} - -func newTestAccount(t *testing.T) (*ecdsa.PrivateKey, common.Address) { - t.Helper() - - key, err := crypto.GenerateKey() - if err != nil { - t.Fatalf("generate key: %v", err) - } - return key, crypto.PubkeyToAddress(key.PublicKey) -} - -func newTestAccounts(t *testing.T, n int) ([]*ecdsa.PrivateKey, []common.Address) { - t.Helper() - keys := make([]*ecdsa.PrivateKey, n) - addrs := make([]common.Address, n) - for i := 0; i < n; i++ { - keys[i], addrs[i] = newTestAccount(t) - } - return keys, addrs -} - -func genesisAllocEther(addrs ...common.Address) types.GenesisAlloc { - bal := new(big.Int).Mul(big.NewInt(1_000_000), big.NewInt(params.Ether)) - alloc := make(types.GenesisAlloc, len(addrs)) - for _, a := range addrs { - alloc[a] = types.Account{Balance: bal} - } - return alloc -} - -func mustReadContractABI(t *testing.T) abi.ABI { - t.Helper() - - abiPath := filepath.Join("contracts", "ParallelVMTest.abi") - abiBytes, err := os.ReadFile(abiPath) - if err != nil { - t.Fatalf("read ABI: %v", err) - } - parsedABI, err := abi.JSON(bytes.NewReader(abiBytes)) - if err != nil { - t.Fatalf("parse ABI: %v", err) - } - return parsedABI -} - -func mustReadContractBinBlockTest(t *testing.T) []byte { - t.Helper() - - binPath := filepath.Join("contracts", "ParallelVMTest.bin") - binBytes, err := os.ReadFile(binPath) - if err != nil { - t.Fatalf("read bytecode: %v", err) - } - hexStr := strings.TrimSpace(string(binBytes)) - if strings.HasPrefix(hexStr, "0x") { - hexStr = hexStr[2:] - } - return common.FromHex("0x" + hexStr) -} - -func mustSignAccessListTx(t *testing.T, cfg *params.ChainConfig, key *ecdsa.PrivateKey, txdata *types.AccessListTx) *types.Transaction { - t.Helper() - - signer := types.LatestSigner(cfg) - tx := types.NewTx(txdata) - signed, err := types.SignTx(tx, signer, key) - if err != nil { - t.Fatalf("sign tx: %v", err) - } - return signed -} - -func isolatedAccessList(contract common.Address, lane uint64) types.AccessList { - return types.AccessList{ - { - Address: contract, - StorageKeys: []common.Hash{ - mappingSlotBlock(lane, 0), // laneValue[lane] - mappingSlotBlock(lane, 1), // laneDigest[lane] - }, - }, - } -} - -func mappingSlotBlock(key uint64, baseSlot uint64) common.Hash { - return crypto.Keccak256Hash( - common.LeftPadBytes(new(big.Int).SetUint64(key).Bytes(), 32), - common.LeftPadBytes(new(big.Int).SetUint64(baseSlot).Bytes(), 32), - ) -} - -func assertStorageUintBlock(t *testing.T, statedb *state.StateDB, addr common.Address, slot common.Hash, want uint64) { - t.Helper() - - got := statedb.GetState(addr, slot).Big().Uint64() - if got != want { - t.Fatalf("slot %s got %d want %d", slot.Hex(), got, want) - } -} - -func mustStateAtCurrentHead(t *testing.T, chain *core.BlockChain) *state.StateDB { - t.Helper() - - root := chain.CurrentBlock().Root - - // Most branches expose one of these two shapes: - // 1) chain.StateAt(root) - // 2) rebuild using state.New(root, state.NewDatabase(...)) - // - // Try the direct API first if your branch has it. - statedb, err := chain.StateAt(root) - if err == nil { - return statedb - } - - // Fallback path for branches without StateAt. - db := rawdb.NewMemoryDatabase() - tdb := triedb.NewDatabase(db, &triedb.Config{Preimages: true}) - sdb := state.NewDatabase(tdb, nil) - st, err2 := state.New(root, sdb) - if err2 != nil { - t.Fatalf("state lookup failed: direct=%v fallback=%v", err, err2) - } - return st -} - -// This exists only to keep imports aligned if your branch keeps tracing/uint256 -// in nearby tests and gofmt otherwise complains when you copy patterns around. -var ( - _ = tracing.BalanceChangeUnspecified - _ = uint256.NewInt -) diff --git a/tests/parallel_vm_test_util.go b/tests/parallel_vm_test_util.go new file mode 100644 index 0000000000..e8aaa11390 --- /dev/null +++ b/tests/parallel_vm_test_util.go @@ -0,0 +1,202 @@ +package tests + +import ( + "bytes" + "crypto/ecdsa" + "errors" + "math/big" + "os" + "path/filepath" + "strings" + "testing" + "time" + + "github.com/ethereum/go-ethereum/accounts/abi" + "github.com/ethereum/go-ethereum/common" + "github.com/ethereum/go-ethereum/consensus/ethash" + "github.com/ethereum/go-ethereum/core" + "github.com/ethereum/go-ethereum/core/rawdb" + "github.com/ethereum/go-ethereum/core/state" + "github.com/ethereum/go-ethereum/core/tracing" + "github.com/ethereum/go-ethereum/core/types" + "github.com/ethereum/go-ethereum/crypto" + "github.com/ethereum/go-ethereum/params" + "github.com/ethereum/go-ethereum/triedb" + "github.com/holiman/uint256" +) + +func runAndCheckChain(t *testing.T, genesis *core.Genesis, engine *ethash.Ethash, blocks []*types.Block, check func(*state.StateDB)) { + options := &core.BlockChainConfig{ + TrieCleanLimit: 256, + TrieDirtyLimit: 256, + TrieTimeLimit: 5 * time.Minute, + SnapshotLimit: 0, + Preimages: true, + ArchiveMode: true, + } + + chain, err := core.NewBlockChain(rawdb.NewMemoryDatabase(), genesis, engine, options) + if err != nil { + t.Fatalf("create chain: %v", err) + } + defer chain.Stop() + + if n, err := chain.InsertChain(blocks); err != nil { + t.Fatalf("block %d failed: %s", n, formatInsertChainErrorForDebug(err)) + } + + statedb := mustStateAtCurrentHead(t, chain) + check(statedb) +} + +// formatInsertChainErrorForDebug prints the full errors.Unwrap chain (outer → inner) +// and adds a file hint for known core errors so test failures are easier to trace. +func formatInsertChainErrorForDebug(err error) string { + var b strings.Builder + b.WriteString(err.Error()) + for u := errors.Unwrap(err); u != nil; u = errors.Unwrap(u) { + b.WriteString("\n ← unwrap: ") + b.WriteString(u.Error()) + } + switch { + case errors.Is(err, core.ErrNonceTooHigh): + b.WriteString("\n [origin] core/state_transition.go — (*stateTransition).preCheck when !msg.SkipNonceChecks (tx nonce > state nonce)") + case errors.Is(err, core.ErrNonceTooLow): + b.WriteString("\n [origin] core/state_transition.go — (*stateTransition).preCheck when !msg.SkipNonceChecks") + case errors.Is(err, core.ErrInsufficientFunds): + b.WriteString("\n [origin] core/state_transition.go — (*stateTransition).buyGas or balance checks in preCheck") + } + return b.String() +} + +func newTestAccount(t *testing.T) (*ecdsa.PrivateKey, common.Address) { + t.Helper() + + key, err := crypto.GenerateKey() + if err != nil { + t.Fatalf("generate key: %v", err) + } + return key, crypto.PubkeyToAddress(key.PublicKey) +} + +func newTestAccounts(t *testing.T, n int) ([]*ecdsa.PrivateKey, []common.Address) { + t.Helper() + keys := make([]*ecdsa.PrivateKey, n) + addrs := make([]common.Address, n) + for i := 0; i < n; i++ { + keys[i], addrs[i] = newTestAccount(t) + } + return keys, addrs +} + +func genesisAllocEther(addrs ...common.Address) types.GenesisAlloc { + bal := new(big.Int).Mul(big.NewInt(1_000_000), big.NewInt(params.Ether)) + alloc := make(types.GenesisAlloc, len(addrs)) + for _, a := range addrs { + alloc[a] = types.Account{Balance: bal} + } + return alloc +} + +func mustReadContractABI(t *testing.T) abi.ABI { + t.Helper() + + abiPath := filepath.Join("contracts", "ParallelVMTest.abi") + abiBytes, err := os.ReadFile(abiPath) + if err != nil { + t.Fatalf("read ABI: %v", err) + } + parsedABI, err := abi.JSON(bytes.NewReader(abiBytes)) + if err != nil { + t.Fatalf("parse ABI: %v", err) + } + return parsedABI +} + +func mustReadContractBinBlockTest(t *testing.T) []byte { + t.Helper() + + binPath := filepath.Join("contracts", "ParallelVMTest.bin") + binBytes, err := os.ReadFile(binPath) + if err != nil { + t.Fatalf("read bytecode: %v", err) + } + hexStr := strings.TrimSpace(string(binBytes)) + if strings.HasPrefix(hexStr, "0x") { + hexStr = hexStr[2:] + } + return common.FromHex("0x" + hexStr) +} + +func mustSignAccessListTx(t *testing.T, cfg *params.ChainConfig, key *ecdsa.PrivateKey, txdata *types.AccessListTx) *types.Transaction { + t.Helper() + + signer := types.LatestSigner(cfg) + tx := types.NewTx(txdata) + signed, err := types.SignTx(tx, signer, key) + if err != nil { + t.Fatalf("sign tx: %v", err) + } + return signed +} + +func isolatedAccessList(contract common.Address, lane uint64) types.AccessList { + return types.AccessList{ + { + Address: contract, + StorageKeys: []common.Hash{ + mappingSlotBlock(lane, 0), // laneValue[lane] + mappingSlotBlock(lane, 1), // laneDigest[lane] + }, + }, + } +} + +func mappingSlotBlock(key uint64, baseSlot uint64) common.Hash { + return crypto.Keccak256Hash( + common.LeftPadBytes(new(big.Int).SetUint64(key).Bytes(), 32), + common.LeftPadBytes(new(big.Int).SetUint64(baseSlot).Bytes(), 32), + ) +} + +func assertStorageUintBlock(t *testing.T, statedb *state.StateDB, addr common.Address, slot common.Hash, want uint64) { + t.Helper() + + got := statedb.GetState(addr, slot).Big().Uint64() + if got != want { + t.Fatalf("slot %s got %d want %d", slot.Hex(), got, want) + } +} + +func mustStateAtCurrentHead(t *testing.T, chain *core.BlockChain) *state.StateDB { + t.Helper() + + root := chain.CurrentBlock().Root + + // Most branches expose one of these two shapes: + // 1) chain.StateAt(root) + // 2) rebuild using state.New(root, state.NewDatabase(...)) + // + // Try the direct API first if your branch has it. + statedb, err := chain.StateAt(root) + if err == nil { + return statedb + } + + // Fallback path for branches without StateAt. + db := rawdb.NewMemoryDatabase() + tdb := triedb.NewDatabase(db, &triedb.Config{Preimages: true}) + sdb := state.NewDatabase(tdb, nil) + st, err2 := state.New(root, sdb) + if err2 != nil { + t.Fatalf("state lookup failed: direct=%v fallback=%v", err, err2) + } + return st +} + +// This exists only to keep imports aligned if your branch keeps tracing/uint256 +// in nearby tests and gofmt otherwise complains when you copy patterns around. +var ( + _ = tracing.BalanceChangeUnspecified + _ = uint256.NewInt +)