Skip to content
Merged
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
167 changes: 137 additions & 30 deletions rpc/backend/tx_info.go
Original file line number Diff line number Diff line change
Expand Up @@ -5,12 +5,14 @@ import (
"fmt"
"math"
"math/big"
"strings"
"time"

"github.com/ethereum/go-ethereum/common"
"github.com/ethereum/go-ethereum/common/hexutil"
ethtypes "github.com/ethereum/go-ethereum/core/types"
"github.com/ethereum/go-ethereum/core/vm"
"github.com/ethereum/go-ethereum/crypto"
"github.com/ethereum/go-ethereum/eth/tracers/logger"
"github.com/ethereum/go-ethereum/params"
"github.com/pkg/errors"
Expand All @@ -23,12 +25,77 @@ import (
servertypes "github.com/cosmos/evm/server/types"
"github.com/cosmos/evm/utils"
evmtypes "github.com/cosmos/evm/x/vm/types"
"github.com/cosmos/evm/x/vm/types/legacy"

errorsmod "cosmossdk.io/errors"

sdk "github.com/cosmos/cosmos-sdk/types"
)

// asEthereumTxMsg converts a sdk.Msg to EthereumTxMsg interface.
// It handles both the new MsgEthereumTx and legacy MsgEthereumTx formats.
func asEthereumTxMsg(msg sdk.Msg) (evmtypes.EthereumTxMsg, bool) {
// Try new format first
if ethMsg, ok := msg.(*evmtypes.MsgEthereumTx); ok {
return ethMsg, true
}
// Try legacy format
if legacyMsg, ok := msg.(*legacy.MsgEthereumTx); ok {
return legacyMsg, true
}
return nil, false
}

// isLegacyTxError checks if an error indicates a legacy transaction format issue
func isLegacyTxError(err error) bool {
if err == nil {
return false
}
errStr := err.Error()
return strings.Contains(errStr, "errUnknownField") &&
strings.Contains(errStr, "MsgEthereumTx")
}

// decodeTxWithLegacyFallback decodes a transaction, falling back to legacy decoding if needed.
// It returns the EthereumTxMsg at the given message index.
func (b *Backend) decodeTxWithLegacyFallback(txBytes []byte, msgIndex int) (evmtypes.EthereumTxMsg, error) {
// Try standard decoder first
tx, err := b.ClientCtx.TxConfig.TxDecoder()(txBytes)
if err == nil {
msgs := tx.GetMsgs()
if msgIndex >= len(msgs) {
return nil, fmt.Errorf("message index %d out of bounds", msgIndex)
}
if msg, ok := asEthereumTxMsg(msgs[msgIndex]); ok {
return msg, nil
}
return nil, errors.New("not an ethereum tx")
}

// If it's a legacy tx error, try to decode as legacy
if !isLegacyTxError(err) {
return nil, err
}

// Fallback: decode using legacy-aware decoder
legacyTx, legacyErr := legacy.DecodeTx(txBytes)
if legacyErr != nil {
// Return original error if legacy decoding also fails
return nil, fmt.Errorf("failed to decode tx: %w (legacy decode also failed: %v)", err, legacyErr)
}

msgs := legacyTx.GetMsgs()
if msgIndex >= len(msgs) {
return nil, fmt.Errorf("message index %d out of bounds", msgIndex)
}

if msg, ok := asEthereumTxMsg(msgs[msgIndex]); ok {
return msg, nil
}

return nil, errors.New("not an ethereum tx")
}

// GetTransactionByHash returns the Ethereum format transaction identified by Ethereum transaction hash
func (b *Backend) GetTransactionByHash(txHash common.Hash) (*rpctypes.RPCTransaction, error) {
res, err := b.GetTxByEthHash(txHash)
Expand All @@ -41,17 +108,12 @@ func (b *Backend) GetTransactionByHash(txHash common.Hash) (*rpctypes.RPCTransac
return nil, err
}

tx, err := b.ClientCtx.TxConfig.TxDecoder()(block.Block.Txs[res.TxIndex])
// Decode transaction with fallback to legacy format
msg, err := b.decodeTxWithLegacyFallback(block.Block.Txs[res.TxIndex], int(res.MsgIndex))
if err != nil {
return nil, err
}

// the `res.MsgIndex` is inferred from tx index, should be within the bound.
msg, ok := tx.GetMsgs()[res.MsgIndex].(*evmtypes.MsgEthereumTx)
if !ok {
return nil, errors.New("invalid ethereum tx")
}

blockRes, err := b.RPCClient.BlockResults(b.Ctx, &block.Block.Height)
if err != nil {
b.Logger.Debug("block result not found", "height", block.Block.Height, "error", err.Error())
Expand Down Expand Up @@ -113,7 +175,7 @@ func (b *Backend) GetTransactionByHashPending(txHash common.Hash) (*rpctypes.RPC
continue
}

if msg.Hash() == txHash {
if msg.GetHash() == txHash {
// use zero block values since it's not included in a block yet
return rpctypes.NewTransactionFromMsg(
msg,
Expand Down Expand Up @@ -180,26 +242,23 @@ func (b *Backend) GetTransactionReceipt(hash common.Hash) (map[string]interface{
return nil, fmt.Errorf("block not found at height %d: %w", res.Height, err)
}

tx, err := b.ClientCtx.TxConfig.TxDecoder()(resBlock.Block.Txs[res.TxIndex])
if err != nil {
b.Logger.Debug("decoding failed", "error", err.Error())
return nil, fmt.Errorf("failed to decode tx: %w", err)
}

blockRes, err := b.RPCClient.BlockResults(b.Ctx, &res.Height)
if err != nil {
b.Logger.Debug("failed to retrieve block results", "height", res.Height, "error", err.Error())
return nil, fmt.Errorf("block result not found at height %d: %w", res.Height, err)
}

ethMsg := tx.GetMsgs()[res.MsgIndex].(*evmtypes.MsgEthereumTx)
receipts, err := b.ReceiptsFromCometBlock(resBlock, blockRes, []*evmtypes.MsgEthereumTx{ethMsg})
// Decode transaction with fallback to legacy format
ethMsg, err := b.decodeTxWithLegacyFallback(resBlock.Block.Txs[res.TxIndex], int(res.MsgIndex))
if err != nil {
return nil, fmt.Errorf("failed to get receipts from comet block")
b.Logger.Debug("decoding failed", "error", err.Error())
return nil, fmt.Errorf("failed to decode tx: %w", err)
}

var signer ethtypes.Signer
// Build receipt from the decoded message
ethTx := ethMsg.AsTransaction()

var signer ethtypes.Signer
if ethTx.Protected() {
signer = ethtypes.LatestSignerForChainID(ethTx.ChainId())
} else {
Expand All @@ -210,7 +269,62 @@ func (b *Backend) GetTransactionReceipt(hash common.Hash) (map[string]interface{
return nil, fmt.Errorf("failed to get sender: %w", err)
}

return rpctypes.RPCMarshalReceipt(receipts[0], ethTx, from)
// Build the receipt manually
baseFee, err := b.BaseFee(blockRes)
if err != nil {
b.Logger.Error("failed to fetch Base Fee", "height", res.Height, "error", err)
}

var effectiveGasPrice *big.Int
if baseFee != nil {
effectiveGasPrice = rpctypes.EffectiveGasPrice(ethTx, baseFee)
} else {
effectiveGasPrice = ethTx.GasFeeCap()
}

var status uint64
if res.Failed {
status = ethtypes.ReceiptStatusFailed
} else {
status = ethtypes.ReceiptStatusSuccessful
}

contractAddress := common.Address{}
if ethTx.To() == nil {
contractAddress = crypto.CreateAddress(from, ethTx.Nonce())
}

msgIndex := int(res.MsgIndex)
logs, err := evmtypes.DecodeMsgLogs(
blockRes.TxsResults[res.TxIndex].Data,
msgIndex,
uint64(resBlock.Block.Height),
)
if err != nil {
b.Logger.Debug("failed to parse tx logs", "error", err.Error())
}

bloom := ethtypes.CreateBloom(&ethtypes.Receipt{Logs: logs})

receipt := &ethtypes.Receipt{
Type: ethTx.Type(),
PostState: nil,
Status: status,
CumulativeGasUsed: res.GasUsed, // simplified: just use this tx's gas
Bloom: bloom,
Logs: logs,
TxHash: ethMsg.GetHash(),
ContractAddress: contractAddress,
GasUsed: res.GasUsed,
EffectiveGasPrice: effectiveGasPrice,
BlobGasUsed: uint64(0),
BlobGasPrice: big.NewInt(0),
BlockHash: common.BytesToHash(resBlock.BlockID.Hash),
BlockNumber: big.NewInt(resBlock.Block.Height),
TransactionIndex: uint(res.EthTxIndex),
}

return rpctypes.RPCMarshalReceipt(receipt, ethTx, from)
}

// GetTransactionLogs returns the transaction logs identified by hash.
Expand Down Expand Up @@ -364,21 +478,14 @@ func (b *Backend) GetTransactionByBlockAndIndex(block *cmtrpctypes.ResultBlock,
return nil, nil
}

var msg *evmtypes.MsgEthereumTx
var msg evmtypes.EthereumTxMsg
// find in tx indexer
res, err := b.GetTxByTxIndex(block.Block.Height, uint(idx))
if err == nil {
tx, err := b.ClientCtx.TxConfig.TxDecoder()(block.Block.Txs[res.TxIndex])
// Decode transaction with fallback to legacy format
msg, err = b.decodeTxWithLegacyFallback(block.Block.Txs[res.TxIndex], int(res.MsgIndex))
if err != nil {
b.Logger.Debug("invalid ethereum tx", "height", block.Block.Header, "index", idx)
return nil, nil
}

var ok bool
// msgIndex is inferred from tx events, should be within bound.
msg, ok = tx.GetMsgs()[res.MsgIndex].(*evmtypes.MsgEthereumTx)
if !ok {
b.Logger.Debug("invalid ethereum tx", "height", block.Block.Header, "index", idx)
b.Logger.Debug("invalid ethereum tx", "height", block.Block.Header, "index", idx, "error", err.Error())
return nil, nil
}
} else {
Expand Down
3 changes: 2 additions & 1 deletion rpc/types/utils.go
Original file line number Diff line number Diff line change
Expand Up @@ -154,8 +154,9 @@ func MakeHeader(

// NewTransactionFromMsg returns a transaction that will serialize to the RPC
// representation, with the given location metadata set (if available).
// Accepts EthereumTxMsg interface to support both legacy and new message formats.
func NewTransactionFromMsg(
msg *evmtypes.MsgEthereumTx,
msg evmtypes.EthereumTxMsg,
blockHash common.Hash,
blockNumber, blockTime, index uint64,
baseFee *big.Int,
Expand Down
30 changes: 29 additions & 1 deletion x/vm/types/codec.go
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,8 @@ import (
sdk "github.com/cosmos/cosmos-sdk/types"
"github.com/cosmos/cosmos-sdk/types/msgservice"
"github.com/cosmos/cosmos-sdk/types/tx"

"github.com/cosmos/evm/x/vm/types/legacy"
)

var (
Expand Down Expand Up @@ -34,11 +36,37 @@ func RegisterInterfaces(registry codectypes.InterfaceRegistry) {
registry.RegisterImplementations(
(*tx.TxExtensionOptionI)(nil),
&ExtensionOptionsEthereumTx{},
&legacy.ExtensionOptionsEthereumTx{}, // Legacy extension option
)
registry.RegisterImplementations(
(*sdk.Msg)(nil),
&MsgEthereumTx{},
&MsgUpdateParams{},
&legacy.MsgEthereumTx{}, // Legacy MsgEthereumTx
)

// Register TxData implementations for unpacking legacy Any field
// These are registered under multiple namespaces for backward compatibility
registry.RegisterInterface(
"ethermint.evm.v1.TxData",
(*legacy.TxData)(nil),
&legacy.DynamicFeeTx{},
&legacy.AccessListTx{},
&legacy.LegacyTx{},
)
registry.RegisterInterface(
"os.evm.v1.TxData",
(*legacy.TxData)(nil),
&legacy.DynamicFeeTx{},
&legacy.AccessListTx{},
&legacy.LegacyTx{},
)
registry.RegisterInterface(
"cosmos.evm.vm.v1.TxData",
(*legacy.TxData)(nil),
&legacy.DynamicFeeTx{},
&legacy.AccessListTx{},
&legacy.LegacyTx{},
)

msgservice.RegisterMsgServiceDesc(registry, &_Msg_serviceDesc)
Expand All @@ -47,4 +75,4 @@ func RegisterInterfaces(registry codectypes.InterfaceRegistry) {
// RegisterLegacyAminoCodec required for EIP-712
func RegisterLegacyAminoCodec(cdc *codec.LegacyAmino) {
cdc.RegisterConcrete(&MsgUpdateParams{}, updateParamsName, nil)
}
}
45 changes: 45 additions & 0 deletions x/vm/types/interfaces.go
Original file line number Diff line number Diff line change
Expand Up @@ -93,3 +93,48 @@ type BankWrapper interface {
type ConsensusParamsKeeper interface {
Params(context.Context, *types.QueryParamsRequest) (*types.QueryParamsResponse, error)
}

// EthereumTxMsg is the common interface for both legacy and new MsgEthereumTx.
// This allows the codebase to handle both transaction formats uniformly.
type EthereumTxMsg interface {
sdk.Msg

// AsTransaction converts the message to a go-ethereum Transaction
AsTransaction() *ethtypes.Transaction

// GetFrom returns the sender address
GetFrom() sdk.AccAddress

// GetSenderLegacy returns the sender, falling back to signature recovery if needed
GetSenderLegacy(signer ethtypes.Signer) (common.Address, error)

// GetGas returns the gas limit
GetGas() uint64

// GetGasPrice returns the big.Int gas price
GetGasPrice() *big.Int

// GetGasFeeCap returns the max fee per gas (EIP-1559) or gas price (legacy)
GetGasFeeCap() *big.Int

// GetGasTipCap returns the max priority fee (EIP-1559) or gas price (legacy)
GetGasTipCap() *big.Int

// GetValue returns the transaction value
GetValue() *big.Int

// GetNonce returns the transaction nonce
GetNonce() uint64

// GetTo returns the recipient address (nil for contract creation)
GetTo() *common.Address

// GetInputData returns the transaction input data
GetInputData() []byte

// GetHash returns the transaction hash
GetHash() common.Hash

// IsLegacy returns true if this is a legacy format message
IsLegacy() bool
}
Loading
Loading