Skip to content

[F-2026-17740][Re-eval] KV indexer path misses derived transactions#25

Open
AryaLanjewar3005 wants to merge 2 commits into
audit/evm-mergefrom
audit/evm-merge-kv-index
Open

[F-2026-17740][Re-eval] KV indexer path misses derived transactions#25
AryaLanjewar3005 wants to merge 2 commits into
audit/evm-mergefrom
audit/evm-merge-kv-index

Conversation

@AryaLanjewar3005

Copy link
Copy Markdown
Collaborator

Description

KV Indexer: Derived Tx Support — Design and Port Notes

Background

Finding: F-2026-17740 — KV indexer path misses derived transactions

The KV indexer (indexer/kv_indexer.go) skipped all non-eth Cosmos transactions during IndexBlock. Derived EVM txs — internal EVM executions triggered by Cosmos messages (e.g., IBC transfers, Universal Executor payloads) that emit ethereum_tx and tx_log ABCI events but carry no embedded MsgEthereumTx — were therefore invisible to the indexer.

When a client queried eth_getTransactionByHash or eth_getTransactionReceipt for a derived tx hash while the KV indexer was active, the lookup returned an error immediately instead of falling through to the CometBFT tx_search path. The derived tx was unreachable via the JSON-RPC API even though it existed on chain.

The original fix was authored on the audit-fixes branch (targeting the 0.2.0 architecture). This document describes how the fix works and what changed when porting it to the 0.5.0 architecture on audit/evm-merge-kv-index.


How the Fix Works

1. KV indexer: index derived txs during IndexBlock

IndexBlock iterates every Cosmos tx in a block. Previously, non-eth txs hit continue immediately. The fix inserts a call to indexDerivedTxs before that continue:

if !isEthTx(tx) {
    ethTxIndex, err = kv.indexDerivedTxs(batch, height, uint32(txIndex), result, ethTxIndex)
    if err != nil {
        kv.logger.Error("Fail to index derived txs", ...)
    }
    continue
}

indexDerivedTxs calls rpctypes.ParseTxResult(result, nil) (passing nil for the SDK tx because there is no embedded MsgEthereumTx to decode) and iterates the parsed entries looking for DerivedTxType. For each one:

  • A TxResult is written via saveTxResult (same schema as native txs: hash key + block-index key).
  • A separate marker key DerivedTxHashKey(hash) is written in the same batch, committing atomically.
  • The shared ethTxIndex counter is incremented once per derived tx — keeping it aligned with the counter the keeper emits for both derived and standard txs in the same block (fixed by F-2026-17745).

2. New marker key and IsDerivedTx / IsDerivedTxByBlockAndIndex

A third key prefix is added alongside KeyPrefixTxHash = 1 and KeyPrefixTxIndex = 2:

KeyPrefixTxDerived = 3

DerivedTxHashKey(hash) returns []byte{3} + hash.Bytes(). The value is a single sentinel byte {1}.

Two new interface methods gate the (more expensive) event-reparse behind a single cheap key read:

  • IsDerivedTx(hash) — checks DerivedTxHashKey(hash) directly.
  • IsDerivedTxByBlockAndIndex(height, txIndex) — reads the block-index entry to get the hash, then delegates to IsDerivedTx. Two key reads total.

3. RPC backend: rebuild TxResultAdditionalFields on KV hit

The KV indexer stores only TxResult (height, txIndex, msgIndex, ethTxIndex, gasUsed, failed). For standard txs this is enough — callers decode the MsgEthereumTx directly from the block. For derived txs there is no MsgEthereumTx to decode; the RPC backend panics trying to cast.

derivedTxAdditionalFields(hash, res) solves this:

  1. Calls IsDerivedTx(hash) — returns (nil, nil) cheaply for standard txs.
  2. If derived, delegates to buildDerivedAdditional(res), which:
    • Fetches BlockResults for res.Height.
    • Calls ParseTxResult on the relevant TxsResults[res.TxIndex] entry.
    • Retrieves the parsed tx by res.MsgIndex (the positional index stored during indexing).
    • Constructs a full TxResultAdditionalFields from the parsed event fields.

GetTxByEthHash and GetTxByEthHashAndMsgIndex call derivedTxAdditionalFields on every KV hit and return the result alongside the TxResult. GetTxByTxIndex uses IsDerivedTxByBlockAndIndex then buildDerivedAdditional on the block-index path, so trace predecessors are reconstructed correctly.

4. KV miss: fall through to CometBFT tx_search

Before the fix, a KV indexer error on lookup immediately propagated to the caller. After the fix, err != nil on a KV lookup is treated as a miss and falls through to the CometBFT tx_search path. This preserves correctness for blocks indexed before the fix was deployed — derived txs in those blocks have no KV entries but are still reachable via event-based search.


Differences Between 0.2.0 (audit-fixes) and 0.5.0 (audit/evm-merge-kv-index)

Type and package renames

