Skip to content
Merged
Show file tree
Hide file tree
Changes from 1 commit
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
101 changes: 101 additions & 0 deletions ethclient/ethclient.go
Original file line number Diff line number Diff line change
Expand Up @@ -28,6 +28,7 @@ import (
"github.com/ethereum/go-ethereum/common"
"github.com/ethereum/go-ethereum/common/hexutil"
"github.com/ethereum/go-ethereum/core/types"
"github.com/ethereum/go-ethereum/crypto/kzg4844"
"github.com/ethereum/go-ethereum/rpc"
)

Expand Down Expand Up @@ -819,3 +820,103 @@ func (p *rpcProgress) toSyncProgress() *ethereum.SyncProgress {
StateIndexRemaining: uint64(p.StateIndexRemaining),
}
}

// SimulateOptions represents the options for eth_simulateV1.
type SimulateOptions struct {
BlockStateCalls []SimulateBlock `json:"blockStateCalls"`
TraceTransfers bool `json:"traceTransfers"`
Validation bool `json:"validation"`
ReturnFullTransactions bool `json:"returnFullTransactions"`
}

// SimulateBlock represents a batch of calls to be simulated.
type SimulateBlock struct {
BlockOverrides *BlockOverrides `json:"blockOverrides,omitempty"`
StateOverrides *StateOverride `json:"stateOverrides,omitempty"`
Calls []CallArgs `json:"calls"`
}

// BlockOverrides is a set of header fields to override.
type BlockOverrides struct {
Number *hexutil.Big `json:"number,omitempty"`
Difficulty *hexutil.Big `json:"difficulty,omitempty"`
Time *hexutil.Uint64 `json:"time,omitempty"`
GasLimit *hexutil.Uint64 `json:"gasLimit,omitempty"`
FeeRecipient *common.Address `json:"feeRecipient,omitempty"`
PrevRandao *common.Hash `json:"prevRandao,omitempty"`
BaseFeePerGas *hexutil.Big `json:"baseFeePerGas,omitempty"`
BlobBaseFee *hexutil.Big `json:"blobBaseFee,omitempty"`
BeaconRoot *common.Hash `json:"beaconRoot,omitempty"`
Withdrawals *types.Withdrawals `json:"withdrawals,omitempty"`
}

// StateOverride is the collection of overridden accounts.
type StateOverride map[common.Address]OverrideAccount

// OverrideAccount indicates the overriding fields of account during the execution
// of a message call.
type OverrideAccount struct {
Nonce *hexutil.Uint64 `json:"nonce,omitempty"`
Code *hexutil.Bytes `json:"code,omitempty"`
Balance *hexutil.Big `json:"balance,omitempty"`
State map[common.Hash]common.Hash `json:"state,omitempty"`
StateDiff map[common.Hash]common.Hash `json:"stateDiff,omitempty"`
MovePrecompileTo *common.Address `json:"movePrecompileToAddress,omitempty"`
}

// CallArgs represents the arguments to construct a transaction call.
type CallArgs struct {
From *common.Address `json:"from,omitempty"`
To *common.Address `json:"to,omitempty"`
Gas *hexutil.Uint64 `json:"gas,omitempty"`
GasPrice *hexutil.Big `json:"gasPrice,omitempty"`
MaxFeePerGas *hexutil.Big `json:"maxFeePerGas,omitempty"`
MaxPriorityFeePerGas *hexutil.Big `json:"maxPriorityFeePerGas,omitempty"`
Value *hexutil.Big `json:"value,omitempty"`
Nonce *hexutil.Uint64 `json:"nonce,omitempty"`
Data *hexutil.Bytes `json:"data,omitempty"`
Input *hexutil.Bytes `json:"input,omitempty"`
AccessList *types.AccessList `json:"accessList,omitempty"`
ChainID *hexutil.Big `json:"chainId,omitempty"`
BlobFeeCap *hexutil.Big `json:"maxFeePerBlobGas,omitempty"`
BlobHashes []common.Hash `json:"blobVersionedHashes,omitempty"`
Blobs []kzg4844.Blob `json:"blobs,omitempty"`
Commitments []kzg4844.Commitment `json:"commitments,omitempty"`
Proofs []kzg4844.Proof `json:"proofs,omitempty"`
AuthorizationList []types.SetCodeAuthorization `json:"authorizationList,omitempty"`
}

