Skip to content

Commit 5691d73

Browse files
committed
ethclient: add support for eth_simulateV1
1 parent e42af53 commit 5691d73

File tree

2 files changed

+347
-0
lines changed

2 files changed

+347
-0
lines changed

ethclient/ethclient.go

Lines changed: 101 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -28,6 +28,7 @@ import (
2828
"github.com/ethereum/go-ethereum/common"
2929
"github.com/ethereum/go-ethereum/common/hexutil"
3030
"github.com/ethereum/go-ethereum/core/types"
31+
"github.com/ethereum/go-ethereum/crypto/kzg4844"
3132
"github.com/ethereum/go-ethereum/rpc"
3233
)
3334

@@ -819,3 +820,103 @@ func (p *rpcProgress) toSyncProgress() *ethereum.SyncProgress {
819820
StateIndexRemaining: uint64(p.StateIndexRemaining),
820821
}
821822
}
823+
824+
// SimulateOptions represents the options for eth_simulateV1.
825+
type SimulateOptions struct {
826+
BlockStateCalls []SimulateBlock `json:"blockStateCalls"`
827+
TraceTransfers bool `json:"traceTransfers"`
828+
Validation bool `json:"validation"`
829+
ReturnFullTransactions bool `json:"returnFullTransactions"`
830+
}
831+
832+
// SimulateBlock represents a batch of calls to be simulated.
833+
type SimulateBlock struct {
834+
BlockOverrides *BlockOverrides `json:"blockOverrides,omitempty"`
835+
StateOverrides *StateOverride `json:"stateOverrides,omitempty"`
836+
Calls []CallArgs `json:"calls"`
837+
}
838+
839+
// BlockOverrides is a set of header fields to override.
840+
type BlockOverrides struct {
841+
Number *hexutil.Big `json:"number,omitempty"`
842+
Difficulty *hexutil.Big `json:"difficulty,omitempty"`
843+
Time *hexutil.Uint64 `json:"time,omitempty"`
844+
GasLimit *hexutil.Uint64 `json:"gasLimit,omitempty"`
845+
FeeRecipient *common.Address `json:"feeRecipient,omitempty"`
846+
PrevRandao *common.Hash `json:"prevRandao,omitempty"`
847+
BaseFeePerGas *hexutil.Big `json:"baseFeePerGas,omitempty"`
848+
BlobBaseFee *hexutil.Big `json:"blobBaseFee,omitempty"`
849+
BeaconRoot *common.Hash `json:"beaconRoot,omitempty"`
850+
Withdrawals *types.Withdrawals `json:"withdrawals,omitempty"`
851+
}
852+
853+
// StateOverride is the collection of overridden accounts.
854+
type StateOverride map[common.Address]OverrideAccount
855+
856+
// OverrideAccount indicates the overriding fields of account during the execution
857+
// of a message call.
858+
type OverrideAccount struct {
859+
Nonce *hexutil.Uint64 `json:"nonce,omitempty"`
860+
Code *hexutil.Bytes `json:"code,omitempty"`
861+
Balance *hexutil.Big `json:"balance,omitempty"`
862+
State map[common.Hash]common.Hash `json:"state,omitempty"`
863+
StateDiff map[common.Hash]common.Hash `json:"stateDiff,omitempty"`
864+
MovePrecompileTo *common.Address `json:"movePrecompileToAddress,omitempty"`
865+
}
866+
867+
// CallArgs represents the arguments to construct a transaction call.
868+
type CallArgs struct {
869+
From *common.Address `json:"from,omitempty"`
870+
To *common.Address `json:"to,omitempty"`
871+
Gas *hexutil.Uint64 `json:"gas,omitempty"`
872+
GasPrice *hexutil.Big `json:"gasPrice,omitempty"`
873+
MaxFeePerGas *hexutil.Big `json:"maxFeePerGas,omitempty"`
874+
MaxPriorityFeePerGas *hexutil.Big `json:"maxPriorityFeePerGas,omitempty"`
875+
Value *hexutil.Big `json:"value,omitempty"`
876+
Nonce *hexutil.Uint64 `json:"nonce,omitempty"`
877+
Data *hexutil.Bytes `json:"data,omitempty"`
878+
Input *hexutil.Bytes `json:"input,omitempty"`
879+
AccessList *types.AccessList `json:"accessList,omitempty"`
880+
ChainID *hexutil.Big `json:"chainId,omitempty"`
881+
BlobFeeCap *hexutil.Big `json:"maxFeePerBlobGas,omitempty"`
882+
BlobHashes []common.Hash `json:"blobVersionedHashes,omitempty"`
883+
Blobs []kzg4844.Blob `json:"blobs,omitempty"`
884+
Commitments []kzg4844.Commitment `json:"commitments,omitempty"`
885+
Proofs []kzg4844.Proof `json:"proofs,omitempty"`
886+
AuthorizationList []types.SetCodeAuthorization `json:"authorizationList,omitempty"`
887+
}
888+
889+
// SimulateCallResult is the result of a simulated call.
890+
type SimulateCallResult struct {
891+
ReturnValue hexutil.Bytes `json:"returnData"`
892+
Logs []*types.Log `json:"logs"`
893+
GasUsed hexutil.Uint64 `json:"gasUsed"`
894+
Status hexutil.Uint64 `json:"status"`
895+
Error *CallError `json:"error,omitempty"`
896+
}
897+
898+
// CallError represents an error from a simulated call.
899+
type CallError struct {
900+
Code int `json:"code"`
901+
Message string `json:"message"`
902+
Data string `json:"data,omitempty"`
903+
}
904+
905+
// SimulateBlockResult represents the result of a simulated block.
906+
type SimulateBlockResult struct {
907+
Number hexutil.Uint64 `json:"number"`
908+
Hash common.Hash `json:"hash"`
909+
Timestamp hexutil.Uint64 `json:"timestamp"`
910+
GasLimit hexutil.Uint64 `json:"gasLimit"`
911+
GasUsed hexutil.Uint64 `json:"gasUsed"`
912+
FeeRecipient common.Address `json:"miner"`
913+
BaseFeePerGas *hexutil.Big `json:"baseFeePerGas,omitempty"`
914+
Calls []SimulateCallResult `json:"calls"`
915+
}
916+
917+
// SimulateV1 executes transactions on top of a base state.
918+
func (ec *Client) SimulateV1(ctx context.Context, opts SimulateOptions, blockNrOrHash *rpc.BlockNumberOrHash) ([]SimulateBlockResult, error) {
919+
var result []SimulateBlockResult
920+
err := ec.c.CallContext(ctx, &result, "eth_simulateV1", opts, blockNrOrHash)
921+
return result, err
922+
}

