Skip to content
Open
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
102 changes: 102 additions & 0 deletions indexer/kv_indexer.go
Original file line number Diff line number Diff line change
Expand Up @@ -25,6 +25,10 @@ import (
const (
KeyPrefixTxHash = 1
KeyPrefixTxIndex = 2
// KeyPrefixTxDerived marks a tx hash as a derived EVM tx (event-only, no embedded
// MsgEthereumTx). Lets the RPC backend cheaply decide whether a KV-indexer hit needs
// its TxResultAdditionalFields rebuilt from events, without parsing every lookup.
KeyPrefixTxDerived = 3

// TxIndexKeyLength is the length of tx-index key
TxIndexKeyLength = 1 + 8 + 8
Expand Down Expand Up @@ -70,6 +74,15 @@ func (kv *KVIndexer) IndexBlock(block *cmttypes.Block, txResults []*abci.ExecTxR
}

if !isEthTx(tx) {
// Derived txs (internal EVM executions emitted only as events, not as an
// embedded MsgEthereumTx) still occupy slots in the block's eth-tx index
// sequence. Index them by hash and by (height, ethTxIndex), advancing
// the shared ethTxIndex so it stays aligned with the emitted txIndex for any
// standard eth txs that follow in the same block.
ethTxIndex, err = kv.indexDerivedTxs(batch, height, uint32(txIndex), result, ethTxIndex) //#nosec G115
if err != nil {
kv.logger.Error("Fail to index derived txs", "err", err, "block", height, "txIndex", txIndex)
}
continue
}

Expand Down Expand Up @@ -161,11 +174,100 @@ func (kv *KVIndexer) GetByBlockAndIndex(blockNumber int64, txIndex int32) (*serv
return kv.GetByTxHash(common.BytesToHash(bz))
}

// IsDerivedTx reports whether the hash was indexed as a derived EVM tx (event-only, no
// embedded MsgEthereumTx). A cheap single key read used by the RPC backend to gate the
// (more expensive) rebuild of TxResultAdditionalFields from block events.
func (kv *KVIndexer) IsDerivedTx(hash common.Hash) (bool, error) {
bz, err := kv.db.Get(DerivedTxHashKey(hash))
if err != nil {
return false, errorsmod.Wrapf(err, "IsDerivedTx %s", hash.Hex())
}
return len(bz) > 0, nil
}

// IsDerivedTxByBlockAndIndex reports whether the tx at (blockNumber, eth tx index) is a
// derived EVM tx. It resolves the hash from the block-index entry and consults the derived
// marker — two cheap key reads — so the RPC backend can gate derived-tx reconstruction on
// the block-index path without reparsing block events for ordinary txs.
func (kv *KVIndexer) IsDerivedTxByBlockAndIndex(blockNumber int64, txIndex int32) (bool, error) {
bz, err := kv.db.Get(TxIndexKey(blockNumber, txIndex))
if err != nil {
return false, errorsmod.Wrapf(err, "IsDerivedTxByBlockAndIndex %d %d", blockNumber, txIndex)
}
if len(bz) == 0 {
return false, nil
}
return kv.IsDerivedTx(common.BytesToHash(bz))
}

// indexDerivedTxs indexes the derived EVM transactions carried as events inside a
// non-eth Cosmos tx (e.g. a Universal Executor MsgExecutePayload). Each derived tx is
// stored by hash and by (height, ethTxIndex) using the same schema as standard
// MsgEthereumTx entries, so eth_getTransactionByHash / Receipt and block-and-index
// lookups resolve it directly without depending on a CometBFT tx_search fallback.
//
// ethTxIndex is the shared, block-level eth tx counter; it is advanced once per derived
// tx and returned so the caller's sequence stays aligned with the txIndex the keeper
// emits for both derived and standard txs.
func (kv *KVIndexer) indexDerivedTxs(
batch dbm.Batch,
height int64,
txIndex uint32,
result *abci.ExecTxResult,
ethTxIndex int32,
) (int32, error) {
// nil tx: ParseTxResult only needs it for the failed-cosmos-tx gas fallback, which
// assumes embedded MsgEthereumTx messages and does not apply to derived txs.
txs, err := rpctypes.ParseTxResult(result, nil)
if err != nil {
return ethTxIndex, err
}

var cumulativeGasUsed uint64
for _, parsed := range txs.Txs {
if parsed.Type != evmtypes.DerivedTxType {
continue
}

if parsed.EthTxIndex >= 0 && parsed.EthTxIndex != ethTxIndex {
kv.logger.Error("derived eth tx index doesn't match", "expect", ethTxIndex, "found", parsed.EthTxIndex)
}

cumulativeGasUsed += parsed.GasUsed
txResult := servertypes.TxResult{
Height: height,
TxIndex: txIndex,
MsgIndex: uint32(parsed.MsgIndex), //#nosec G115 -- int overflow is not a concern here
EthTxIndex: ethTxIndex,
Failed: parsed.Failed,
GasUsed: parsed.GasUsed,
CumulativeGasUsed: cumulativeGasUsed,
}
ethTxIndex++

if err := saveTxResult(kv.clientCtx.Codec, batch, parsed.Hash, &txResult); err != nil {
return ethTxIndex, errorsmod.Wrapf(err, "indexDerivedTxs %d", height)
}
// Mark the hash as derived in the same batch, so it commits atomically with the
// tx-hash/tx-index entries: a derived tx is findable iff its marker exists.
if err := batch.Set(DerivedTxHashKey(parsed.Hash), []byte{1}); err != nil {
return ethTxIndex, errorsmod.Wrapf(err, "indexDerivedTxs %d set derived key", height)
}
}

return ethTxIndex, nil
}

// TxHashKey returns the key for db entry: `tx hash -> tx result struct`
func TxHashKey(hash common.Hash) []byte {
return append([]byte{KeyPrefixTxHash}, hash.Bytes()...)
}

// DerivedTxHashKey returns the key for db entry: `tx hash -> derived marker`
func DerivedTxHashKey(hash common.Hash) []byte {
return append([]byte{KeyPrefixTxDerived}, hash.Bytes()...)
}

// TxIndexKey returns the key for db entry: `(block number, tx index) -> tx hash`
func TxIndexKey(blockNumber int64, txIndex int32) []byte {
bz1 := sdk.Uint64ToBigEndian(uint64(blockNumber)) //nolint:gosec // G115 // block number won't exceed uint64
Expand Down
4 changes: 2 additions & 2 deletions rpc/backend/blocks.go
Original file line number Diff line number Diff line change
Expand Up @@ -197,9 +197,9 @@ func (b *Backend) GetBlockReceipts(
return nil, fmt.Errorf("block result not found for height %d", resBlock.Block.Height)
}

msgs, _ := b.EthMsgsFromCometBlock(resBlock, blockRes)
msgs, txsAdditional := b.EthMsgsFromCometBlock(resBlock, blockRes)

receipts, err := b.ReceiptsFromCometBlock(resBlock, blockRes, msgs)
receipts, err := b.ReceiptsFromCometBlock(resBlock, blockRes, msgs, txsAdditional)
if err != nil {
return nil, fmt.Errorf("failed to get receipts from comet block: %w, ", err)

Expand Down
90 changes: 78 additions & 12 deletions rpc/backend/comet_to_eth.go
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
package backend

import (
"encoding/json"
"fmt"
"math/big"

Expand Down Expand Up @@ -256,7 +257,7 @@ func (b *Backend) EthBlockFromCometBlock(
}

// 7. receipts
receipts, err := b.ReceiptsFromCometBlock(resBlock, blockRes, msgs)
receipts, err := b.ReceiptsFromCometBlock(resBlock, blockRes, msgs, additionals)
if err != nil {
return nil, fmt.Errorf("failed to get receipts from comet block: %w", err)
}
Expand Down Expand Up @@ -310,24 +311,67 @@ func (b *Backend) MinerFromCometBlock(
return common.BytesToAddress(validatorAccAddr), nil
}

// derivedTxLogsFromEvents finds EVM logs for a derived tx by scanning tx_log events and
// matching each log's TxHash to the given hash. Returns nil, nil when no matching logs are
// found — valid for a successful derived tx that emits no EVM events.
func derivedTxLogsFromEvents(events []abci.Event, txHash common.Hash, blockNumber uint64) ([]*ethtypes.Log, error) {
var result []*ethtypes.Log
for _, event := range events {
if event.Type != evmtypes.EventTypeTxLog {
continue
}
for _, attr := range event.Attributes {
if attr.Key != evmtypes.AttributeKeyTxLog {
continue
}
var log evmtypes.Log
if err := json.Unmarshal([]byte(attr.Value), &log); err != nil {
return nil, err
}
if common.HexToHash(log.TxHash) != txHash {
continue
}
l := log.ToEthereum()
l.BlockNumber = blockNumber
result = append(result, l)
}
}
return result, nil
}

func (b *Backend) ReceiptsFromCometBlock(
resBlock *cmtrpctypes.ResultBlock,
blockRes *cmtrpctypes.ResultBlockResults,
msgs []*evmtypes.MsgEthereumTx,
additionals []*rpctypes.TxResultAdditionalFields,
) ([]*ethtypes.Receipt, error) {
baseFee, err := b.BaseFee(blockRes)
if err != nil {
// handle the error for pruned node.
b.Logger.Error("failed to fetch Base Fee from prunned block. Check node prunning configuration", "height", resBlock.Block.Height, "error", err)
}

blockHeight := uint64(resBlock.Block.Height) // #nosec G115
blockHash := common.BytesToHash(resBlock.BlockID.Hash)
receipts := make([]*ethtypes.Receipt, len(msgs))
cumulatedGasUsed := uint64(0)
for i, ethMsg := range msgs {
txResult, _, err := b.GetTxByEthHash(ethMsg.Hash())
var additional *rpctypes.TxResultAdditionalFields
if additionals != nil && i < len(additionals) {
additional = additionals[i]
}

// Derived txs must be looked up by the event hash (additional.Hash); native txs use ethMsg.Hash().
var lookupHash common.Hash
if additional != nil {
lookupHash = additional.Hash
} else {
lookupHash = ethMsg.Hash()
}

txResult, _, err := b.GetTxByEthHash(lookupHash)
if err != nil {
return nil, fmt.Errorf("tx not found: hash=%s, error=%s", ethMsg.Hash(), err.Error())
return nil, fmt.Errorf("tx not found: hash=%s, error=%s", lookupHash, err.Error())
}

cumulatedGasUsed += txResult.GasUsed
Expand All @@ -351,18 +395,40 @@ func (b *Backend) ReceiptsFromCometBlock(
contractAddress = crypto.CreateAddress(ethMsg.GetSender(), ethMsg.Raw.Nonce())
}

msgIndex := int(txResult.MsgIndex) // #nosec G115 -- checked for int overflow already
logs, err := evmtypes.DecodeMsgLogs(
blockRes.TxsResults[txResult.TxIndex].Data,
msgIndex,
uint64(resBlock.Block.Height), // #nosec G115 -- checked for int overflow already
)
if err != nil {
return nil, fmt.Errorf("failed to convert tx result to eth receipt: %w", err)
var logs []*ethtypes.Log
if additional != nil {
// Derived tx: no MsgEthereumTxResponse in the Cosmos tx Data field.
// Parse logs from tx_log ABCI events by matching TxHash instead.
logs, err = derivedTxLogsFromEvents(
blockRes.TxsResults[txResult.TxIndex].Events,
additional.Hash,
blockHeight,
)
if err != nil {
return nil, fmt.Errorf("failed to parse derived tx logs: %w", err)
}
} else {
msgIndex := int(txResult.MsgIndex) // #nosec G115 -- checked for int overflow already
logs, err = evmtypes.DecodeMsgLogs(
blockRes.TxsResults[txResult.TxIndex].Data,
msgIndex,
blockHeight,
)
if err != nil {
return nil, fmt.Errorf("failed to convert tx result to eth receipt: %w", err)
}
}

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

// Derived txs use the event hash as the canonical TxHash in the receipt.
var txHash common.Hash
if additional != nil {
txHash = additional.Hash
} else {
txHash = ethMsg.Hash()
}

receipt := &ethtypes.Receipt{
// Consensus fields: These fields are defined by the Yellow Paper
Type: ethMsg.Raw.Type(),
Expand All @@ -373,7 +439,7 @@ func (b *Backend) ReceiptsFromCometBlock(
Logs: logs,

// Implementation fields: These fields are added by geth when processing a transaction.
TxHash: ethMsg.Hash(),
TxHash: txHash,
ContractAddress: contractAddress,
GasUsed: txResult.GasUsed,
EffectiveGasPrice: effectiveGasPrice,
Expand Down
Loading
Loading