// SimulateCallResult is the result of a simulated call.
type SimulateCallResult struct {
ReturnValue hexutil.Bytes `json:"returnData"`
Logs []*types.Log `json:"logs"`
GasUsed hexutil.Uint64 `json:"gasUsed"`
Status hexutil.Uint64 `json:"status"`
Error *CallError `json:"error,omitempty"`
}

// CallError represents an error from a simulated call.
type CallError struct {
Code int `json:"code"`
Message string `json:"message"`
Data string `json:"data,omitempty"`
}

// SimulateBlockResult represents the result of a simulated block.
type SimulateBlockResult struct {
Number hexutil.Uint64 `json:"number"`
Hash common.Hash `json:"hash"`
Timestamp hexutil.Uint64 `json:"timestamp"`
GasLimit hexutil.Uint64 `json:"gasLimit"`
GasUsed hexutil.Uint64 `json:"gasUsed"`
FeeRecipient common.Address `json:"miner"`
BaseFeePerGas *hexutil.Big `json:"baseFeePerGas,omitempty"`
Calls []SimulateCallResult `json:"calls"`
}

// SimulateV1 executes transactions on top of a base state.
func (ec *Client) SimulateV1(ctx context.Context, opts SimulateOptions, blockNrOrHash *rpc.BlockNumberOrHash) ([]SimulateBlockResult, error) {
var result []SimulateBlockResult
err := ec.c.CallContext(ctx, &result, "eth_simulateV1", opts, blockNrOrHash)
return result, err
}
246 changes: 246 additions & 0 deletions ethclient/ethclient_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -29,6 +29,7 @@ import (
"github.com/ethereum/go-ethereum"
"github.com/ethereum/go-ethereum/accounts/abi"
"github.com/ethereum/go-ethereum/common"
"github.com/ethereum/go-ethereum/common/hexutil"
"github.com/ethereum/go-ethereum/consensus/beacon"
"github.com/ethereum/go-ethereum/consensus/ethash"
"github.com/ethereum/go-ethereum/core"
Expand Down Expand Up @@ -754,3 +755,248 @@ func ExampleRevertErrorData() {
// revert: 08c379a00000000000000000000000000000000000000000000000000000000000000020000000000000000000000000000000000000000000000000000000000000000a75736572206572726f72
// message: user error
}

func TestSimulateV1(t *testing.T) {
backend, _, err := newTestBackend(nil)
if err != nil {
t.Fatalf("Failed to create test backend: %v", err)
}
defer backend.Close()

client := ethclient.NewClient(backend.Attach())
defer client.Close()

ctx := context.Background()

// Get current base fee
header, err := client.HeaderByNumber(ctx, nil)
if err != nil {
t.Fatalf("Failed to get header: %v", err)
}

// Simple test: transfer ETH from one account to another
from := testAddr
to := common.HexToAddress("0x0000000000000000000000000000000000000001")
value := hexutil.Big(*big.NewInt(100))
gas := hexutil.Uint64(100000)
maxFeePerGas := hexutil.Big(*new(big.Int).Mul(header.BaseFee, big.NewInt(2)))

opts := ethclient.SimulateOptions{
BlockStateCalls: []ethclient.SimulateBlock{
{
Calls: []ethclient.CallArgs{
{
From: &from,
To: &to,
Value: &value,
Gas: &gas,
MaxFeePerGas: &maxFeePerGas,
},
},
},
},
Validation: true,
}

results, err := client.SimulateV1(ctx, opts, nil)
if err != nil {
t.Fatalf("SimulateV1 failed: %v", err)
}

if len(results) != 1 {
t.Fatalf("expected 1 block result, got %d", len(results))
}

if len(results[0].Calls) != 1 {
t.Fatalf("expected 1 call result, got %d", len(results[0].Calls))
}

// Check that the transaction succeeded
if results[0].Calls[0].Status != 1 {
t.Errorf("expected status 1 (success), got %d", results[0].Calls[0].Status)
}

if results[0].Calls[0].Error != nil {
t.Errorf("expected no error, got %v", results[0].Calls[0].Error)
}
}

func TestSimulateV1WithBlockOverrides(t *testing.T) {
backend, _, err := newTestBackend(nil)
if err != nil {
t.Fatalf("Failed to create test backend: %v", err)
}
defer backend.Close()

client := ethclient.NewClient(backend.Attach())
defer client.Close()

ctx := context.Background()

// Get current base fee
header, err := client.HeaderByNumber(ctx, nil)
if err != nil {
t.Fatalf("Failed to get header: %v", err)
}

from := testAddr
to := common.HexToAddress("0x0000000000000000000000000000000000000001")
value := hexutil.Big(*big.NewInt(100))
gas := hexutil.Uint64(100000)
maxFeePerGas := hexutil.Big(*new(big.Int).Mul(header.BaseFee, big.NewInt(2)))

// Override timestamp only
timestamp := hexutil.Uint64(1234567890)

opts := ethclient.SimulateOptions{
BlockStateCalls: []ethclient.SimulateBlock{
{
BlockOverrides: &ethclient.BlockOverrides{
Time: &timestamp,
},
Calls: []ethclient.CallArgs{
{
From: &from,
To: &to,
Value: &value,
Gas: &gas,
MaxFeePerGas: &maxFeePerGas,
},
},
},
},
Validation: true,
}

results, err := client.SimulateV1(ctx, opts, nil)
if err != nil {
t.Fatalf("SimulateV1 with block overrides failed: %v", err)
}

if len(results) != 1 {
t.Fatalf("expected 1 block result, got %d", len(results))
}

// Verify the timestamp was overridden
if results[0].Timestamp != timestamp {
t.Errorf("expected timestamp %d, got %d", timestamp, results[0].Timestamp)
}
}

func TestSimulateV1WithStateOverrides(t *testing.T) {
backend, _, err := newTestBackend(nil)
if err != nil {
t.Fatalf("Failed to create test backend: %v", err)
}
defer backend.Close()

client := ethclient.NewClient(backend.Attach())
defer client.Close()

ctx := context.Background()

// Get current base fee
header, err := client.HeaderByNumber(ctx, nil)
if err != nil {
t.Fatalf("Failed to get header: %v", err)
}

from := testAddr
to := common.HexToAddress("0x0000000000000000000000000000000000000001")
value := hexutil.Big(*big.NewInt(1000000000000000000)) // 1 ETH
gas := hexutil.Uint64(100000)
maxFeePerGas := hexutil.Big(*new(big.Int).Mul(header.BaseFee, big.NewInt(2)))

// Override the balance of the 'from' address
balanceStr := "1000000000000000000000"
balance := new(big.Int)
balance.SetString(balanceStr, 10)

opts := ethclient.SimulateOptions{
BlockStateCalls: []ethclient.SimulateBlock{
{
StateOverrides: &ethclient.StateOverride{
from: ethclient.OverrideAccount{
Balance: (*hexutil.Big)(balance),
},
},
Calls: []ethclient.CallArgs{
{
From: &from,
To: &to,
Value: &value,
Gas: &gas,
MaxFeePerGas: &maxFeePerGas,
},
},
},
},
Validation: true,
}

results, err := client.SimulateV1(ctx, opts, nil)
if err != nil {
t.Fatalf("SimulateV1 with state overrides failed: %v", err)
}

if len(results) != 1 {
t.Fatalf("expected 1 block result, got %d", len(results))
}

if results[0].Calls[0].Status != 1 {
t.Errorf("expected status 1 (success), got %d", results[0].Calls[0].Status)
}
}

func TestSimulateV1WithBlockNumberOrHash(t *testing.T) {
backend, _, err := newTestBackend(nil)
if err != nil {
t.Fatalf("Failed to create test backend: %v", err)
}
defer backend.Close()

client := ethclient.NewClient(backend.Attach())
defer client.Close()

ctx := context.Background()

// Get current base fee
header, err := client.HeaderByNumber(ctx, nil)
if err != nil {
t.Fatalf("Failed to get header: %v", err)
}

from := testAddr
to := common.HexToAddress("0x0000000000000000000000000000000000000001")
value := hexutil.Big(*big.NewInt(100))
gas := hexutil.Uint64(100000)
maxFeePerGas := hexutil.Big(*new(big.Int).Mul(header.BaseFee, big.NewInt(2)))

opts := ethclient.SimulateOptions{
BlockStateCalls: []ethclient.SimulateBlock{
{
Calls: []ethclient.CallArgs{
{
From: &from,
To: &to,
Value: &value,
Gas: &gas,
MaxFeePerGas: &maxFeePerGas,
},
},
},
},
Validation: true,
}

// Simulate on the latest block
latest := rpc.BlockNumberOrHashWithNumber(rpc.LatestBlockNumber)
results, err := client.SimulateV1(ctx, opts, &latest)
if err != nil {
t.Fatalf("SimulateV1 with latest block failed: %v", err)
}

if len(results) != 1 {
t.Fatalf("expected 1 block result, got %d", len(results))
}
}
Loading