Skip to content

Commit c37bd67

Browse files
hero5512s1na
andauthored
ethclient: add support for eth_simulateV1 (#32856)
Adds ethclient support for the eth_simulateV1 RPC method, which allows simulating transactions on top of a base state without making changes to the blockchain. --------- Co-authored-by: Sina Mahmoodi <itz.s1na@gmail.com>
1 parent 5c53507 commit c37bd67

File tree

6 files changed

+575
-94
lines changed

6 files changed

+575
-94
lines changed

ethclient/ethclient.go

Lines changed: 86 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -828,3 +828,89 @@ func (p *rpcProgress) toSyncProgress() *ethereum.SyncProgress {
828828
StateIndexRemaining: uint64(p.StateIndexRemaining),
829829
}
830830
}
831+
832+
// SimulateOptions represents the options for eth_simulateV1.
833+
type SimulateOptions struct {
834+
BlockStateCalls []SimulateBlock `json:"blockStateCalls"`
835+
TraceTransfers bool `json:"traceTransfers"`
836+
Validation bool `json:"validation"`
837+
ReturnFullTransactions bool `json:"returnFullTransactions"`
838+
}
839+
840+
// SimulateBlock represents a batch of calls to be simulated.
841+
type SimulateBlock struct {
842+
BlockOverrides *ethereum.BlockOverrides `json:"blockOverrides,omitempty"`
843+
StateOverrides map[common.Address]ethereum.OverrideAccount `json:"stateOverrides,omitempty"`
844+
Calls []ethereum.CallMsg `json:"calls"`
845+
}
846+
847+
// MarshalJSON implements json.Marshaler for SimulateBlock.
848+
func (s SimulateBlock) MarshalJSON() ([]byte, error) {
849+
type Alias struct {
850+
BlockOverrides *ethereum.BlockOverrides `json:"blockOverrides,omitempty"`
851+
StateOverrides map[common.Address]ethereum.OverrideAccount `json:"stateOverrides,omitempty"`
852+
Calls []interface{} `json:"calls"`
853+
}
854+
calls := make([]interface{}, len(s.Calls))
855+
for i, call := range s.Calls {
856+
calls[i] = toCallArg(call)
857+
}
858+
return json.Marshal(Alias{
859+
BlockOverrides: s.BlockOverrides,
860+
StateOverrides: s.StateOverrides,
861+
Calls: calls,
862+
})
863+
}
864+
865+
//go:generate go run github.com/fjl/gencodec -type SimulateCallResult -field-override simulateCallResultMarshaling -out gen_simulate_call_result.go
866+
867+
// SimulateCallResult is the result of a simulated call.
868+
type SimulateCallResult struct {
869+
ReturnValue []byte `json:"returnData"`
870+
Logs []*types.Log `json:"logs"`
871+
GasUsed uint64 `json:"gasUsed"`
872+
Status uint64 `json:"status"`
873+
Error *CallError `json:"error,omitempty"`
874+
}
875+
876+
type simulateCallResultMarshaling struct {
877+
ReturnValue hexutil.Bytes
878+
GasUsed hexutil.Uint64
879+
Status hexutil.Uint64
880+
}
881+
882+
// CallError represents an error from a simulated call.
883+
type CallError struct {
884+
Code int `json:"code"`
885+
Message string `json:"message"`
886+
Data string `json:"data,omitempty"`
887+
}
888+
889+
//go:generate go run github.com/fjl/gencodec -type SimulateBlockResult -field-override simulateBlockResultMarshaling -out gen_simulate_block_result.go
890+
891+
// SimulateBlockResult represents the result of a simulated block.
892+
type SimulateBlockResult struct {
893+
Number *big.Int `json:"number"`
894+
Hash common.Hash `json:"hash"`
895+
Timestamp uint64 `json:"timestamp"`
896+
GasLimit uint64 `json:"gasLimit"`
897+
GasUsed uint64 `json:"gasUsed"`
898+
FeeRecipient common.Address `json:"miner"`
899+
BaseFeePerGas *big.Int `json:"baseFeePerGas,omitempty"`
900+
Calls []SimulateCallResult `json:"calls"`
901+
}
902+
903+
type simulateBlockResultMarshaling struct {
904+
Number *hexutil.Big
905+
Timestamp hexutil.Uint64
906+
GasLimit hexutil.Uint64
907+
GasUsed hexutil.Uint64
908+
BaseFeePerGas *hexutil.Big
909+
}
910+
911+
// SimulateV1 executes transactions on top of a base state.
912+
func (ec *Client) SimulateV1(ctx context.Context, opts SimulateOptions, blockNrOrHash *rpc.BlockNumberOrHash) ([]SimulateBlockResult, error) {
913+
var result []SimulateBlockResult
914+
err := ec.c.CallContext(ctx, &result, "eth_simulateV1", opts, blockNrOrHash)
915+
return result, err
916+
}

ethclient/ethclient_test.go

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

0 commit comments

Comments
 (0)