ethclient/ethclient_test.go

Lines changed: 246 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -29,6 +29,7 @@ import (
2929
"github.com/ethereum/go-ethereum"
3030
"github.com/ethereum/go-ethereum/accounts/abi"
3131
"github.com/ethereum/go-ethereum/common"
32+
"github.com/ethereum/go-ethereum/common/hexutil"
3233
"github.com/ethereum/go-ethereum/consensus/beacon"
3334
"github.com/ethereum/go-ethereum/consensus/ethash"
3435
"github.com/ethereum/go-ethereum/core"
@@ -754,3 +755,248 @@ func ExampleRevertErrorData() {
754755
// revert: 08c379a00000000000000000000000000000000000000000000000000000000000000020000000000000000000000000000000000000000000000000000000000000000a75736572206572726f72
755756
// message: user error
756757
}
758+
759+
func TestSimulateV1(t *testing.T) {
760+
backend, _, err := newTestBackend(nil)
761+
if err != nil {
762+
t.Fatalf("Failed to create test backend: %v", err)
763+
}
764+
defer backend.Close()
765+
766+
client := ethclient.NewClient(backend.Attach())
767+
defer client.Close()
768+
769+
ctx := context.Background()
770+
771+
// Get current base fee
772+
header, err := client.HeaderByNumber(ctx, nil)
773+
if err != nil {
774+
t.Fatalf("Failed to get header: %v", err)
775+
}
776+
777+
// Simple test: transfer ETH from one account to another
778+
from := testAddr
779+
to := common.HexToAddress("0x0000000000000000000000000000000000000001")
780+
value := hexutil.Big(*big.NewInt(100))
781+
gas := hexutil.Uint64(100000)
782+
maxFeePerGas := hexutil.Big(*new(big.Int).Mul(header.BaseFee, big.NewInt(2)))
783+
784+
opts := ethclient.SimulateOptions{
785+
BlockStateCalls: []ethclient.SimulateBlock{
786+
{
787+
Calls: []ethclient.CallArgs{
788+
{
789+
From: &from,
790+
To: &to,
791+
Value: &value,
792+
Gas: &gas,
793+
MaxFeePerGas: &maxFeePerGas,
794+
},
795+
},
796+
},
797+
},
798+
Validation: true,
799+
}
800+
801+
results, err := client.SimulateV1(ctx, opts, nil)
802+
if err != nil {
803+
t.Fatalf("SimulateV1 failed: %v", err)
804+
}
805+
806+
if len(results) != 1 {
807+
t.Fatalf("expected 1 block result, got %d", len(results))
808+
}
809+
810+
if len(results[0].Calls) != 1 {
811+
t.Fatalf("expected 1 call result, got %d", len(results[0].Calls))
812+
}
813+
814+
// Check that the transaction succeeded
815+
if results[0].Calls[0].Status != 1 {
816+
t.Errorf("expected status 1 (success), got %d", results[0].Calls[0].Status)
817+
}
818+
819+
if results[0].Calls[0].Error != nil {
820+
t.Errorf("expected no error, got %v", results[0].Calls[0].Error)
821+
}
822+
}
823+
824+
func TestSimulateV1WithBlockOverrides(t *testing.T) {
825+
backend, _, err := newTestBackend(nil)
826+
if err != nil {
827+
t.Fatalf("Failed to create test backend: %v", err)
828+
}
829+
defer backend.Close()
830+
831+
client := ethclient.NewClient(backend.Attach())
832+
defer client.Close()
833+
834+
ctx := context.Background()
835+
836+
// Get current base fee
837+
header, err := client.HeaderByNumber(ctx, nil)
838+
if err != nil {
839+
t.Fatalf("Failed to get header: %v", err)
840+
}
841+
842+
from := testAddr
843+
to := common.HexToAddress("0x0000000000000000000000000000000000000001")
844+
value := hexutil.Big(*big.NewInt(100))
845+
gas := hexutil.Uint64(100000)
846+
maxFeePerGas := hexutil.Big(*new(big.Int).Mul(header.BaseFee, big.NewInt(2)))
847+
848+
// Override timestamp only
849+
timestamp := hexutil.Uint64(1234567890)
850+
851+
opts := ethclient.SimulateOptions{
852+
BlockStateCalls: []ethclient.SimulateBlock{
853+
{
854+
BlockOverrides: &ethclient.BlockOverrides{
855+
Time: &timestamp,
856+
},
857+
Calls: []ethclient.CallArgs{
858+
{
859+
From: &from,
860+
To: &to,
861+
Value: &value,
862+
Gas: &gas,
863+
MaxFeePerGas: &maxFeePerGas,
864+
},
865+
},
866+
},
867+
},
868+
Validation: true,
869+
}
870+
871+
results, err := client.SimulateV1(ctx, opts, nil)
872+
if err != nil {
873+
t.Fatalf("SimulateV1 with block overrides failed: %v", err)
874+
}
875+
876+
if len(results) != 1 {
877+
t.Fatalf("expected 1 block result, got %d", len(results))
878+
}
879+
880+
// Verify the timestamp was overridden
881+
if results[0].Timestamp != timestamp {
882+
t.Errorf("expected timestamp %d, got %d", timestamp, results[0].Timestamp)
883+
}
884+
}
885+
886+
func TestSimulateV1WithStateOverrides(t *testing.T) {
887+
backend, _, err := newTestBackend(nil)
888+
if err != nil {
889+
t.Fatalf("Failed to create test backend: %v", err)
890+
}
891+
defer backend.Close()
892+
893+
client := ethclient.NewClient(backend.Attach())
894+
defer client.Close()
895+
896+
ctx := context.Background()
897+
898+
// Get current base fee
899+
header, err := client.HeaderByNumber(ctx, nil)
900+
if err != nil {
901+
t.Fatalf("Failed to get header: %v", err)
902+
}
903+
904+
from := testAddr
905+
to := common.HexToAddress("0x0000000000000000000000000000000000000001")
906+
value := hexutil.Big(*big.NewInt(1000000000000000000)) // 1 ETH
907+
gas := hexutil.Uint64(100000)
908+
maxFeePerGas := hexutil.Big(*new(big.Int).Mul(header.BaseFee, big.NewInt(2)))
909+
910+
// Override the balance of the 'from' address
911+
balanceStr := "1000000000000000000000"
912+
balance := new(big.Int)
913+
balance.SetString(balanceStr, 10)
914+
915+
opts := ethclient.SimulateOptions{
916+
BlockStateCalls: []ethclient.SimulateBlock{
917+
{
918+
StateOverrides: &ethclient.StateOverride{
919+
from: ethclient.OverrideAccount{
920+
Balance: (*hexutil.Big)(balance),
921+
},
922+
},
923+
Calls: []ethclient.CallArgs{
924+
{
925+
From: &from,
926+
To: &to,
927+
Value: &value,
928+
Gas: &gas,
929+
MaxFeePerGas: &maxFeePerGas,
930+
},
931+
},
932+
},
933+
},
934+
Validation: true,
935+
}
936+
937+
results, err := client.SimulateV1(ctx, opts, nil)
938+
if err != nil {
939+
t.Fatalf("SimulateV1 with state overrides failed: %v", err)
940+
}
941+
942+
if len(results) != 1 {
943+
t.Fatalf("expected 1 block result, got %d", len(results))
944+
}
945+
946+
if results[0].Calls[0].Status != 1 {
947+
t.Errorf("expected status 1 (success), got %d", results[0].Calls[0].Status)
948+
}
949+
}
950+
951+
func TestSimulateV1WithBlockNumberOrHash(t *testing.T) {
952+
backend, _, err := newTestBackend(nil)
953+
if err != nil {
954+
t.Fatalf("Failed to create test backend: %v", err)
955+
}
956+
defer backend.Close()
957+
958+
client := ethclient.NewClient(backend.Attach())
959+
defer client.Close()
960+
961+
ctx := context.Background()
962+
963+
// Get current base fee
964+
header, err := client.HeaderByNumber(ctx, nil)
965+
if err != nil {
966+
t.Fatalf("Failed to get header: %v", err)
967+
}
968+
969+
from := testAddr
970+
to := common.HexToAddress("0x0000000000000000000000000000000000000001")
971+
value := hexutil.Big(*big.NewInt(100))
972+
gas := hexutil.Uint64(100000)
973+
maxFeePerGas := hexutil.Big(*new(big.Int).Mul(header.BaseFee, big.NewInt(2)))
974+
975+
opts := ethclient.SimulateOptions{
976+
BlockStateCalls: []ethclient.SimulateBlock{
977+
{
978+
Calls: []ethclient.CallArgs{
979+
{
980+
From: &from,
981+
To: &to,
982+
Value: &value,
983+
Gas: &gas,
984+
MaxFeePerGas: &maxFeePerGas,
985+
},
986+
},
987+
},
988+
},
989+
Validation: true,
990+
}
991+
992+
// Simulate on the latest block
993+
latest := rpc.BlockNumberOrHashWithNumber(rpc.LatestBlockNumber)
994+
results, err := client.SimulateV1(ctx, opts, &latest)
995+
if err != nil {
996+
t.Fatalf("SimulateV1 with latest block failed: %v", err)
997+
}
998+
999+
if len(results) != 1 {
1000+
t.Fatalf("expected 1 block result, got %d", len(results))
1001+
}
1002+
}

0 commit comments

Comments
 (0)