From a7052559660504c417d9f24c626af87ba9de9c9b Mon Sep 17 00:00:00 2001 From: Thomas Nguy Date: Thu, 30 Oct 2025 09:50:39 +0900 Subject: [PATCH 1/4] support create_access_list --- rpc/backend/backend.go | 1 + rpc/backend/backend_suite_test.go | 18 +++ rpc/backend/call_tx.go | 3 + rpc/backend/client_test.go | 116 ++++++++++---- rpc/backend/evm_query_client_test.go | 124 ++++++++++---- rpc/backend/tx_info.go | 167 +++++++++++++++++++ rpc/backend/tx_info_test.go | 231 +++++++++++++++++++++++++++ rpc/namespaces/ethereum/eth/api.go | 15 ++ rpc/types/types.go | 7 + x/evm/types/tracer.go | 6 +- x/evm/types/tx_args.go | 50 ++++++ 11 files changed, 672 insertions(+), 66 deletions(-) diff --git a/rpc/backend/backend.go b/rpc/backend/backend.go index 9e29ef391e..d6cc770f3c 100644 --- a/rpc/backend/backend.go +++ b/rpc/backend/backend.go @@ -125,6 +125,7 @@ type EVMBackend interface { GetTransactionReceipt(hash common.Hash, resBlock *tmrpctypes.ResultBlock) (map[string]interface{}, error) GetTransactionByBlockHashAndIndex(hash common.Hash, idx hexutil.Uint) (*rpctypes.RPCTransaction, error) GetTransactionByBlockNumberAndIndex(blockNum rpctypes.BlockNumber, idx hexutil.Uint) (*rpctypes.RPCTransaction, error) + CreateAccessList(args evmtypes.TransactionArgs, blockNrOrHash rpctypes.BlockNumberOrHash, overrides *json.RawMessage) (*rpctypes.AccessListResult, error) // Send Transaction Resend(args evmtypes.TransactionArgs, gasPrice *hexutil.Big, gasLimit *hexutil.Uint64) (common.Hash, error) diff --git a/rpc/backend/backend_suite_test.go b/rpc/backend/backend_suite_test.go index 153dd17625..fc8044c40f 100644 --- a/rpc/backend/backend_suite_test.go +++ b/rpc/backend/backend_suite_test.go @@ -2,6 +2,7 @@ package backend import ( "bufio" + sdkmath "cosmossdk.io/math" "math/big" "os" "path/filepath" @@ -194,3 +195,20 @@ func (suite *BackendTestSuite) signAndEncodeEthTx(msgEthereumTx *evmtypes.MsgEth return txBz } + +func (suite *BackendTestSuite) SetupMockCLient() { + c := suite.backend.clientCtx.Client.(*mocks.Client) + _, err := RegisterBlockResults(c, 1) + suite.Require().NoError(err) + height := rpctypes.NewBlockNumber(big.NewInt(1)).Int64() + _, err = RegisterHeader(c, &height, nil) + suite.Require().NoError(err) + RegisterHeaderByHashAny(c) + RegisterABCIQueryAny(c, 1) + + queryClient := suite.backend.queryClient.QueryClient.(*mocks.EVMQueryClient) + RegisterParamsAny(queryClient) + RegisterBaseFee(queryClient, sdkmath.NewInt(1)) + validator := sdk.AccAddress(tests.GenerateAddress().Bytes()) + RegisterValidatorAccount(queryClient, validator) +} diff --git a/rpc/backend/call_tx.go b/rpc/backend/call_tx.go index 171f4037fa..607dd346cc 100644 --- a/rpc/backend/call_tx.go +++ b/rpc/backend/call_tx.go @@ -245,6 +245,9 @@ func (b *Backend) SetTxDefaults(args evmtypes.TransactionArgs) (evmtypes.Transac args.Value = new(hexutil.Big) } if args.Nonce == nil { + if args.From == nil { + return args, errors.New("missing from in argument") + } // get the nonce from the account retriever // ignore error in case tge account doesn't exist yet nonce, _ := b.getAccountNonce(*args.From, true, 0, b.logger) diff --git a/rpc/backend/client_test.go b/rpc/backend/client_test.go index 0e1d8b2a36..8515d81d62 100644 --- a/rpc/backend/client_test.go +++ b/rpc/backend/client_test.go @@ -35,57 +35,67 @@ var _ tmrpcclient.Client = &mocks.Client{} func RegisterTxSearch(client *mocks.Client, query string, txBz []byte) { resulTxs := []*tmrpctypes.ResultTx{{Tx: txBz}} client.On("TxSearch", rpc.ContextWithHeight(1), query, false, (*int)(nil), (*int)(nil), ""). - Return(&tmrpctypes.ResultTxSearch{Txs: resulTxs, TotalCount: 1}, nil) + Return(&tmrpctypes.ResultTxSearch{Txs: resulTxs, TotalCount: 1}, nil). + Maybe() } func RegisterTxSearchEmpty(client *mocks.Client, query string) { client.On("TxSearch", rpc.ContextWithHeight(1), query, false, (*int)(nil), (*int)(nil), ""). - Return(&tmrpctypes.ResultTxSearch{}, nil) + Return(&tmrpctypes.ResultTxSearch{}, nil). + Maybe() } func RegisterTxSearchError(client *mocks.Client, query string) { client.On("TxSearch", rpc.ContextWithHeight(1), query, false, (*int)(nil), (*int)(nil), ""). - Return(nil, errortypes.ErrInvalidRequest) + Return(nil, errortypes.ErrInvalidRequest). + Maybe() } // Broadcast Tx func RegisterBroadcastTx(client *mocks.Client, tx types.Tx) { client.On("BroadcastTxSync", context.Background(), tx). - Return(&tmrpctypes.ResultBroadcastTx{}, nil) + Return(&tmrpctypes.ResultBroadcastTx{}, nil). + Maybe() } func RegisterBroadcastTxError(client *mocks.Client, tx types.Tx) { client.On("BroadcastTxSync", context.Background(), tx). - Return(nil, errortypes.ErrInvalidRequest) + Return(nil, errortypes.ErrInvalidRequest). + Maybe() } // Unconfirmed Transactions func RegisterUnconfirmedTxs(client *mocks.Client, limit *int, txs []types.Tx) { client.On("UnconfirmedTxs", rpc.ContextWithHeight(1), limit). - Return(&tmrpctypes.ResultUnconfirmedTxs{Txs: txs}, nil) + Return(&tmrpctypes.ResultUnconfirmedTxs{Txs: txs}, nil). + Maybe() } func RegisterUnconfirmedTxsEmpty(client *mocks.Client, limit *int) { client.On("UnconfirmedTxs", rpc.ContextWithHeight(1), limit). Return(&tmrpctypes.ResultUnconfirmedTxs{ Txs: make([]types.Tx, 2), - }, nil) + }, nil). + Maybe() } func RegisterUnconfirmedTxsError(client *mocks.Client, limit *int) { client.On("UnconfirmedTxs", rpc.ContextWithHeight(1), limit). - Return(nil, errortypes.ErrInvalidRequest) + Return(nil, errortypes.ErrInvalidRequest). + Maybe() } // Status func RegisterStatus(client *mocks.Client) { client.On("Status", rpc.ContextWithHeight(1)). - Return(&tmrpctypes.ResultStatus{}, nil) + Return(&tmrpctypes.ResultStatus{}, nil). + Maybe() } func RegisterStatusError(client *mocks.Client) { client.On("Status", rpc.ContextWithHeight(1)). - Return(nil, errortypes.ErrInvalidRequest) + Return(nil, errortypes.ErrInvalidRequest). + Maybe() } // Block @@ -97,7 +107,8 @@ func RegisterBlockMultipleTxs( block := types.MakeBlock(height, txs, nil, nil) block.ChainID = ChainID resBlock := &tmrpctypes.ResultBlock{Block: block} - client.On("Block", rpc.ContextWithHeight(height), mock.AnythingOfType("*int64")).Return(resBlock, nil) + client.On("Block", rpc.ContextWithHeight(height), mock.AnythingOfType("*int64")).Return(resBlock, nil). + Maybe() return resBlock, nil } @@ -111,7 +122,8 @@ func RegisterBlock( emptyBlock := types.MakeBlock(height, []types.Tx{}, nil, nil) emptyBlock.ChainID = ChainID resBlock := &tmrpctypes.ResultBlock{Block: emptyBlock} - client.On("Block", rpc.ContextWithHeight(height), mock.AnythingOfType("*int64")).Return(resBlock, nil) + client.On("Block", rpc.ContextWithHeight(height), mock.AnythingOfType("*int64")).Return(resBlock, nil). + Maybe() return resBlock, nil } @@ -119,14 +131,16 @@ func RegisterBlock( block := types.MakeBlock(height, []types.Tx{tx}, nil, nil) block.ChainID = ChainID resBlock := &tmrpctypes.ResultBlock{Block: block} - client.On("Block", rpc.ContextWithHeight(height), mock.AnythingOfType("*int64")).Return(resBlock, nil) + client.On("Block", rpc.ContextWithHeight(height), mock.AnythingOfType("*int64")).Return(resBlock, nil). + Maybe() return resBlock, nil } // Block returns error func RegisterBlockError(client *mocks.Client, height int64) { client.On("Block", rpc.ContextWithHeight(height), mock.AnythingOfType("*int64")). - Return(nil, errortypes.ErrInvalidRequest) + Return(nil, errortypes.ErrInvalidRequest). + Maybe() } // Block not found @@ -135,7 +149,8 @@ func RegisterBlockNotFound( height int64, ) (*tmrpctypes.ResultBlock, error) { client.On("Block", rpc.ContextWithHeight(height), mock.AnythingOfType("*int64")). - Return(&tmrpctypes.ResultBlock{Block: nil}, nil) + Return(&tmrpctypes.ResultBlock{Block: nil}, nil). + Maybe() return &tmrpctypes.ResultBlock{Block: nil}, nil } @@ -145,7 +160,8 @@ func RegisterBlockPanic(client *mocks.Client, height int64) { client.On("Block", rpc.ContextWithHeight(height), mock.AnythingOfType("*int64")). Return(func(context.Context, *int64) *tmrpctypes.ResultBlock { panic("Block call panic") - }, nil) + }, nil). + Maybe() } func TestRegisterBlock(t *testing.T) { @@ -166,12 +182,14 @@ func TestRegisterBlock(t *testing.T) { func RegisterConsensusParams(client *mocks.Client, height int64) { consensusParams := types.DefaultConsensusParams() client.On("ConsensusParams", rpc.ContextWithHeight(height), mock.AnythingOfType("*int64")). - Return(&tmrpctypes.ResultConsensusParams{ConsensusParams: *consensusParams}, nil) + Return(&tmrpctypes.ResultConsensusParams{ConsensusParams: *consensusParams}, nil). + Maybe() } func RegisterConsensusParamsError(client *mocks.Client, height int64) { client.On("ConsensusParams", rpc.ContextWithHeight(height), mock.AnythingOfType("*int64")). - Return(nil, errortypes.ErrInvalidRequest) + Return(nil, errortypes.ErrInvalidRequest). + Maybe() } func TestRegisterConsensusParams(t *testing.T) { @@ -209,7 +227,8 @@ func RegisterBlockResultsWithEventLog(client *mocks.Client, height int64) (*tmrp } client.On("BlockResults", rpc.ContextWithHeight(height), mock.AnythingOfType("*int64")). - Return(res, nil) + Return(res, nil). + Maybe() return res, nil } @@ -223,13 +242,15 @@ func RegisterBlockResults( } client.On("BlockResults", rpc.ContextWithHeight(height), mock.AnythingOfType("*int64")). - Return(res, nil) + Return(res, nil). + Maybe() return res, nil } func RegisterBlockResultsError(client *mocks.Client, height int64) { client.On("BlockResults", rpc.ContextWithHeight(height), mock.AnythingOfType("*int64")). - Return(nil, errortypes.ErrInvalidRequest) + Return(nil, errortypes.ErrInvalidRequest). + Maybe() } func TestRegisterBlockResults(t *testing.T) { @@ -256,18 +277,21 @@ func RegisterBlockByHash( resBlock := &tmrpctypes.ResultBlock{Block: block} client.On("BlockByHash", rpc.ContextWithHeight(1), []byte{0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0}). - Return(resBlock, nil) + Return(resBlock, nil). + Maybe() return resBlock, nil } func RegisterBlockByHashError(client *mocks.Client, hash common.Hash, tx []byte) { client.On("BlockByHash", rpc.ContextWithHeight(1), []byte{0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0}). - Return(nil, errortypes.ErrInvalidRequest) + Return(nil, errortypes.ErrInvalidRequest). + Maybe() } func RegisterBlockByHashNotFound(client *mocks.Client, hash common.Hash, tx []byte) { client.On("BlockByHash", rpc.ContextWithHeight(1), []byte{0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0}). - Return(nil, nil) + Return(nil, nil). + Maybe() } // HeaderByHash @@ -280,36 +304,47 @@ func RegisterHeaderByHash( resHeader := &tmrpctypes.ResultHeader{Header: &block.Header} client.On("HeaderByHash", rpc.ContextWithHeight(1), bytes.HexBytes(hash.Bytes())). - Return(resHeader, nil) + Return(resHeader, nil). + Maybe() return resHeader, nil } func RegisterHeaderByHashError(client *mocks.Client, hash common.Hash, tx []byte) { client.On("HeaderByHash", rpc.ContextWithHeight(1), bytes.HexBytes(hash.Bytes())). - Return(nil, errortypes.ErrInvalidRequest) + Return(nil, errortypes.ErrInvalidRequest). + Maybe() } func RegisterHeaderByHashNotFound(client *mocks.Client, hash common.Hash, tx []byte) { client.On("HeaderByHash", rpc.ContextWithHeight(1), bytes.HexBytes(hash.Bytes())). - Return(&tmrpctypes.ResultHeader{Header: nil}, nil) + Return(&tmrpctypes.ResultHeader{Header: nil}, nil). + Maybe() +} + +func RegisterHeaderByHashAny(client *mocks.Client) { + client.On("HeaderByHash", mock.Anything, mock.Anything). + Return(&tmrpctypes.ResultHeader{Header: &types.Header{Height: 1}}, nil).Maybe() } // Header func RegisterHeader(client *mocks.Client, height *int64, tx []byte) (*tmrpctypes.ResultHeader, error) { block := types.MakeBlock(*height, []types.Tx{tx}, nil, nil) resHeader := &tmrpctypes.ResultHeader{Header: &block.Header} - client.On("Header", rpc.ContextWithHeight(*height), height).Return(resHeader, nil) + client.On("Header", rpc.ContextWithHeight(*height), height).Return(resHeader, nil). + Maybe() return resHeader, nil } func RegisterHeaderError(client *mocks.Client, height *int64) { - client.On("Header", rpc.ContextWithHeight(*height), height).Return(nil, errortypes.ErrInvalidRequest) + client.On("Header", rpc.ContextWithHeight(*height), height).Return(nil, errortypes.ErrInvalidRequest). + Maybe() } // Header not found func RegisterHeaderNotFound(client *mocks.Client, height int64) { client.On("Header", rpc.ContextWithHeight(height), mock.AnythingOfType("*int64")). - Return(&tmrpctypes.ResultHeader{Header: nil}, nil) + Return(&tmrpctypes.ResultHeader{Header: nil}, nil). + Maybe() } func RegisterABCIQueryWithOptions(client *mocks.Client, height int64, path string, data bytes.HexBytes, opts tmrpcclient.ABCIQueryOptions) { @@ -319,12 +354,26 @@ func RegisterABCIQueryWithOptions(client *mocks.Client, height int64, path strin Value: []byte{2}, // TODO replace with data.Bytes(), Height: height, }, - }, nil) + }, nil). + Maybe() } func RegisterABCIQueryWithOptionsError(clients *mocks.Client, path string, data bytes.HexBytes, opts tmrpcclient.ABCIQueryOptions) { clients.On("ABCIQueryWithOptions", context.Background(), path, data, opts). - Return(nil, errortypes.ErrInvalidRequest) + Return(nil, errortypes.ErrInvalidRequest). + Maybe() +} + +func RegisterABCIQueryAny(client *mocks.Client, height int64) { + client.On("ABCIQueryWithOptions", context.Background(), mock.Anything, mock.Anything, mock.Anything). + Return(&tmrpctypes.ResultABCIQuery{ + Response: abci.ResponseQuery{ + Value: []byte{2}, // TODO replace with data.Bytes(), + Height: height, + }, + }, nil). + Maybe() + } func RegisterABCIQueryAccount(clients *mocks.Client, data bytes.HexBytes, opts tmrpcclient.ABCIQueryOptions, acc client.Account) { @@ -338,5 +387,6 @@ func RegisterABCIQueryAccount(clients *mocks.Client, data bytes.HexBytes, opts t Value: respBz, Height: 1, }, - }, nil) + }, nil). + Maybe() } diff --git a/rpc/backend/evm_query_client_test.go b/rpc/backend/evm_query_client_test.go index 0b73443436..416f945e3d 100644 --- a/rpc/backend/evm_query_client_test.go +++ b/rpc/backend/evm_query_client_test.go @@ -58,7 +58,8 @@ func RegisterTraceTransactionWithPredecessors(queryClient *mocks.EVMQueryClient, } return true })). - Return(&evmtypes.QueryTraceTxResponse{Data: data}, nil) + Return(&evmtypes.QueryTraceTxResponse{Data: data}, nil). + Maybe() } func RegisterTraceTransaction(queryClient *mocks.EVMQueryClient, msgEthTx *evmtypes.MsgEthereumTx) { @@ -78,12 +79,14 @@ func RegisterTraceTransaction(queryClient *mocks.EVMQueryClient, msgEthTx *evmty } return true })). - Return(&evmtypes.QueryTraceTxResponse{Data: data}, nil) + Return(&evmtypes.QueryTraceTxResponse{Data: data}, nil). + Maybe() } func RegisterTraceTransactionError(queryClient *mocks.EVMQueryClient, msgEthTx *evmtypes.MsgEthereumTx) { queryClient.On("TraceTx", rpc.ContextWithHeight(1), &evmtypes.QueryTraceTxRequest{Msg: msgEthTx, BlockNumber: 1, ChainId: 9000}). - Return(nil, errortypes.ErrInvalidRequest) + Return(nil, errortypes.ErrInvalidRequest). + Maybe() } // TraceBlock @@ -104,12 +107,14 @@ func RegisterTraceBlock(queryClient *mocks.EVMQueryClient, txs []*evmtypes.MsgEt } return true })). - Return(&evmtypes.QueryTraceBlockResponse{Data: data}, nil) + Return(&evmtypes.QueryTraceBlockResponse{Data: data}, nil). + Maybe() } func RegisterTraceBlockError(queryClient *mocks.EVMQueryClient) { queryClient.On("TraceBlock", rpc.ContextWithHeight(1), &evmtypes.QueryTraceBlockRequest{}). - Return(nil, errortypes.ErrInvalidRequest) + Return(nil, errortypes.ErrInvalidRequest). + Maybe() } // Params @@ -122,12 +127,36 @@ func RegisterParams(queryClient *mocks.EVMQueryClient, header *metadata.MD, heig h := metadata.MD{} h.Set(grpctypes.GRPCBlockHeightHeader, fmt.Sprint(height)) *arg.HeaderAddr = h - }) + }). + Maybe() +} + +func RegisterParamsAny(queryClient *mocks.EVMQueryClient) { + queryClient.On("Params", mock.Anything, mock.Anything). + Return(&evmtypes.QueryParamsResponse{ + Params: evmtypes.DefaultParams(), + }, nil). + Maybe() + + queryClient.On("Params", mock.Anything, mock.Anything, mock.Anything). + Return(&evmtypes.QueryParamsResponse{ + Params: evmtypes.DefaultParams(), + }, nil). + Run(func(args mock.Arguments) { + // If Params call is successful, also update the header height + if opt, ok := args.Get(2).(grpc.HeaderCallOption); ok { + h := metadata.MD{} + h.Set(grpctypes.GRPCBlockHeightHeader, fmt.Sprint(1)) + *opt.HeaderAddr = h + } + }). + Maybe() } func RegisterParamsWithoutHeader(queryClient *mocks.EVMQueryClient, height int64) { queryClient.On("Params", rpc.ContextWithHeight(height), &evmtypes.QueryParamsRequest{}). - Return(&evmtypes.QueryParamsResponse{Params: evmtypes.DefaultParams()}, nil) + Return(&evmtypes.QueryParamsResponse{Params: evmtypes.DefaultParams()}, nil). + Maybe() } func RegisterParamsInvalidHeader(queryClient *mocks.EVMQueryClient, header *metadata.MD, height int64) { @@ -138,7 +167,8 @@ func RegisterParamsInvalidHeader(queryClient *mocks.EVMQueryClient, header *meta arg := args.Get(2).(grpc.HeaderCallOption) h := metadata.MD{} *arg.HeaderAddr = h - }) + }). + Maybe() } func RegisterParamsInvalidHeight(queryClient *mocks.EVMQueryClient, header *metadata.MD, height int64) { @@ -150,18 +180,21 @@ func RegisterParamsInvalidHeight(queryClient *mocks.EVMQueryClient, header *meta h := metadata.MD{} h.Set(grpctypes.GRPCBlockHeightHeader, "invalid") *arg.HeaderAddr = h - }) + }). + Maybe() } func RegisterParamsWithoutHeaderError(queryClient *mocks.EVMQueryClient, height int64) { queryClient.On("Params", rpc.ContextWithHeight(height), &evmtypes.QueryParamsRequest{}). - Return(nil, errortypes.ErrInvalidRequest) + Return(nil, errortypes.ErrInvalidRequest). + Maybe() } // Params returns error func RegisterParamsError(queryClient *mocks.EVMQueryClient, header *metadata.MD, height int64) { queryClient.On("Params", rpc.ContextWithHeight(height), &evmtypes.QueryParamsRequest{}, grpc.Header(header)). - Return(nil, errortypes.ErrInvalidRequest) + Return(nil, errortypes.ErrInvalidRequest). + Maybe() } func TestRegisterParams(t *testing.T) { @@ -190,38 +223,53 @@ func TestRegisterParamsError(t *testing.T) { func RegisterEthCall(queryClient *mocks.EVMQueryClient, request *evmtypes.EthCallRequest) { ctx, _ := context.WithCancel(rpc.ContextWithHeight(1)) queryClient.On("EthCall", ctx, request). - Return(&evmtypes.MsgEthereumTxResponse{}, nil) + Return(&evmtypes.MsgEthereumTxResponse{}, nil). + Maybe() } func RegisterEthCallError(queryClient *mocks.EVMQueryClient, request *evmtypes.EthCallRequest) { ctx, _ := context.WithCancel(rpc.ContextWithHeight(1)) queryClient.On("EthCall", ctx, request). - Return(nil, errortypes.ErrInvalidRequest) + Return(nil, errortypes.ErrInvalidRequest). + Maybe() +} + +func RegisterEthCallAny(queryClient *mocks.EVMQueryClient, gasUsed uint64, vmError string) { + queryClient.On("EthCall", mock.Anything, mock.Anything). + Return(&evmtypes.MsgEthereumTxResponse{ + GasUsed: gasUsed, + VmError: vmError, + }, nil). + Maybe() } // Estimate Gas func RegisterEstimateGas(queryClient *mocks.EVMQueryClient, args evmtypes.TransactionArgs) { bz, _ := json.Marshal(args) queryClient.On("EstimateGas", rpc.ContextWithHeight(1), &evmtypes.EthCallRequest{Args: bz, ChainId: args.ChainID.ToInt().Int64()}). - Return(&evmtypes.EstimateGasResponse{}, nil) + Return(&evmtypes.EstimateGasResponse{}, nil). + Maybe() } // BaseFee func RegisterBaseFee(queryClient *mocks.EVMQueryClient, baseFee sdkmath.Int) { queryClient.On("BaseFee", rpc.ContextWithHeight(1), &evmtypes.QueryBaseFeeRequest{}). - Return(&evmtypes.QueryBaseFeeResponse{BaseFee: &baseFee}, nil) + Return(&evmtypes.QueryBaseFeeResponse{BaseFee: &baseFee}, nil). + Maybe() } // Base fee returns error func RegisterBaseFeeError(queryClient *mocks.EVMQueryClient) { queryClient.On("BaseFee", rpc.ContextWithHeight(1), &evmtypes.QueryBaseFeeRequest{}). - Return(&evmtypes.QueryBaseFeeResponse{}, evmtypes.ErrInvalidBaseFee) + Return(&evmtypes.QueryBaseFeeResponse{}, evmtypes.ErrInvalidBaseFee). + Maybe() } // Base fee not enabled func RegisterBaseFeeDisabled(queryClient *mocks.EVMQueryClient) { queryClient.On("BaseFee", rpc.ContextWithHeight(1), &evmtypes.QueryBaseFeeRequest{}). - Return(&evmtypes.QueryBaseFeeResponse{}, nil) + Return(&evmtypes.QueryBaseFeeResponse{}, nil). + Maybe() } func TestRegisterBaseFee(t *testing.T) { @@ -252,19 +300,22 @@ func TestRegisterBaseFeeDisabled(t *testing.T) { // ValidatorAccount func RegisterValidatorAccount(queryClient *mocks.EVMQueryClient, validator sdk.AccAddress) { queryClient.On("ValidatorAccount", rpc.ContextWithHeight(1), &evmtypes.QueryValidatorAccountRequest{}). - Return(&evmtypes.QueryValidatorAccountResponse{AccountAddress: validator.String()}, nil) + Return(&evmtypes.QueryValidatorAccountResponse{AccountAddress: validator.String()}, nil). + Maybe() } func RegisterValidatorAccountWithConsAddress(queryClient *mocks.EVMQueryClient, validator sdk.AccAddress, consAddress string) { queryClient.On("ValidatorAccount", rpc.ContextWithHeight(1), &evmtypes.QueryValidatorAccountRequest{ ConsAddress: consAddress, }). - Return(&evmtypes.QueryValidatorAccountResponse{AccountAddress: validator.String()}, nil) + Return(&evmtypes.QueryValidatorAccountResponse{AccountAddress: validator.String()}, nil). + Maybe() } func RegisterValidatorAccountError(queryClient *mocks.EVMQueryClient) { queryClient.On("ValidatorAccount", rpc.ContextWithHeight(1), &evmtypes.QueryValidatorAccountRequest{}). - Return(nil, status.Error(codes.InvalidArgument, "empty request")) + Return(nil, status.Error(codes.InvalidArgument, "empty request")). + Maybe() } func TestRegisterValidatorAccount(t *testing.T) { @@ -280,23 +331,27 @@ func TestRegisterValidatorAccount(t *testing.T) { // Code func RegisterCode(queryClient *mocks.EVMQueryClient, addr common.Address, code []byte) { queryClient.On("Code", rpc.ContextWithHeight(1), &evmtypes.QueryCodeRequest{Address: addr.String()}). - Return(&evmtypes.QueryCodeResponse{Code: code}, nil) + Return(&evmtypes.QueryCodeResponse{Code: code}, nil). + Maybe() } func RegisterCodeError(queryClient *mocks.EVMQueryClient, addr common.Address) { queryClient.On("Code", rpc.ContextWithHeight(1), &evmtypes.QueryCodeRequest{Address: addr.String()}). - Return(nil, errortypes.ErrInvalidRequest) + Return(nil, errortypes.ErrInvalidRequest). + Maybe() } // Storage func RegisterStorageAt(queryClient *mocks.EVMQueryClient, addr common.Address, key string, storage string) { queryClient.On("Storage", rpc.ContextWithHeight(1), &evmtypes.QueryStorageRequest{Address: addr.String(), Key: key}). - Return(&evmtypes.QueryStorageResponse{Value: storage}, nil) + Return(&evmtypes.QueryStorageResponse{Value: storage}, nil). + Maybe() } func RegisterStorageAtError(queryClient *mocks.EVMQueryClient, addr common.Address, key string) { queryClient.On("Storage", rpc.ContextWithHeight(1), &evmtypes.QueryStorageRequest{Address: addr.String(), Key: key}). - Return(nil, errortypes.ErrInvalidRequest) + Return(nil, errortypes.ErrInvalidRequest). + Maybe() } func RegisterAccount(queryClient *mocks.EVMQueryClient, addr common.Address, height int64) { @@ -307,38 +362,45 @@ func RegisterAccount(queryClient *mocks.EVMQueryClient, addr common.Address, hei Nonce: 0, }, nil, - ) + ). + Maybe() } // Balance func RegisterBalance(queryClient *mocks.EVMQueryClient, addr common.Address, height int64) { queryClient.On("Balance", rpc.ContextWithHeight(height), &evmtypes.QueryBalanceRequest{Address: addr.String()}). - Return(&evmtypes.QueryBalanceResponse{Balance: "1"}, nil) + Return(&evmtypes.QueryBalanceResponse{Balance: "1"}, nil). + Maybe() } func RegisterBalanceInvalid(queryClient *mocks.EVMQueryClient, addr common.Address, height int64) { queryClient.On("Balance", rpc.ContextWithHeight(height), &evmtypes.QueryBalanceRequest{Address: addr.String()}). - Return(&evmtypes.QueryBalanceResponse{Balance: "invalid"}, nil) + Return(&evmtypes.QueryBalanceResponse{Balance: "invalid"}, nil). + Maybe() } func RegisterBalanceNegative(queryClient *mocks.EVMQueryClient, addr common.Address, height int64) { queryClient.On("Balance", rpc.ContextWithHeight(height), &evmtypes.QueryBalanceRequest{Address: addr.String()}). - Return(&evmtypes.QueryBalanceResponse{Balance: "-1"}, nil) + Return(&evmtypes.QueryBalanceResponse{Balance: "-1"}, nil). + Maybe() } func RegisterBalanceError(queryClient *mocks.EVMQueryClient, addr common.Address, height int64) { queryClient.On("Balance", rpc.ContextWithHeight(height), &evmtypes.QueryBalanceRequest{Address: addr.String()}). - Return(nil, errortypes.ErrInvalidRequest) + Return(nil, errortypes.ErrInvalidRequest). + Maybe() } // TraceCall func RegisterTraceCall(queryClient *mocks.EVMQueryClient, request *evmtypes.QueryTraceCallRequest, response *evmtypes.QueryTraceCallResponse) { queryClient.On("TraceCall", rpc.ContextWithHeight(request.BlockNumber), request). - Return(response, nil) + Return(response, nil). + Maybe() } func RegisterTraceCallError(queryClient *mocks.EVMQueryClient, request *evmtypes.QueryTraceCallRequest) { ctx, _ := context.WithCancel(rpc.ContextWithHeight(1)) queryClient.On("TraceCall", ctx, request). - Return(nil, errortypes.ErrInvalidRequest) + Return(nil, errortypes.ErrInvalidRequest). + Maybe() } diff --git a/rpc/backend/tx_info.go b/rpc/backend/tx_info.go index 31a30003f8..11116d8e87 100644 --- a/rpc/backend/tx_info.go +++ b/rpc/backend/tx_info.go @@ -16,7 +16,11 @@ package backend import ( + "encoding/json" "fmt" + "github.com/ethereum/go-ethereum/core/vm" + "github.com/ethereum/go-ethereum/eth/tracers/logger" + "github.com/ethereum/go-ethereum/params" errorsmod "cosmossdk.io/errors" tmrpcclient "github.com/cometbft/cometbft/rpc/client" @@ -480,3 +484,166 @@ func (b *Backend) GetTransactionByBlockAndIndex(block *tmrpctypes.ResultBlock, i b.chainID, ) } + +// CreateAccessList returns the list of addresses and storage keys used by the transaction (except for the +// sender account and precompiles), plus the estimated gas if the access list were added to the transaction. +func (b *Backend) CreateAccessList( + args evmtypes.TransactionArgs, + blockNrOrHash rpctypes.BlockNumberOrHash, + overrides *json.RawMessage, +) (*rpctypes.AccessListResult, error) { + accessList, gasUsed, vmErr, err := b.createAccessList(args, blockNrOrHash, overrides) + if err != nil { + return nil, err + } + + hexGasUsed := hexutil.Uint64(gasUsed) + result := rpctypes.AccessListResult{ + AccessList: &accessList, + GasUsed: &hexGasUsed, + } + if vmErr != nil { + result.Error = vmErr.Error() + } + return &result, nil +} + +// createAccessList creates the access list for the transaction. +// It iteratively expands the access list until it converges. +// If the access list has converged, the access list is returned. +// If the access list has not converged, an error is returned. +// If the transaction itself fails, an vmErr is returned. +func (b *Backend) createAccessList( + args evmtypes.TransactionArgs, + blockNrOrHash rpctypes.BlockNumberOrHash, + overrides *json.RawMessage, +) (ethtypes.AccessList, uint64, error, error) { + args, err := b.SetTxDefaults(args) + if err != nil { + b.logger.Error("failed to set tx defaults", "error", err) + return nil, 0, nil, err + } + + blockNum, err := b.BlockNumberFromTendermint(blockNrOrHash) + if err != nil { + b.logger.Error("failed to get block number", "error", err) + return nil, 0, nil, err + } + + addressesToExclude, err := b.getAccessListExcludes(args, blockNum) + if err != nil { + b.logger.Error("failed to get access list excludes", "error", err) + return nil, 0, nil, err + } + + prevTracer, traceArgs, err := b.initAccessListTracer(args, blockNum, addressesToExclude) + if err != nil { + b.logger.Error("failed to init access list tracer", "error", err) + return nil, 0, nil, err + } + + // iteratively expand the access list + for { + accessList := prevTracer.AccessList() + traceArgs.AccessList = &accessList + res, err := b.DoCall(*traceArgs, blockNum, overrides) + if err != nil { + b.logger.Error("failed to apply transaction", "error", err) + return nil, 0, nil, fmt.Errorf("failed to apply transaction: %v err: %v", traceArgs.ToTransaction().Hash(), err) + } + + // Check if access list has converged (no new addresses/slots accessed) + newTracer := logger.NewAccessListTracer(accessList, addressesToExclude) + if newTracer.Equal(prevTracer) { + b.logger.Info("access list converged", "accessList", accessList) + var vmErr error + if res.VmError != "" { + b.logger.Error("vm error after access list converged", "vmError", res.VmError) + vmErr = errors.New(res.VmError) + } + return accessList, res.GasUsed, vmErr, nil + } + prevTracer = newTracer + } +} + +// getAccessListExcludes returns the addresses to exclude from the access list. +// This includes the sender account, the target account (if provided), precompiles, +// and any addresses in the authorization list. +func (b *Backend) getAccessListExcludes(args evmtypes.TransactionArgs, blockNum rpctypes.BlockNumber) (map[common.Address]struct{}, error) { + header, err := b.HeaderByNumber(blockNum) + if err != nil { + b.logger.Error("failed to get header by number", "error", err) + return nil, err + } + + // exclude sender and precompiles + addressesToExclude := make(map[common.Address]struct{}) + addressesToExclude[args.GetFrom()] = struct{}{} + if args.To != nil { + addressesToExclude[*args.To] = struct{}{} + } + + isMerge := b.ChainConfig().MergeNetsplitBlock != nil + precompiles := vm.ActivePrecompiles(b.ChainConfig().Rules(header.Number, isMerge, header.Time)) + for _, addr := range precompiles { + addressesToExclude[addr] = struct{}{} + } + + // check if enough gas was provided to cover all authorization lists + maxAuthorizations := uint64(*args.Gas) / params.CallNewAccountGas + if uint64(len(args.AuthorizationList)) > maxAuthorizations { + b.logger.Error("insufficient gas to process all authorizations", "maxAuthorizations", maxAuthorizations) + return nil, errors.New("insufficient gas to process all authorizations") + } + + for _, auth := range args.AuthorizationList { + // validate authorization (duplicating stateTransition.validateAuthorization() logic from geth: https://github.com/ethereum/go-ethereum/blob/bf8f63dcd27e178bd373bfe41ea718efee2851dd/core/state_transition.go#L575) + nonceOverflow := auth.Nonce+1 < auth.Nonce + invalidChainID := !auth.ChainID.IsZero() && auth.ChainID.CmpBig(b.ChainConfig().ChainID) != 0 + if nonceOverflow || invalidChainID { + b.logger.Error("invalid authorization", "auth", auth) + continue + } + if authority, err := auth.Authority(); err == nil { + addressesToExclude[authority] = struct{}{} + } + } + + b.logger.Debug("access list excludes created", "addressesToExclude", addressesToExclude) + return addressesToExclude, nil +} + +// initAccessListTracer initializes the access list tracer for the transaction. +// It sets the default call arguments and creates a new access list tracer. +// If an access list is provided in args, it uses that instead of creating a new one. +func (b *Backend) initAccessListTracer(args evmtypes.TransactionArgs, blockNum rpctypes.BlockNumber, addressesToExclude map[common.Address]struct{}) (*logger.AccessListTracer, *evmtypes.TransactionArgs, error) { + header, err := b.HeaderByNumber(blockNum) + if err != nil { + b.logger.Error("failed to get header by number", "error", err) + return nil, nil, err + } + + if args.Nonce == nil { + pending := blockNum == rpctypes.EthPendingBlockNumber + nonce, err := b.getAccountNonce(args.GetFrom(), pending, blockNum.Int64(), b.logger) + if err != nil { + b.logger.Error("failed to get account nonce", "error", err) + return nil, nil, err + } + nonce64 := hexutil.Uint64(nonce) + args.Nonce = &nonce64 + } + if err = args.CallDefaults(b.RPCGasCap(), header.BaseFee, b.ChainConfig().ChainID); err != nil { + b.logger.Error("failed to set default call args", "error", err) + return nil, nil, err + } + + tracer := logger.NewAccessListTracer(nil, addressesToExclude) + if args.AccessList != nil { + tracer = logger.NewAccessListTracer(*args.AccessList, addressesToExclude) + } + + b.logger.Debug("access list tracer initialized", "tracer", tracer) + return tracer, &args, nil +} diff --git a/rpc/backend/tx_info_test.go b/rpc/backend/tx_info_test.go index 4b429dfdae..3d21bbd997 100644 --- a/rpc/backend/tx_info_test.go +++ b/rpc/backend/tx_info_test.go @@ -761,3 +761,234 @@ func (suite *BackendTestSuite) buildSetCodeTx() *evmtypes.MsgEthereumTx { return msgEthereumTx } + +func (suite *BackendTestSuite) TestCreateAccessList() { + testCases := []struct { + name string + malleate func() (evmtypes.TransactionArgs, rpctypes.BlockNumberOrHash) + expectError bool + errorContains string + expectGasUsed bool + expectAccList bool + }{ + { + name: "success - basic transaction", + malleate: func() (evmtypes.TransactionArgs, rpctypes.BlockNumberOrHash) { + from := common.HexToAddress("0x1234567890abcdef1234567890abcdef12345678") + to := common.HexToAddress("0xabcdefabcdefabcdefabcdefabcdefabcdefabcdef") + gas := hexutil.Uint64(21000) + value := (*hexutil.Big)(big.NewInt(1000)) + gasPrice := (*hexutil.Big)(big.NewInt(20000000000)) + + args := evmtypes.TransactionArgs{ + From: &from, + To: &to, + Gas: &gas, + Value: value, + GasPrice: gasPrice, + } + + blockNum := rpctypes.EthLatestBlockNumber + blockNumOrHash := rpctypes.BlockNumberOrHash{ + BlockNumber: &blockNum, + } + queryClient := suite.backend.queryClient.QueryClient.(*mocks.EVMQueryClient) + RegisterEthCallAny(queryClient, 100, "") + + return args, blockNumOrHash + }, + expectError: false, + expectGasUsed: true, + expectAccList: true, + }, + { + name: "success - transaction with data", + malleate: func() (evmtypes.TransactionArgs, rpctypes.BlockNumberOrHash) { + from := common.HexToAddress("0x1234567890abcdef1234567890abcdef12345678") + to := common.HexToAddress("0xabcdefabcdefabcdefabcdefabcdefabcdefabcdef") + gas := hexutil.Uint64(100000) + gasPrice := (*hexutil.Big)(big.NewInt(20000000000)) + data := hexutil.Bytes("0xa9059cbb") + + args := evmtypes.TransactionArgs{ + From: &from, + To: &to, + Gas: &gas, + GasPrice: gasPrice, + Data: &data, + } + + blockNum := rpctypes.EthLatestBlockNumber + blockNumOrHash := rpctypes.BlockNumberOrHash{ + BlockNumber: &blockNum, + } + queryClient := suite.backend.queryClient.QueryClient.(*mocks.EVMQueryClient) + RegisterEthCallAny(queryClient, 100, "") + + return args, blockNumOrHash + }, + expectError: false, + expectGasUsed: true, + expectAccList: true, + }, + { + name: "success - transaction with existing access list", + malleate: func() (evmtypes.TransactionArgs, rpctypes.BlockNumberOrHash) { + from := common.HexToAddress("0x1234567890abcdef1234567890abcdef12345678") + to := common.HexToAddress("0xabcdefabcdefabcdefabcdefabcdefabcdefabcdef") + gas := hexutil.Uint64(100000) + gasPrice := (*hexutil.Big)(big.NewInt(20000000000)) + + accessList := ethtypes.AccessList{ + { + Address: common.HexToAddress("0x1111111111111111111111111111111111111111"), + StorageKeys: []common.Hash{ + common.HexToHash("0x0000000000000000000000000000000000000000000000000000000000000001"), + }, + }, + } + + args := evmtypes.TransactionArgs{ + From: &from, + To: &to, + Gas: &gas, + GasPrice: gasPrice, + AccessList: &accessList, + } + + blockNum := rpctypes.EthLatestBlockNumber + blockNumOrHash := rpctypes.BlockNumberOrHash{ + BlockNumber: &blockNum, + } + queryClient := suite.backend.queryClient.QueryClient.(*mocks.EVMQueryClient) + RegisterEthCallAny(queryClient, 100, "") + + return args, blockNumOrHash + }, + expectError: false, + expectGasUsed: true, + expectAccList: true, + }, + { + name: "success - transaction with specific block hash", + malleate: func() (evmtypes.TransactionArgs, rpctypes.BlockNumberOrHash) { + from := common.HexToAddress("0x1234567890abcdef1234567890abcdef12345678") + to := common.HexToAddress("0xabcdefabcdefabcdefabcdefabcdefabcdefabcdef") + gas := hexutil.Uint64(21000) + gasPrice := (*hexutil.Big)(big.NewInt(20000000000)) + + args := evmtypes.TransactionArgs{ + From: &from, + To: &to, + Gas: &gas, + GasPrice: gasPrice, + } + + blockHash := common.HexToHash("0x1234567890abcdef1234567890abcdef1234567890abcdef1234567890abcdef12") + blockNumOrHash := rpctypes.BlockNumberOrHash{ + BlockHash: &blockHash, + } + queryClient := suite.backend.queryClient.QueryClient.(*mocks.EVMQueryClient) + RegisterEthCallAny(queryClient, 100, "") + + return args, blockNumOrHash + }, + expectError: false, + expectGasUsed: true, + expectAccList: true, + }, + { + name: "error - missing from address", + malleate: func() (evmtypes.TransactionArgs, rpctypes.BlockNumberOrHash) { + to := common.HexToAddress("0xabcdefabcdefabcdefabcdefabcdefabcdefabcdef") + gas := hexutil.Uint64(21000) + gasPrice := (*hexutil.Big)(big.NewInt(20000000000)) + + args := evmtypes.TransactionArgs{ + To: &to, + Gas: &gas, + GasPrice: gasPrice, + } + + blockNum := rpctypes.EthLatestBlockNumber + blockNumOrHash := rpctypes.BlockNumberOrHash{ + BlockNumber: &blockNum, + } + queryClient := suite.backend.queryClient.QueryClient.(*mocks.EVMQueryClient) + RegisterEthCallAny(queryClient, 0, "error") + + return args, blockNumOrHash + }, + expectError: true, + expectGasUsed: false, + expectAccList: false, + }, + { + name: "error - invalid gas limit", + malleate: func() (evmtypes.TransactionArgs, rpctypes.BlockNumberOrHash) { + from := common.HexToAddress("0x1234567890abcdef1234567890abcdef12345678") + to := common.HexToAddress("0xabcdefabcdefabcdefabcdefabcdefabcdefabcdef") + gas := hexutil.Uint64(0) + gasPrice := (*hexutil.Big)(big.NewInt(20000000000)) + + args := evmtypes.TransactionArgs{ + From: &from, + To: &to, + Gas: &gas, + GasPrice: gasPrice, + } + + blockNum := rpctypes.EthLatestBlockNumber + blockNumOrHash := rpctypes.BlockNumberOrHash{ + BlockNumber: &blockNum, + } + queryClient := suite.backend.queryClient.QueryClient.(*mocks.EVMQueryClient) + RegisterEthCallAny(queryClient, 0, "error") + + return args, blockNumOrHash + }, + expectError: true, + expectGasUsed: false, + expectAccList: false, + }, + } + + for _, tc := range testCases { + suite.Run(tc.name, func() { + suite.SetupTest() + suite.SetupMockCLient() + + args, blockNumOrHash := tc.malleate() + + suite.Require().True(blockNumOrHash.BlockNumber != nil || blockNumOrHash.BlockHash != nil, + "BlockNumberOrHash should have either BlockNumber or BlockHash set") + + if !tc.expectError || tc.name != "error - missing from address" { + suite.Require().NotEqual(common.Address{}, args.GetFrom(), "From address should not be zero") + } + + result, err := suite.backend.CreateAccessList(args, blockNumOrHash, nil) + + if tc.expectError { + suite.Require().Error(err) + suite.Require().Nil(result) + if tc.errorContains != "" { + suite.Require().Contains(err.Error(), tc.errorContains) + } + return + } + + suite.Require().NoError(err) + suite.Require().NotNil(result) + + if tc.expectGasUsed { + suite.Require().NotNil(result.GasUsed) + suite.Require().Greater(uint64(*result.GasUsed), uint64(0)) + } + + if tc.expectAccList { + suite.Require().NotNil(result.AccessList) + } + }) + } +} diff --git a/rpc/namespaces/ethereum/eth/api.go b/rpc/namespaces/ethereum/eth/api.go index 926e129273..081a8da526 100644 --- a/rpc/namespaces/ethereum/eth/api.go +++ b/rpc/namespaces/ethereum/eth/api.go @@ -115,6 +115,7 @@ type EthereumAPI interface { FillTransaction(args evmtypes.TransactionArgs) (*rpctypes.SignTransactionResult, error) Resend(ctx context.Context, args evmtypes.TransactionArgs, gasPrice *hexutil.Big, gasLimit *hexutil.Uint64) (common.Hash, error) GetPendingTransactions() ([]*rpctypes.RPCTransaction, error) + CreateAccessList(args evmtypes.TransactionArgs, blockNrOrHash rpctypes.BlockNumberOrHash, overrides *json.RawMessage) (*rpctypes.AccessListResult, error) // eth_signTransaction (on Ethereum.org) // eth_getCompilers (on Ethereum.org) // eth_compileSolidity (on Ethereum.org) @@ -531,3 +532,17 @@ func (e *PublicAPI) GetPendingTransactions() ([]*rpctypes.RPCTransaction, error) return result, nil } + +// CreateAccessList returns the list of addresses and storage keys used by the transaction (except for the +// sender account and precompiles), plus the estimated gas if the access list were added to the transaction. +func (e *PublicAPI) CreateAccessList( + args evmtypes.TransactionArgs, + blockNrOrHash rpctypes.BlockNumberOrHash, + overrides *json.RawMessage, +) (*rpctypes.AccessListResult, error) { + res, err := e.backend.CreateAccessList(args, blockNrOrHash, overrides) + if err != nil { + return nil, err + } + return res, nil +} diff --git a/rpc/types/types.go b/rpc/types/types.go index f7c5dae0f5..b1fa51436f 100644 --- a/rpc/types/types.go +++ b/rpc/types/types.go @@ -187,6 +187,13 @@ type OneFeeHistory struct { GasUsedRatio float64 // the ratio of gas used to the gas limit for each block } +// AccessListResult represents the access list and gas used for a transaction +type AccessListResult struct { + AccessList *ethtypes.AccessList `json:"accessList"` + GasUsed *hexutil.Uint64 `json:"gasUsed"` + Error string `json:"error"` +} + type TraceConfig struct { evmtypes.TraceConfig TracerConfig json.RawMessage `json:"tracerConfig"` diff --git a/x/evm/types/tracer.go b/x/evm/types/tracer.go index da777130b4..237052360b 100644 --- a/x/evm/types/tracer.go +++ b/x/evm/types/tracer.go @@ -44,8 +44,10 @@ func NewTracer(tracer string, msg core.Message, cfg *params.ChainConfig, height switch tracer { case TracerAccessList: - blockAddrs := map[common.Address]struct{}{ - *msg.To: {}, msg.From: {}, + blockAddrs := make(map[common.Address]struct{}) + blockAddrs[msg.From] = struct{}{} + if msg.To != nil { + blockAddrs[*msg.To] = struct{}{} } precompiles := vm.ActivePrecompiles(cfg.Rules(big.NewInt(height), cfg.MergeNetsplitBlock != nil, timestamp)) for _, addr := range precompiles { diff --git a/x/evm/types/tx_args.go b/x/evm/types/tx_args.go index 13b1fe1a5d..bb01b3a4dc 100644 --- a/x/evm/types/tx_args.go +++ b/x/evm/types/tx_args.go @@ -18,6 +18,8 @@ package types import ( "errors" "fmt" + "github.com/ethereum/go-ethereum/log" + "math" "math/big" "github.com/ethereum/go-ethereum/common" @@ -251,3 +253,51 @@ func (args *TransactionArgs) GetData() []byte { } return nil } + +// CallDefaults sanitizes the transaction arguments, often filling in zero values, +// for the purpose of eth_call class of RPC methods. +func (args *TransactionArgs) CallDefaults(globalGasCap uint64, baseFee *big.Int, chainID *big.Int) error { + // Reject invalid combinations of pre- and post-1559 fee styles + if args.GasPrice != nil && (args.MaxFeePerGas != nil || args.MaxPriorityFeePerGas != nil) { + return errors.New("both gasPrice and (maxFeePerGas or maxPriorityFeePerGas) specified") + } + if args.ChainID == nil { + args.ChainID = (*hexutil.Big)(chainID) + } else { + if have := (*big.Int)(args.ChainID); have.Cmp(chainID) != 0 { + return fmt.Errorf("chainId does not match node's (have=%v, want=%v)", have, chainID) + } + } + if args.Gas == nil { + gas := globalGasCap + if gas == 0 { + gas = uint64(math.MaxUint64 / 2) + } + args.Gas = (*hexutil.Uint64)(&gas) + } else if globalGasCap > 0 && globalGasCap < uint64(*args.Gas) { + log.Warn("Caller gas above allowance, capping", "requested", args.Gas, "cap", globalGasCap) + args.Gas = (*hexutil.Uint64)(&globalGasCap) + } + if args.Nonce == nil { + args.Nonce = new(hexutil.Uint64) + } + if args.Value == nil { + args.Value = new(hexutil.Big) + } + if baseFee == nil { + // If there's no basefee, then it must be a non-1559 execution + if args.GasPrice == nil { + args.GasPrice = new(hexutil.Big) + } + } else { + // A basefee is provided, necessitating 1559-type execution + if args.MaxFeePerGas == nil { + args.MaxFeePerGas = new(hexutil.Big) + } + if args.MaxPriorityFeePerGas == nil { + args.MaxPriorityFeePerGas = new(hexutil.Big) + } + } + + return nil +} From dd48cad874e42db2f0fce21ddf8f54d5657d41de Mon Sep 17 00:00:00 2001 From: Thomas Nguy Date: Thu, 30 Oct 2025 09:53:44 +0900 Subject: [PATCH 2/4] add changelog --- CHANGELOG.md | 3 +++ 1 file changed, 3 insertions(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index ba97e44ae6..21ca807483 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -37,11 +37,14 @@ Ref: https://keepachangelog.com/en/1.0.0/ ## Unreleased +### Features + * (evm) [#725](https://github.com/crypto-org-chain/ethermint/pull/725) feat(RPC): add authorizationList from eth_getTransactionByHash response for EIP-7702 transactions * (evm) [#740](https://github.com/crypto-org-chain/ethermint/pull/740) fix: missing tx context during vm initialisation * (evm) [#742](https://github.com/crypto-org-chain/ethermint/pull/742) fix: prevent nil pointer dereference in tracer hooks * (evm) [#728](https://github.com/crypto-org-chain/ethermint/pull/728) feat: support preinstalls * (evm) [#722](https://github.com/crypto-org-chain/ethermint/pull/722) feat: support EIP-2935 +* (api) [#768](https://github.com/crypto-org-chain/ethermint/pull/768) feat: support create access list ## [v0.22.0] - 2025-08-12 From 920920630fc930f719af1e626f50ae5520a73c12 Mon Sep 17 00:00:00 2001 From: Thomas Nguy Date: Thu, 30 Oct 2025 10:43:01 +0900 Subject: [PATCH 3/4] fix lint --- rpc/backend/backend.go | 6 +++++- rpc/backend/tx_info.go | 7 ++++++- rpc/namespaces/ethereum/eth/api.go | 6 +++++- x/evm/types/tx_args.go | 3 ++- 4 files changed, 18 insertions(+), 4 deletions(-) diff --git a/rpc/backend/backend.go b/rpc/backend/backend.go index d6cc770f3c..f7a66b977c 100644 --- a/rpc/backend/backend.go +++ b/rpc/backend/backend.go @@ -125,7 +125,11 @@ type EVMBackend interface { GetTransactionReceipt(hash common.Hash, resBlock *tmrpctypes.ResultBlock) (map[string]interface{}, error) GetTransactionByBlockHashAndIndex(hash common.Hash, idx hexutil.Uint) (*rpctypes.RPCTransaction, error) GetTransactionByBlockNumberAndIndex(blockNum rpctypes.BlockNumber, idx hexutil.Uint) (*rpctypes.RPCTransaction, error) - CreateAccessList(args evmtypes.TransactionArgs, blockNrOrHash rpctypes.BlockNumberOrHash, overrides *json.RawMessage) (*rpctypes.AccessListResult, error) + CreateAccessList( + args evmtypes.TransactionArgs, + blockNrOrHash rpctypes.BlockNumberOrHash, + overrides *json.RawMessage, + ) (*rpctypes.AccessListResult, error) // Send Transaction Resend(args evmtypes.TransactionArgs, gasPrice *hexutil.Big, gasLimit *hexutil.Uint64) (common.Hash, error) diff --git a/rpc/backend/tx_info.go b/rpc/backend/tx_info.go index 11116d8e87..43945b13bb 100644 --- a/rpc/backend/tx_info.go +++ b/rpc/backend/tx_info.go @@ -18,6 +18,7 @@ package backend import ( "encoding/json" "fmt" + "github.com/ethereum/go-ethereum/core/vm" "github.com/ethereum/go-ethereum/eth/tracers/logger" "github.com/ethereum/go-ethereum/params" @@ -617,7 +618,11 @@ func (b *Backend) getAccessListExcludes(args evmtypes.TransactionArgs, blockNum // initAccessListTracer initializes the access list tracer for the transaction. // It sets the default call arguments and creates a new access list tracer. // If an access list is provided in args, it uses that instead of creating a new one. -func (b *Backend) initAccessListTracer(args evmtypes.TransactionArgs, blockNum rpctypes.BlockNumber, addressesToExclude map[common.Address]struct{}) (*logger.AccessListTracer, *evmtypes.TransactionArgs, error) { +func (b *Backend) initAccessListTracer( + args evmtypes.TransactionArgs, + blockNum rpctypes.BlockNumber, + addressesToExclude map[common.Address]struct{}, +) (*logger.AccessListTracer, *evmtypes.TransactionArgs, error) { header, err := b.HeaderByNumber(blockNum) if err != nil { b.logger.Error("failed to get header by number", "error", err) diff --git a/rpc/namespaces/ethereum/eth/api.go b/rpc/namespaces/ethereum/eth/api.go index 081a8da526..89b32ee8dd 100644 --- a/rpc/namespaces/ethereum/eth/api.go +++ b/rpc/namespaces/ethereum/eth/api.go @@ -115,7 +115,11 @@ type EthereumAPI interface { FillTransaction(args evmtypes.TransactionArgs) (*rpctypes.SignTransactionResult, error) Resend(ctx context.Context, args evmtypes.TransactionArgs, gasPrice *hexutil.Big, gasLimit *hexutil.Uint64) (common.Hash, error) GetPendingTransactions() ([]*rpctypes.RPCTransaction, error) - CreateAccessList(args evmtypes.TransactionArgs, blockNrOrHash rpctypes.BlockNumberOrHash, overrides *json.RawMessage) (*rpctypes.AccessListResult, error) + CreateAccessList( + args evmtypes.TransactionArgs, + blockNrOrHash rpctypes.BlockNumberOrHash, + overrides *json.RawMessage, + ) (*rpctypes.AccessListResult, error) // eth_signTransaction (on Ethereum.org) // eth_getCompilers (on Ethereum.org) // eth_compileSolidity (on Ethereum.org) diff --git a/x/evm/types/tx_args.go b/x/evm/types/tx_args.go index bb01b3a4dc..d16809c1e0 100644 --- a/x/evm/types/tx_args.go +++ b/x/evm/types/tx_args.go @@ -18,10 +18,11 @@ package types import ( "errors" "fmt" - "github.com/ethereum/go-ethereum/log" "math" "math/big" + "github.com/ethereum/go-ethereum/log" + "github.com/ethereum/go-ethereum/common" "github.com/ethereum/go-ethereum/common/hexutil" "github.com/ethereum/go-ethereum/core" From f4606c5323309641e655ed9d4a1ec272576f7966 Mon Sep 17 00:00:00 2001 From: Thomas Nguy Date: Thu, 30 Oct 2025 14:42:35 +0900 Subject: [PATCH 4/4] make gas limit consistant --- x/evm/types/tx_args.go | 40 ++++++++++++++++++---------------------- 1 file changed, 18 insertions(+), 22 deletions(-) diff --git a/x/evm/types/tx_args.go b/x/evm/types/tx_args.go index d16809c1e0..337be39862 100644 --- a/x/evm/types/tx_args.go +++ b/x/evm/types/tx_args.go @@ -18,11 +18,8 @@ package types import ( "errors" "fmt" - "math" "math/big" - "github.com/ethereum/go-ethereum/log" - "github.com/ethereum/go-ethereum/common" "github.com/ethereum/go-ethereum/common/hexutil" "github.com/ethereum/go-ethereum/core" @@ -31,6 +28,9 @@ import ( "github.com/holiman/uint256" ) +// Ethereum block size is ~36000000, we use this value as default in case neither gas or global cap is specified, to protect against DOS +const defaultMaxGas = uint64(100000000) + // TransactionArgs represents the arguments to construct a new transaction // or a message call using JSON-RPC. // Duplicate struct definition since geth struct is in internal package @@ -158,15 +158,7 @@ func (args *TransactionArgs) ToMessage(globalGasCap uint64, baseFee *big.Int) (* // Set sender address or use zero address if none specified. addr := args.GetFrom() - // Ethereum block size is ~36000000, we use this value as default in case neither gas or global cap is specified, to protect against DOS - defaultGas := uint64(100000000) - gas := defaultGas - if args.Gas != nil { - gas = uint64(*args.Gas) - } - if globalGasCap != 0 && globalGasCap < gas { - gas = globalGasCap - } + gas := computeGasLimit(args.Gas, globalGasCap) var ( gasPrice *big.Int @@ -269,16 +261,6 @@ func (args *TransactionArgs) CallDefaults(globalGasCap uint64, baseFee *big.Int, return fmt.Errorf("chainId does not match node's (have=%v, want=%v)", have, chainID) } } - if args.Gas == nil { - gas := globalGasCap - if gas == 0 { - gas = uint64(math.MaxUint64 / 2) - } - args.Gas = (*hexutil.Uint64)(&gas) - } else if globalGasCap > 0 && globalGasCap < uint64(*args.Gas) { - log.Warn("Caller gas above allowance, capping", "requested", args.Gas, "cap", globalGasCap) - args.Gas = (*hexutil.Uint64)(&globalGasCap) - } if args.Nonce == nil { args.Nonce = new(hexutil.Uint64) } @@ -299,6 +281,20 @@ func (args *TransactionArgs) CallDefaults(globalGasCap uint64, baseFee *big.Int, args.MaxPriorityFeePerGas = new(hexutil.Big) } } + gas := computeGasLimit(args.Gas, globalGasCap) + args.Gas = (*hexutil.Uint64)(&gas) return nil } + +func computeGasLimit(argGas *hexutil.Uint64, globalGasCap uint64) uint64 { + gas := defaultMaxGas + if argGas != nil { + gas = uint64(*argGas) + } + if globalGasCap != 0 && globalGasCap < gas { + gas = globalGasCap + } + + return gas +}