Porting Itay's tests

This commit is contained in:
idotalk 2026-06-20 12:28:23 +03:00
parent dd1ebac117
commit d9f4968275
5 changed files with 1268 additions and 0 deletions

View file

@ -0,0 +1,323 @@
[
{
"anonymous": false,
"inputs": [
{
"indexed": false,
"internalType": "uint256",
"name": "oldValue",
"type": "uint256"
},
{
"indexed": false,
"internalType": "uint256",
"name": "newValue",
"type": "uint256"
},
{
"indexed": false,
"internalType": "bytes32",
"name": "oldDigest",
"type": "bytes32"
},
{
"indexed": false,
"internalType": "bytes32",
"name": "newDigest",
"type": "bytes32"
}
],
"name": "ContendedJob",
"type": "event"
},
{
"anonymous": false,
"inputs": [
{
"indexed": true,
"internalType": "uint256",
"name": "lane",
"type": "uint256"
},
{
"indexed": false,
"internalType": "uint256",
"name": "oldValue",
"type": "uint256"
},
{
"indexed": false,
"internalType": "uint256",
"name": "newValue",
"type": "uint256"
},
{
"indexed": false,
"internalType": "bytes32",
"name": "oldDigest",
"type": "bytes32"
},
{
"indexed": false,
"internalType": "bytes32",
"name": "newDigest",
"type": "bytes32"
}
],
"name": "IsolatedJob",
"type": "event"
},
{
"anonymous": false,
"inputs": [
{
"indexed": true,
"internalType": "uint256",
"name": "lane",
"type": "uint256"
},
{
"indexed": false,
"internalType": "uint256",
"name": "oldLaneValue",
"type": "uint256"
},
{
"indexed": false,
"internalType": "uint256",
"name": "newLaneValue",
"type": "uint256"
},
{
"indexed": false,
"internalType": "uint256",
"name": "oldGlobalValue",
"type": "uint256"
},
{
"indexed": false,
"internalType": "uint256",
"name": "newGlobalValue",
"type": "uint256"
}
],
"name": "MixedJob",
"type": "event"
},
{
"inputs": [
{
"internalType": "uint256",
"name": "addend",
"type": "uint256"
},
{
"internalType": "uint256",
"name": "rounds",
"type": "uint256"
}
],
"name": "contendedJob",
"outputs": [],
"stateMutability": "nonpayable",
"type": "function"
},
{
"inputs": [],
"name": "globalDigest",
"outputs": [
{
"internalType": "bytes32",
"name": "",
"type": "bytes32"
}
],
"stateMutability": "view",
"type": "function"
},
{
"inputs": [],
"name": "globalValue",
"outputs": [
{
"internalType": "uint256",
"name": "",
"type": "uint256"
}
],
"stateMutability": "view",
"type": "function"
},
{
"inputs": [
{
"internalType": "uint256",
"name": "lane",
"type": "uint256"
},
{
"internalType": "uint256",
"name": "addend",
"type": "uint256"
},
{
"internalType": "uint256",
"name": "rounds",
"type": "uint256"
}
],
"name": "isolatedJob",
"outputs": [],
"stateMutability": "nonpayable",
"type": "function"
},
{
"inputs": [
{
"internalType": "uint256",
"name": "",
"type": "uint256"
}
],
"name": "laneDigest",
"outputs": [
{
"internalType": "bytes32",
"name": "",
"type": "bytes32"
}
],
"stateMutability": "view",
"type": "function"
},
{
"inputs": [
{
"internalType": "uint256",
"name": "",
"type": "uint256"
}
],
"name": "laneValue",
"outputs": [
{
"internalType": "uint256",
"name": "",
"type": "uint256"
}
],
"stateMutability": "view",
"type": "function"
},
{
"inputs": [
{
"internalType": "uint256",
"name": "lane",
"type": "uint256"
},
{
"internalType": "uint256",
"name": "addend",
"type": "uint256"
},
{
"internalType": "uint256",
"name": "rounds",
"type": "uint256"
}
],
"name": "mixedJob",
"outputs": [],
"stateMutability": "nonpayable",
"type": "function"
},
{
"inputs": [
{
"internalType": "uint256",
"name": "lane",
"type": "uint256"
}
],
"name": "readLane",
"outputs": [
{
"internalType": "uint256",
"name": "value",
"type": "uint256"
},
{
"internalType": "bytes32",
"name": "digest",
"type": "bytes32"
}
],
"stateMutability": "view",
"type": "function"
},
{
"inputs": [],
"name": "slotForGlobalDigest",
"outputs": [
{
"internalType": "bytes32",
"name": "",
"type": "bytes32"
}
],
"stateMutability": "pure",
"type": "function"
},
{
"inputs": [],
"name": "slotForGlobalValue",
"outputs": [
{
"internalType": "bytes32",
"name": "",
"type": "bytes32"
}
],
"stateMutability": "pure",
"type": "function"
},
{
"inputs": [
{
"internalType": "uint256",
"name": "lane",
"type": "uint256"
}
],
"name": "slotForLaneDigest",
"outputs": [
{
"internalType": "bytes32",
"name": "",
"type": "bytes32"
}
],
"stateMutability": "pure",
"type": "function"
},
{
"inputs": [
{
"internalType": "uint256",
"name": "lane",
"type": "uint256"
}
],
"name": "slotForLaneValue",
"outputs": [
{
"internalType": "bytes32",
"name": "",
"type": "bytes32"
}
],
"stateMutability": "pure",
"type": "function"
}
]

