mirror of
https://github.com/ethereum/go-ethereum.git
synced 2026-07-24 05:36:46 +00:00
Porting Itay's tests
This commit is contained in:
parent
dd1ebac117
commit
d9f4968275
5 changed files with 1268 additions and 0 deletions
323
tests/contracts/ParallelVMTest.abi
Normal file
323
tests/contracts/ParallelVMTest.abi
Normal 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"
|
||||
}
|
||||
]
|
||||
1
tests/contracts/ParallelVMTest.bin
Normal file
1
tests/contracts/ParallelVMTest.bin
Normal file
File diff suppressed because one or more lines are too long
176
tests/contracts/ParallelVMTest.sol
Normal file
176
tests/contracts/ParallelVMTest.sol
Normal 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));
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
521
tests/parallel_vm_block_test.go
Normal file
521
tests/parallel_vm_block_test.go
Normal 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
247
tests/parallel_vm_test.go
Normal 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 ¶llelVMHarness{
|
||||
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)
|
||||
}
|
||||
}
|
||||
Loading…
Reference in a new issue