Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
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
1 change: 1 addition & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -51,6 +51,7 @@
- [\#684](https://github.com/cosmos/evm/pull/684) Add unit test cases for EIP-7702
- [\#685](https://github.com/cosmos/evm/pull/685) Add EIP-7702 e2e test
- [\#680](https://github.com/cosmos/evm/pull/680) Introduce a `StaticPrecompiles` builder
- [\#691](https://github.com/cosmos/evm/pull/691) Add debug_traceCall api

### FEATURES

Expand Down
1,911 changes: 1,695 additions & 216 deletions api/cosmos/evm/vm/v1/query.pulsar.go

Large diffs are not rendered by default.

39 changes: 39 additions & 0 deletions api/cosmos/evm/vm/v1/query_grpc.pb.go

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

31 changes: 31 additions & 0 deletions proto/cosmos/evm/vm/v1/query.proto
Original file line number Diff line number Diff line change
Expand Up @@ -75,6 +75,11 @@ service Query {
option (google.api.http).get = "/cosmos/evm/vm/v1/trace_block";
}

// TraceCall implements the `debug_traceCall` rpc api
rpc TraceCall(QueryTraceCallRequest) returns (QueryTraceCallResponse) {
option (google.api.http).get = "/cosmos/evm/vm/v1/trace_call";
}

// BaseFee queries the base fee of the parent block of the current block,
// it's similar to feemarket module's method, but also checks london hardfork
// status.
Expand Down Expand Up @@ -343,6 +348,32 @@ message QueryTraceBlockResponse {
bytes data = 1;
}

// QueryTraceCallRequest defines TraceCall request
message QueryTraceCallRequest {
// args uses the same json format as the json rpc api.
bytes args = 1;
// gas_cap defines the default gas cap to be used
uint64 gas_cap = 2;
// proposer_address of the requested block in hex format
bytes proposer_address = 3 [(gogoproto.casttype) = "github.com/cosmos/cosmos-sdk/types.ConsAddress"];
// trace_config holds extra parameters to trace functions.
TraceConfig trace_config = 4;
// block_number of requested transaction
int64 block_number = 5;
// block_hash of requested transaction
string block_hash = 6;
// block_time of requested transaction
google.protobuf.Timestamp block_time = 7 [(gogoproto.nullable) = false, (gogoproto.stdtime) = true];
// chain_id is the the eip155 chain id parsed from the requested block header
int64 chain_id = 8;
}

// QueryTraceCallResponse defines TraceCall response
message QueryTraceCallResponse {
// data is the response serialized in bytes
bytes data = 1;
}

// QueryBaseFeeRequest defines the request type for querying the EIP1559 base
// fee.
message QueryBaseFeeRequest {}
Expand Down
1 change: 1 addition & 0 deletions rpc/backend/backend.go
Original file line number Diff line number Diff line change
Expand Up @@ -134,6 +134,7 @@ type EVMBackend interface {
// Tracing
TraceTransaction(hash common.Hash, config *types.TraceConfig) (interface{}, error)
TraceBlock(height types.BlockNumber, config *types.TraceConfig, block *tmrpctypes.ResultBlock) ([]*evmtypes.TxTraceResult, error)
TraceCall(args evmtypes.TransactionArgs, blockNrOrHash types.BlockNumberOrHash, config *types.TraceConfig) (interface{}, error)
}

var _ BackendI = (*Backend)(nil)
Expand Down
37 changes: 37 additions & 0 deletions rpc/backend/mocks/evm_query_client.go

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

66 changes: 66 additions & 0 deletions rpc/backend/tracing.go
Original file line number Diff line number Diff line change
Expand Up @@ -229,3 +229,69 @@ func (b *Backend) TraceBlock(height rpctypes.BlockNumber,

return decodedResults, nil
}

// TraceCall executes a call with the given arguments and returns the structured logs
// created during the execution of EVM. It returns them as a JSON object.
// Note: This implementation requires the gRPC TraceCall method to be implemented on the server side.
func (b *Backend) TraceCall(
args evmtypes.TransactionArgs,
blockNrOrHash rpctypes.BlockNumberOrHash,
config *rpctypes.TraceConfig,
) (interface{}, error) {
// Marshal tx args
bz, err := json.Marshal(&args)
if err != nil {
return nil, err
}

// Get block number from blockNrOrHash
blockNr, err := b.BlockNumberFromComet(blockNrOrHash)
if err != nil {
return nil, err
}

// Get the block to get necessary context
header, err := b.CometHeaderByNumber(blockNr)
if err != nil {
b.Logger.Debug("block not found", "number", blockNr)
return nil, err
}

traceCallRequest := evmtypes.QueryTraceCallRequest{
Args: bz,
GasCap: b.RPCGasCap(),
ProposerAddress: sdk.ConsAddress(header.Header.ProposerAddress),
BlockNumber: header.Header.Height,
BlockHash: common.Bytes2Hex(header.Header.Hash()),
BlockTime: header.Header.Time,
ChainId: b.EvmChainID.Int64(),
}

if config != nil {
traceCallRequest.TraceConfig = b.convertConfig(config)
}

// get the context of provided block
contextHeight := header.Header.Height
if contextHeight < 1 {
// 0 is a special value in `ContextWithHeight`
contextHeight = 1
}

// Use the block height as context for the query
ctxWithHeight := rpctypes.ContextWithHeight(contextHeight)
traceResult, err := b.QueryClient.TraceCall(ctxWithHeight, &traceCallRequest)
if err != nil {
return nil, err
}

// Response format is unknown due to custom tracer config param
// More information can be found here https://geth.ethereum.org/docs/dapp/tracing-filtered
var decodedResult interface{}
err = json.Unmarshal(traceResult.Data, &decodedResult)
if err != nil {
return nil, err
}

return decodedResult, nil
}
68 changes: 68 additions & 0 deletions rpc/namespaces/ethereum/debug/api.go
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,9 @@ package debug
import (
"bytes"
"errors"
"fmt"
"io"
"math"
"os"
"runtime"
"runtime/debug"
Expand All @@ -14,6 +16,7 @@ import (
"github.com/davecgh/go-spew/spew"
"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/rlp"
stderrors "github.com/pkg/errors"

Expand Down Expand Up @@ -102,6 +105,71 @@ func (a *API) TraceBlockByHash(hash common.Hash, config *rpctypes.TraceConfig) (
return a.backend.TraceBlock(rpctypes.BlockNumber(resBlock.Block.Height), config, resBlock)
}

// TraceBlock returns the structured logs created during the execution of
// EVM and returns them as a JSON object. It accepts an RLP-encoded block.
func (a *API) TraceBlock(tblockRlp hexutil.Bytes, config *rpctypes.TraceConfig) ([]*evmtypes.TxTraceResult, error) {
a.logger.Debug("debug_traceBlock", "size", len(tblockRlp))
// Decode RLP-encoded block
var block types.Block
if err := rlp.DecodeBytes(tblockRlp, &block); err != nil {
a.logger.Debug("failed to decode block", "error", err.Error())
return nil, fmt.Errorf("could not decode block: %w", err)
}

// Get block number from the decoded block
blockNum := block.NumberU64()
if blockNum > math.MaxInt64 {
return nil, fmt.Errorf("block number overflow: %d exceeds max int64", blockNum)
}
blockNumber := rpctypes.BlockNumber(blockNum) //#nosec G115 -- overflow checked above
a.logger.Debug("decoded block", "number", blockNumber, "hash", block.Hash().Hex())

// Get CometBFT block by number (not hash, as Ethereum block hash may differ from CometBFT hash)
resBlock, err := a.backend.CometBlockByNumber(blockNumber)
if err != nil {
a.logger.Debug("get block failed", "number", blockNumber, "error", err.Error())
return nil, err
}

if resBlock == nil || resBlock.Block == nil {
a.logger.Debug("block not found", "number", blockNumber)
return nil, errors.New("block not found")
}

return a.backend.TraceBlock(blockNumber, config, resBlock)
}

// TraceCall lets you trace a given eth_call. It collects the structured logs
// created during the execution of EVM if the given transaction was added on
// top of the provided block and returns them as a JSON object.
func (a *API) TraceCall(args evmtypes.TransactionArgs, blockNrOrHash rpctypes.BlockNumberOrHash, config *rpctypes.TraceConfig) (interface{}, error) {
a.logger.Debug("debug_traceCall", "args", args, "block number or hash", blockNrOrHash)
return a.backend.TraceCall(args, blockNrOrHash, config)
}

// GetRawBlock retrieves the RLP-encoded block by block number or hash.
func (a *API) GetRawBlock(blockNrOrHash rpctypes.BlockNumberOrHash) (hexutil.Bytes, error) {
a.logger.Debug("debug_getRawBlock", "block number or hash", blockNrOrHash)

// Get block number from blockNrOrHash
blockNum, err := a.backend.BlockNumberFromComet(blockNrOrHash)
if err != nil {
return nil, err
}

// Get Ethereum block by number
block, err := a.backend.EthBlockByNumber(blockNum)
if err != nil {
return nil, err
}
if block == nil {
return nil, fmt.Errorf("block not found")
}

// Encode block to RLP
return rlp.EncodeToBytes(block)
}

// BlockProfile turns on goroutine profiling for nsec seconds and writes profile data to
// file. It uses a profile rate of 1 for most accurate information. If a different rate is
// desired, set the rate and write the profile manually.
Expand Down
19 changes: 19 additions & 0 deletions tests/integration/rpc/backend/test_evm_query_client.go
Original file line number Diff line number Diff line change
Expand Up @@ -84,6 +84,25 @@ func RegisterTraceBlockError(queryClient *mocks.EVMQueryClient) {
Return(nil, errortypes.ErrInvalidRequest)
}

// TraceCall
func RegisterTraceCall(queryClient *mocks.EVMQueryClient, msgEthTx *evmtypes.MsgEthereumTx) {
data := []byte{0x7b, 0x22, 0x74, 0x65, 0x73, 0x74, 0x22, 0x3a, 0x20, 0x22, 0x74, 0x72, 0x61, 0x63, 0x65, 0x5f, 0x63, 0x61, 0x6c, 0x6c, 0x22, 0x7d} // {"test": "trace_call"}
queryClient.On("TraceCall", rpc.ContextWithHeight(1), mock.AnythingOfType("*types.QueryTraceCallRequest")).
Return(&evmtypes.QueryTraceCallResponse{Data: data}, nil)
}

func RegisterTraceCallWithTracer(queryClient *mocks.EVMQueryClient, msgEthTx *evmtypes.MsgEthereumTx, tracer string) {
data := []byte{0x7b, 0x22, 0x74, 0x79, 0x70, 0x65, 0x22, 0x3a, 0x20, 0x22, 0x43, 0x41, 0x4c, 0x4c, 0x22, 0x7d} // {"type": "CALL"}
queryClient.On("TraceCall", rpc.ContextWithHeight(1), mock.MatchedBy(func(req *evmtypes.QueryTraceCallRequest) bool {
return req.TraceConfig != nil && req.TraceConfig.Tracer == tracer
})).Return(&evmtypes.QueryTraceCallResponse{Data: data}, nil)
}

func RegisterTraceCallError(queryClient *mocks.EVMQueryClient) {
queryClient.On("TraceCall", rpc.ContextWithHeight(1), mock.AnythingOfType("*types.QueryTraceCallRequest")).
Return(nil, errortypes.ErrInvalidRequest)
}

// Params
func RegisterParams(queryClient *mocks.EVMQueryClient, header *metadata.MD, height int64) {
queryClient.On("Params", rpc.ContextWithHeight(height), &evmtypes.QueryParamsRequest{}, grpc.Header(header)).
Expand Down
Loading
Loading