File diff suppressed because one or more lines are too long

View file

@ -0,0 +1,176 @@
// SPDX-License-Identifier: MIT
pragma solidity ^0.8.19;
/// @title ParallelVMTest
/// @notice Contract for testing declared read/write-set based parallel execution.
///
/// Conflict classes:
/// - isolatedJob(lane, ...) touches only lane-local storage
/// - contendedJob(...) always touches the same global slots
/// - mixedJob(lane, ...) touches both lane-local and global storage
///
/// Storage layout:
/// - laneValue[lane] => keccak256(abi.encode(lane, 0))
/// - laneDigest[lane] => keccak256(abi.encode(lane, 1))
/// - globalValue => slot 2
/// - globalDigest => slot 3
contract ParallelVMTest {
// mapping(uint256 => uint256) at slot 0
mapping(uint256 => uint256) public laneValue;
// mapping(uint256 => bytes32) at slot 1
mapping(uint256 => bytes32) public laneDigest;
// fixed slots
uint256 public globalValue; // slot 2
bytes32 public globalDigest; // slot 3
event IsolatedJob(
uint256 indexed lane,
uint256 oldValue,
uint256 newValue,
bytes32 oldDigest,
bytes32 newDigest
);
event ContendedJob(
uint256 oldValue,
uint256 newValue,
bytes32 oldDigest,
bytes32 newDigest
);
event MixedJob(
uint256 indexed lane,
uint256 oldLaneValue,
uint256 newLaneValue,
uint256 oldGlobalValue,
uint256 newGlobalValue
);
/// @notice Touches only storage derived from `lane`.
/// Different lanes should be non-conflicting.
function isolatedJob(
uint256 lane,
uint256 addend,
uint256 rounds
) external {
uint256 oldValue = laneValue[lane];
bytes32 oldDigest = laneDigest[lane];
bytes32 newDigest = _work(
keccak256(abi.encodePacked("isolated", lane, oldValue, oldDigest, addend)),
rounds
);
uint256 newValue = oldValue + addend;
laneValue[lane] = newValue;
laneDigest[lane] = newDigest;
emit IsolatedJob(lane, oldValue, newValue, oldDigest, newDigest);
}
/// @notice Always touches the same global slots.
/// All calls conflict.
function contendedJob(
uint256 addend,
uint256 rounds
) external {
uint256 oldValue = globalValue;
bytes32 oldDigest = globalDigest;
bytes32 newDigest = _work(
keccak256(abi.encodePacked("contended", oldValue, oldDigest, addend)),
rounds
);
uint256 newValue = oldValue + addend;
globalValue = newValue;
globalDigest = newDigest;
emit ContendedJob(oldValue, newValue, oldDigest, newDigest);
}
/// @notice Touches lane-local storage and shared global storage.
/// Different lanes still conflict because of the shared globals.
function mixedJob(
uint256 lane,
uint256 addend,
uint256 rounds
) external {
uint256 oldLaneValue = laneValue[lane];
bytes32 oldLaneDigest = laneDigest[lane];
uint256 oldGlobalValue = globalValue;
bytes32 oldGlobalDigest = globalDigest;
bytes32 laneNewDigest = _work(
keccak256(
abi.encodePacked(
"mixed-lane",
lane,
oldLaneValue,
oldLaneDigest,
addend
)
),
rounds
);
bytes32 globalNewDigest = _work(
keccak256(
abi.encodePacked(
"mixed-global",
oldGlobalValue,
oldGlobalDigest,
addend
)
),
rounds
);
laneValue[lane] = oldLaneValue + addend;
laneDigest[lane] = laneNewDigest;
globalValue = oldGlobalValue + addend;
globalDigest = globalNewDigest;
emit MixedJob(
lane,
oldLaneValue,
oldLaneValue + addend,
oldGlobalValue,
oldGlobalValue + addend
);
}
function slotForLaneValue(uint256 lane) external pure returns (bytes32) {
return keccak256(abi.encode(lane, uint256(0)));
}
function slotForLaneDigest(uint256 lane) external pure returns (bytes32) {
return keccak256(abi.encode(lane, uint256(1)));
}
function slotForGlobalValue() external pure returns (bytes32) {
return bytes32(uint256(2));
}
function slotForGlobalDigest() external pure returns (bytes32) {
return bytes32(uint256(3));
}
function readLane(uint256 lane) external view returns (uint256 value, bytes32 digest) {
return (laneValue[lane], laneDigest[lane]);
}
function _work(bytes32 seed, uint256 rounds) internal pure returns (bytes32 x) {
x = seed;
unchecked {
for (uint256 i = 0; i < rounds; i++) {
x = keccak256(abi.encodePacked(x, i));
}
}
}
}