Concern 0.2.0 (audit-fixes) 0.5.0 (audit/evm-merge-kv-index)
TxResult type cosmosevmtypes.TxResult (from github.com/cosmos/evm/types) servertypes.TxResult (from github.com/cosmos/evm/server/types)
EVMTxIndexer interface file types/indexer.go server/types/indexer.go
Interface compile-guard var _ cosmosevmtypes.EVMTxIndexer = &KVIndexer{} var _ servertypes.EVMTxIndexer = &KVIndexer{}

In 0.5.0 the monolithic types/ package was split and server-specific types (including TxResult and EVMTxIndexer) moved under server/types/. The indexer imports and struct constructors were updated accordingly.

Backend receiver field names

The Backend struct in 0.2.0 uses unexported fields; 0.5.0 promotes them to exported:

Field 0.2.0 0.5.0
RPC client b.rpcClient b.RPCClient
Context b.ctx b.Ctx
KV indexer b.indexer b.Indexer

All three appear in buildDerivedAdditional and the GetTxBy* lookup functions. Every reference was updated.

buildDerivedAdditional factored out as a separate function

In 0.2.0, derivedTxAdditionalFields contained both the IsDerivedTx marker check and the full BlockResults + ParseTxResult reconstruction logic in a single function.

In 0.5.0, the reconstruction logic was extracted into buildDerivedAdditional. derivedTxAdditionalFields now only does the IsDerivedTx gate and delegates to buildDerivedAdditional. This is required because GetTxByTxIndex uses IsDerivedTxByBlockAndIndex (not IsDerivedTx) and needs to call the reconstruction step directly without re-doing the marker check.

0.2.0:
  derivedTxAdditionalFields(hash, res)
    → IsDerivedTx check + full reconstruction

0.5.0:
  derivedTxAdditionalFields(hash, res)   ← used by GetTxByEthHash paths
    → IsDerivedTx check
    → buildDerivedAdditional(res)

  buildDerivedAdditional(res)            ← used directly by GetTxByTxIndex
    → BlockResults + ParseTxResult + field mapping

IsDerivedTxByBlockAndIndex added to the interface

The 0.2.0 commit (970f1c1b / cf0237af) adds only IsDerivedTx to the EVMTxIndexer interface. The IsDerivedTxByBlockAndIndex method (used by GetTxByTxIndex) was added in a subsequent commit in the same PR chain and squashed into the final merge commit 4b0cc66e.

In 0.5.0, both methods land together in server/types/indexer.go from the start, reflecting the complete interface contract.

CometBFT tx_search fallback — already present in the original

Both the 0.2.0 commit and the 0.5.0 port treat a KV indexer miss as fall-through to tx_search. The comment changed from // indexer miss — fall through to event-query (tx_search) reconstruction to // Indexer miss — fall through to CometBFT tx_search for derived tx reconstruction. The behavior is identical; the 0.5.0 version uses "CometBFT" consistently (Tendermint was renamed).

Method name: queryTendermintTxIndexerQueryCometTxIndexer

The CometBFT fallback in GetTxByEthHash calls b.queryTendermintTxIndexer in 0.2.0 and b.QueryCometTxIndexer in 0.5.0. Same function, renamed as part of the broader Tendermint → CometBFT migration in 0.5.0.

MockIndexer in tests

0.5.0 unit tests (tx_info_test.go) use a MockIndexer struct. Adding the two new interface methods required two stub implementations:

func (m *MockIndexer) IsDerivedTx(hash common.Hash) (bool, error) { return false, nil }
func (m *MockIndexer) IsDerivedTxByBlockAndIndex(blockNumber int64, txIndex int32) (bool, error) {
    return false, nil
}

These are no-ops (always returning false) because the mock is used for standard-tx test paths only.


Summary of All Changed Files

File Change
indexer/kv_indexer.go KeyPrefixTxDerived constant; indexDerivedTxs; IsDerivedTx; IsDerivedTxByBlockAndIndex; DerivedTxHashKey; IndexBlock wired to call indexDerivedTxs
server/types/indexer.go IsDerivedTx and IsDerivedTxByBlockAndIndex added to EVMTxIndexer interface
rpc/backend/tx_info.go derivedTxAdditionalFields; buildDerivedAdditional; GetTxByEthHash KV hit path; GetTxByEthHashAndMsgIndex KV hit path; GetTxByTxIndex derived-reconstruction branch; KV miss fall-through on all three
rpc/backend/tx_info_test.go IsDerivedTx and IsDerivedTxByBlockAndIndex stubs added to MockIndexer

Closes: #XXXX


Author Checklist

All items are required. Please add a note to the item if the item is not applicable and
please add links to any relevant follow up issues.

I have...

  • tackled an existing issue or discussed with a team member
  • left instructions on how to review the changes
  • targeted the main branch

@github-actions github-actions Bot added the tests label Jun 12, 2026
@AryaLanjewar3005 AryaLanjewar3005 changed the title [F-2026-17740] KV indexer path misses derived transactions [F-2026-17740][Re-eval] KV indexer path misses derived transactions Jun 12, 2026
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant