mirror of
https://github.com/ethereum/go-ethereum.git
synced 2026-07-21 20:26:41 +00:00
creating an automatic benchamrk for parallel execution
This commit is contained in:
parent
263d65e7ac
commit
4ed8da87df
6 changed files with 583 additions and 183 deletions
36
.github/workflows/parallel_benchmark.yml
vendored
Normal file
36
.github/workflows/parallel_benchmark.yml
vendored
Normal file
|
|
@ -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"
|
||||
5
core/parallel_flags.go
Normal file
5
core/parallel_flags.go
Normal file
|
|
@ -0,0 +1,5 @@
|
|||
package core
|
||||
|
||||
// ParallelTxGroupingByStorageOverlap controls whether the state processor
|
||||
// runs transaction execution in parallel using storage overlap grouping.
|
||||
var ParallelTxGroupingByStorageOverlap = false
|
||||
0
tests/benchmarks/parallel_results.txt
Normal file
0
tests/benchmarks/parallel_results.txt
Normal file
340
tests/parallel_execution_bench_test.go
Normal file
340
tests/parallel_execution_bench_test.go
Normal file
|
|
@ -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
|
||||
}
|
||||
|
|
@ -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
|
||||
)
|
||||
|
|
|
|||
202
tests/parallel_vm_test_util.go
Normal file
202
tests/parallel_vm_test_util.go
Normal file
|
|
@ -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
|
||||
)
|
||||
Loading…
Reference in a new issue