View file

@ -0,0 +1,521 @@
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
// sum of per-tx caps when multiple heavy txs share a block. Raising `rounds` in the
// isolatedJob calls may require increasing both constants.
//
// isolatedTestNumIndependentTxs is how many contracts are deployed (deployer nonces
// 0..N-1) and how many parallel-isolated txs run in block 2 (senders keys[1..N],
// contract lanes 1..N). Needs newTestAccounts(t, N+1) for one deployer + N senders.
const (
isolatedJobTxGasLimit = uint64(550_000_000)
isolatedTestNumIndependentTxs = 3
isolatedTestBlockGasLimit = uint64((isolatedTestNumIndependentTxs + 1) * isolatedJobTxGasLimit)
)
func TestParallelVMBlockAccessListIsolated(t *testing.T) {
n := isolatedTestNumIndependentTxs
if n < 1 {
t.Fatal("isolatedTestNumIndependentTxs must be at least 1")
}
keys, addrs := newTestAccounts(t, n+1)
deployKey := keys[0]
deployFrom := addrs[0]
contractABI := mustReadContractABI(t)
contractBin := mustReadContractBinBlockTest(t)
// N independent deployments; block-2 txs use distinct recipients and senders
// (pairwise disjoint declared addresses → one wave when grouping is enabled).
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: isolatedTestBlockGasLimit,
BaseFee: big.NewInt(params.InitialBaseFee),
}
engine := ethash.NewFaker()
// Block 1: deploy N contracts (deployer nonces 0..N-1).
// Block 2: each of N senders calls only its own contract (disjoint address sets).
_, 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,
To: nil,
Gas: 12_000_000,
GasPrice: b.BaseFee(),
Value: big.NewInt(0),
Data: contractBin,
AccessList: nil,
})
b.AddTx(deployTx)
}
case 1:
// Increase isolatedTestBlockGasLimit / isolatedJobTxGasLimit if you raise this.
const rounds int64 = 120000
for i := 0; i < n; i++ {
lane := int64(i + 1)
senderKey := keys[i+1]
contract := contractAddrs[i]
data, err := contractABI.Pack("isolatedJob", big.NewInt(lane), big.NewInt(1), big.NewInt(rounds))
if err != nil {
t.Fatalf("pack isolatedJob: %v", err)
}
tx := mustSignAccessListTx(t, params.AllEthashProtocolChanges, senderKey, &types.AccessListTx{
ChainID: params.AllEthashProtocolChanges.ChainID,
Nonce: 0,
To: &contract,
Gas: isolatedJobTxGasLimit,
GasPrice: b.BaseFee(),
Value: big.NewInt(0),
Data: data,
AccessList: isolatedAccessList(contract, uint64(lane)),
})
b.AddTx(tx)
}
}
})
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("failed to create blockchain: %v", err)
}
defer chain.Stop()
if insertN, err := chain.InsertChain(blocks); err != nil {
t.Fatalf("block %d failed to insert: %s", insertN, formatInsertChainErrorForDebug(err))
}
// Debug: receipt status / gas (e.g. OOG with high `rounds` shows failed receipts).
head := chain.CurrentBlock()
if head != nil {
if rs := chain.GetReceiptsByHash(head.Hash()); len(rs) > 0 {
// for i, r := range rs {
// status := "failed"
// if r.Status == types.ReceiptStatusSuccessful {
// status = "successful"
// }
// t.Logf("head block #%d tx %d: receipt status=%s (raw=%d) gasUsed=%d cumulativeGas=%d txHash=%s",
// head.Number.Uint64(), i, status, r.Status, r.GasUsed, r.CumulativeGasUsed, r.TxHash.Hex())
// }
} else {
t.Log("head block: no receipts returned (unexpected)")
}
}
// Read final state after block import.
statedb := mustStateAtCurrentHead(t, chain)
for i := 0; i < n; i++ {
lane := uint64(i + 1)
addr := contractAddrs[i]
assertStorageUintBlock(t, statedb, addr, mappingSlotBlock(lane, 0), 1)
assertStorageUintBlock(t, statedb, addr, common.BigToHash(big.NewInt(2)), 0)
}
}
// TestParallelVMBlockAccessListContended exercises true storage contention: three
// senders each call the same contract, which updates the same global slots (2, 3).
//
// Declared address sets all include that contract → address-parallel grouping must
// not put these txs in one wave ([0]|[1]|[2] with current greedy builder).
//
// Sequential / correctly ordered merge must leave globalValue == 3. Running these
// txs concurrently on one shared StateDB without proper isolation typically loses
// updates or corrupts state, so this check fails under broken parallelization.
func TestParallelVMBlockAccessListContended(t *testing.T) {
keys, addrs := newTestAccounts(t, 4)
deployKey, key1, key2, key3 := keys[0], keys[1], keys[2], keys[3]
deployFrom := addrs[0]
contractABI := mustReadContractABI(t)
contractBin := mustReadContractBinBlockTest(t)
contractAddr := crypto.CreateAddress(deployFrom, 0)
genesis := &core.Genesis{
Config: params.AllEthashProtocolChanges,
Alloc: genesisAllocEther(addrs[:]...),
GasLimit: 30_000_000,
BaseFee: big.NewInt(params.InitialBaseFee),
}
engine := ethash.NewFaker()
_, 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,
To: nil,
Gas: 12_000_000,
GasPrice: b.BaseFee(),
Data: contractBin,
})
b.AddTx(deployTx)
case 1:
const rounds int64 = 50
contendedAccess := types.AccessList{
{
Address: contractAddr,
StorageKeys: []common.Hash{
common.BigToHash(big.NewInt(2)),
common.BigToHash(big.NewInt(3)),
},
},
}
callers := []*ecdsa.PrivateKey{key1, key2, key3}
for _, ck := range callers {
data, _ := contractABI.Pack("contendedJob", big.NewInt(1), big.NewInt(rounds))
tx := mustSignAccessListTx(t, params.AllEthashProtocolChanges, ck, &types.AccessListTx{
ChainID: params.AllEthashProtocolChanges.ChainID,
Nonce: 0,
To: &contractAddr,
Gas: 8_000_000,
GasPrice: b.BaseFee(),
Data: data,
AccessList: contendedAccess,
})
b.AddTx(tx)
}
}
})
runAndCheckChain(t, genesis, engine, blocks, func(statedb *state.StateDB) {
assertStorageUintBlock(t, statedb, contractAddr, common.BigToHash(big.NewInt(2)), 3)
})
}
// TestParallelVMBlockAccessListMixed: tx1 and tx2 both call mixedJob on the same
// contract (shared globals + different lanes → declared address set overlaps).
// tx3 calls isolatedJob on a second deployment only; its declared addresses are
// disjoint from tx1 and tx2, so grouping may place tx3 in a different wave than
// the contended pair (e.g. [0,2] then [1] depending on greedy order).
func TestParallelVMBlockAccessListMixed(t *testing.T) {
keys, addrs := newTestAccounts(t, 4)
deployKey, key1, key2, key3 := keys[0], keys[1], keys[2], keys[3]
deployFrom := addrs[0]
contractABI := mustReadContractABI(t)
contractBin := mustReadContractBinBlockTest(t)
cShared := crypto.CreateAddress(deployFrom, 0)
cIsolated := crypto.CreateAddress(deployFrom, 1)
genesis := &core.Genesis{
Config: params.AllEthashProtocolChanges,
Alloc: genesisAllocEther(addrs[:]...),
GasLimit: 30_000_000,
BaseFee: big.NewInt(params.InitialBaseFee),
}
engine := ethash.NewFaker()
_, blocks, _ := core.GenerateChainWithGenesis(genesis, engine, 2, func(i int, b *core.BlockGen) {
switch i {
case 0:
for n := uint64(0); n < 2; n++ {
deployTx := mustSignAccessListTx(t, params.AllEthashProtocolChanges, deployKey, &types.AccessListTx{
ChainID: params.AllEthashProtocolChanges.ChainID,
Nonce: n,
To: nil,
Gas: 12_000_000,
GasPrice: b.BaseFee(),
Data: contractBin,
})
b.AddTx(deployTx)
}
case 1:
const rounds int64 = 50
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)),
},
},
)
}
// Tx1 & tx2: same cShared → contention on contract + global slots.
data1, _ := contractABI.Pack("mixedJob", big.NewInt(1), big.NewInt(2), big.NewInt(rounds))
tx1 := mustSignAccessListTx(t, params.AllEthashProtocolChanges, key1, &types.AccessListTx{
ChainID: params.AllEthashProtocolChanges.ChainID,
Nonce: 0,
To: &cShared,
Gas: 8_000_000,
GasPrice: b.BaseFee(),
Data: data1,
AccessList: mixedAccess(cShared, 1),
})
b.AddTx(tx1)
data2, _ := contractABI.Pack("mixedJob", big.NewInt(2), big.NewInt(3), big.NewInt(rounds))
tx2 := mustSignAccessListTx(t, params.AllEthashProtocolChanges, key2, &types.AccessListTx{
ChainID: params.AllEthashProtocolChanges.ChainID,
Nonce: 0,
To: &cShared,
Gas: 8_000_000,
GasPrice: b.BaseFee(),
Data: data2,
AccessList: mixedAccess(cShared, 2),
})
b.AddTx(tx2)
// Tx3: different contract only — disjoint from tx1/tx2 declared sets (aside from unique senders).
data3, err := contractABI.Pack("isolatedJob", big.NewInt(3), big.NewInt(1), big.NewInt(rounds))
if err != nil {
t.Fatalf("pack isolatedJob lane3: %v", err)
}
tx3 := mustSignAccessListTx(t, params.AllEthashProtocolChanges, key3, &types.AccessListTx{
ChainID: params.AllEthashProtocolChanges.ChainID,
Nonce: 0,
To: &cIsolated,
Gas: 8_000_000,
GasPrice: b.BaseFee(),
Value: big.NewInt(0),
Data: data3,
AccessList: isolatedAccessList(cIsolated, 3),
})
b.AddTx(tx3)
}
})
runAndCheckChain(t, genesis, engine, blocks, func(statedb *state.StateDB) {
// Sequential order 1 then 2 on cShared matches original single-contract mixed expectations.
assertStorageUintBlock(t, statedb, cShared, mappingSlotBlock(1, 0), 2)
assertStorageUintBlock(t, statedb, cShared, mappingSlotBlock(2, 0), 3)
assertStorageUintBlock(t, statedb, cShared, common.BigToHash(big.NewInt(2)), 5)
assertStorageUintBlock(t, statedb, cIsolated, mappingSlotBlock(3, 0), 1)
assertStorageUintBlock(t, statedb, cIsolated, common.BigToHash(big.NewInt(2)), 0)
})
}
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
)

247
tests/parallel_vm_test.go Normal file
View file

@ -0,0 +1,247 @@
package tests
import (
"bytes"
"crypto/ecdsa"
"math/big"
"os"
"path/filepath"
"strings"
"testing"
"github.com/ethereum/go-ethereum/accounts/abi"
"github.com/ethereum/go-ethereum/common"
"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/core/vm"
"github.com/ethereum/go-ethereum/crypto"
"github.com/ethereum/go-ethereum/params"
"github.com/ethereum/go-ethereum/triedb"
"github.com/holiman/uint256"
)
type parallelVMHarness struct {
statedb *state.StateDB
chainConfig *params.ChainConfig
privateKey *ecdsa.PrivateKey
from common.Address
contractAddr common.Address
nonce uint64
testABI abi.ABI
}
func TestParallelVMIsolatedBatch(t *testing.T) {
h := newParallelVMHarness(t)
h.deployContract(t)
const rounds int64 = 50
h.callContract(t, "isolatedJob", big.NewInt(1), big.NewInt(1), big.NewInt(rounds))
h.callContract(t, "isolatedJob", big.NewInt(2), big.NewInt(1), big.NewInt(rounds))
h.callContract(t, "isolatedJob", big.NewInt(3), big.NewInt(1), big.NewInt(rounds))
assertStorageUint(t, h.statedb, h.contractAddr, mappingSlot(1, 0), 1)
assertStorageUint(t, h.statedb, h.contractAddr, mappingSlot(2, 0), 1)
assertStorageUint(t, h.statedb, h.contractAddr, mappingSlot(3, 0), 1)
assertStorageUint(t, h.statedb, h.contractAddr, common.BigToHash(big.NewInt(2)), 0)
}
func TestParallelVMContendedBatch(t *testing.T) {
h := newParallelVMHarness(t)
h.deployContract(t)
const rounds int64 = 50
h.callContract(t, "contendedJob", big.NewInt(1), big.NewInt(rounds))
h.callContract(t, "contendedJob", big.NewInt(1), big.NewInt(rounds))
h.callContract(t, "contendedJob", big.NewInt(1), big.NewInt(rounds))
assertStorageUint(t, h.statedb, h.contractAddr, common.BigToHash(big.NewInt(2)), 3)
}
func TestParallelVMMixedBatch(t *testing.T) {
h := newParallelVMHarness(t)
h.deployContract(t)
const rounds int64 = 50
h.callContract(t, "mixedJob", big.NewInt(1), big.NewInt(2), big.NewInt(rounds))
h.callContract(t, "mixedJob", big.NewInt(2), big.NewInt(3), big.NewInt(rounds))
assertStorageUint(t, h.statedb, h.contractAddr, mappingSlot(1, 0), 2)
assertStorageUint(t, h.statedb, h.contractAddr, mappingSlot(2, 0), 3)
assertStorageUint(t, h.statedb, h.contractAddr, common.BigToHash(big.NewInt(2)), 5)
}
func newParallelVMHarness(t *testing.T) *parallelVMHarness {
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)
}
memdb := rawdb.NewMemoryDatabase()
trieDB := triedb.NewDatabase(memdb, &triedb.Config{Preimages: true})
cacheDB := state.NewDatabase(trieDB, nil)
statedb, err := state.New(types.EmptyRootHash, cacheDB)
if err != nil {
t.Fatalf("create state db: %v", err)
}
key, err := crypto.GenerateKey()
if err != nil {
t.Fatalf("generate key: %v", err)
}
from := crypto.PubkeyToAddress(key.PublicKey)
statedb.AddBalance(
from,
uint256.MustFromBig(new(big.Int).Mul(big.NewInt(1_000_000), big.NewInt(params.Ether))),
tracing.BalanceChangeUnspecified,
)
return &parallelVMHarness{
statedb: statedb,
chainConfig: params.AllEthashProtocolChanges,
privateKey: key,
from: from,
testABI: parsedABI,
}
}
func (h *parallelVMHarness) deployContract(t *testing.T) {
t.Helper()
bytecode := mustReadContractBin(t)
msg := &core.Message{
From: h.from,
To: nil,
Nonce: h.nonce,
Value: big.NewInt(0),
GasLimit: 12_000_000,
GasPrice: big.NewInt(1),
GasFeeCap: big.NewInt(1),
GasTipCap: big.NewInt(1),
Data: bytecode,
SkipNonceChecks: true,
SkipFromEOACheck: true,
}
h.nonce++
blockCtx := vm.BlockContext{
CanTransfer: core.CanTransfer,
Transfer: core.Transfer,
GetHash: func(uint64) common.Hash { return common.Hash{} },
Coinbase: common.Address{},
BlockNumber: big.NewInt(1),
Time: 1,
Difficulty: big.NewInt(1),
GasLimit: 30_000_000,
BaseFee: big.NewInt(0),
}
txCtx := core.NewEVMTxContext(msg)
evm := vm.NewEVM(blockCtx, h.statedb, h.chainConfig, vm.Config{})
evm.SetTxContext(txCtx)
result, err := core.ApplyMessage(evm, msg, new(core.GasPool).AddGas(msg.GasLimit))
if err != nil {
t.Fatalf("deploy ApplyMessage: %v", err)
}
if result.Failed() {
t.Fatalf("deploy reverted: %x | err: %v", result.Return(), err)
}
h.contractAddr = crypto.CreateAddress(h.from, 0)
}
func (h *parallelVMHarness) callContract(t *testing.T, method string, args ...interface{}) {
t.Helper()
input, err := h.testABI.Pack(method, args...)
if err != nil {
t.Fatalf("ABI pack %s: %v", method, err)
}
msg := &core.Message{
From: h.from,
To: &h.contractAddr,
Nonce: h.nonce,
Value: big.NewInt(0),
GasLimit: 12_000_000,
GasPrice: big.NewInt(1),
GasFeeCap: big.NewInt(1),
GasTipCap: big.NewInt(1),
Data: input,
SkipNonceChecks: true,
SkipFromEOACheck: true,
}
h.nonce++
blockCtx := vm.BlockContext{
CanTransfer: core.CanTransfer,
Transfer: core.Transfer,
GetHash: func(uint64) common.Hash { return common.Hash{} },
Coinbase: common.Address{},
BlockNumber: big.NewInt(1),
Time: 1,
Difficulty: big.NewInt(1),
GasLimit: 30_000_000,
BaseFee: big.NewInt(0),
}
txCtx := core.NewEVMTxContext(msg)
evm := vm.NewEVM(blockCtx, h.statedb, h.chainConfig, vm.Config{})
evm.SetTxContext(txCtx)
result, err := core.ApplyMessage(evm, msg, new(core.GasPool).AddGas(msg.GasLimit))
if err != nil {
t.Fatalf("call %s ApplyMessage: %v", method, err)
}
if result.Failed() {
t.Fatalf("call %s reverted: %x", method, result.Return())
}
}
func mustReadContractBin(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 mappingSlot(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 assertStorageUint(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)
}
}