Skip to content

[F-2026-17754] TraceTransaction mixes Cosmos and Ethereum index domains #27

Open
AryaLanjewar3005 wants to merge 3 commits into
audit/evm-mergefrom
audit/evm-merge-trace
Open

[F-2026-17754] TraceTransaction mixes Cosmos and Ethereum index domains #27
AryaLanjewar3005 wants to merge 3 commits into
audit/evm-mergefrom
audit/evm-merge-trace

Conversation

@AryaLanjewar3005

Copy link
Copy Markdown
Collaborator

Description

fix(rpc): TraceTransaction predecessor assembly uses Ethereum tx index (F-2026-17754)

Summary

debug_traceTransaction contained six distinct bugs in its predecessor-assembly loop, all stemming from the same root cause: the Cosmos transaction slot index (TxIndex) and the Ethereum execution counter (EthTxIndex) were used interchangeably, even though they diverge whenever a Cosmos tx holds multiple EVM messages, holds no EVM messages, or derived txs shift the Ethereum counter.

This finding was originally patched in the audit-fixes branch (0.2.0). This PR ports and adapts that fix to the 0.5.0 codebase (audit/evm-merge-inconsistent-logs), resolving all six bugs with appropriate architectural adaptations, and adds a comprehensive unit and integration test suite that did not exist in the original patch.


Root Cause

TxIndex is the ordinal position of a Cosmos tx envelope in block.Txs[]. EthTxIndex is the monotonically-increasing Ethereum execution counter that increments once per EVM execution (including derived txs) across the block. These counters are not interchangeable.

The original code used transaction.TxIndex as the outer predecessor-loop bound while GetTxByTxIndex resolved by Ethereum index. They only coincide when every Cosmos tx holds exactly one MsgEthereumTx and no derived txs exist.


Six Bugs Fixed

Bug 1 — Loop bound used wrong index domain

// Before
for i := 0; i < int(transaction.TxIndex); i++ {

// After
ethTxCount := int(transaction.EthTxIndex)
if ethTxCount < 0 { ethTxCount = 0 }
for i := 0; i < ethTxCount; i++ {

Bug 2 — Same-Cosmos-tx entries were double-counted

When the target is a later message in a multi-message Cosmos tx, the outer loop would fetch the same Cosmos slot and add early messages that the after-loop also adds. A guard now skips same-slot entries:

if int(predecessorTx.TxIndex) == int(transaction.TxIndex) {
    continue
}

Bug 3 — Derived tx event-scan loop was doubly wrong

The old approach scanned parsedTxs.Txs for derived txs before txAdditional.Hash. It both (a) skipped the tx at txAdditional.Hash itself — so the last derived tx in a chain was always missed — and (b) double-counted earlier derived txs already added by their own outer-loop iterations. Each outer-loop iteration corresponds to exactly one Ethereum execution, so the direct add is correct and complete:

if txAdditional != nil {
    ethMsg := b.parseDerivedTxFromAdditionalFields(txAdditional)
    if ethMsg != nil {
        predecessors = append(predecessors, ethMsg)
    }
    continue
}

Bug 4 — Normal tx decoded from wrong Cosmos slot

blk.Block.Txs[i] used the Ethereum index to index the block's Cosmos tx array. When i (Ethereum index) and predecessorTx.TxIndex (Cosmos slot) differ, the wrong raw bytes were decoded:

// Before
tx, err := b.ClientCtx.TxConfig.TxDecoder()(blk.Block.Txs[i])

// After
tx, err := b.ClientCtx.TxConfig.TxDecoder()(blk.Block.Txs[predecessorTx.TxIndex])

Bug 5 — Inner loop excluded message AT MsgIndex

for j := 0; j < MsgIndex; j++ added messages before MsgIndex but never the message at MsgIndex, silently dropping the last EVM message of any multi-message predecessor Cosmos tx from the predecessor set:

// Before
for j := 0; j < int(predecessorTx.MsgIndex); j++ {
    if ethMsg, ok := tx.GetMsgs()[j].(*evmtypes.MsgEthereumTx); ok {
        predecessors = append(predecessors, ethMsg)
    }
}

// After
if ethMsg, ok := tx.GetMsgs()[int(predecessorTx.MsgIndex)].(*evmtypes.MsgEthereumTx); ok {
    predecessors = append(predecessors, ethMsg)
}

Bug 6 — Missing nil guard on blockRes

blockRes could be nil when BlockResults succeeds but returns nil; added blockRes != nil && guard before indexing into blockRes.TxsResults.


Tests

New unit tests (rpc/backend/tracing_test.go) — 11 test functions

Test What it validates
TestTraceTransaction 4 basic cases: tx not found, block not found, single-tx block, multi-tx block
TestTraceTransactionEthTxIndex Second tx in a 2-tx block uses EthTxIndex=1 → one predecessor
TestTraceTransactionMultiMsgSameCosmosTarget 2-msg Cosmos tx, target is second → same-slot guard fires, one predecessor from after-loop
TestTraceTransactionMultiMsgTargetIsThird 3-msg Cosmos tx, target is third → two predecessors from after-loop
TestTraceTransactionMultiMsgCosmosAsPredecessor 2-msg Cosmos tx precedes target → both messages appear in predecessors (validates Bug 5 fix)
TestTraceTransactionThreeTxBlock slot0=1msg, slot1=2msg, slot2=target → all three preceding messages appear
TestTraceTransactionDerivedTxAsPredecessor Derived tx precedes EVM target → reconstructed synthetic msg appears
TestTraceTransactionDerivedTxAsTarget Derived tx is itself the trace target → only preceding EVM msg appears

Integration test fixes (tests/integration/rpc/backend/test_tracing.go)

  • "fail - tx not found": registered RegisterTxSearchEmpty mock — after the KV miss on an empty block, the code correctly falls through to CometBFT TxSearch before returning the error.
  • "pass - transaction found in a block with multiple transactions": changed RegisterTraceTransactionWithPredecessors to RegisterTraceTransaction — the target has EthTxIndex=0 so zero predecessors is the correct expectation.

What Was Different from the 0.2.0 Application

The original patch was written against Ethermint/Evmos 0.2.0 (audit-fixes branch). Applying the same fix to 0.5.0 required the following adaptations:

1. Exported vs Unexported Backend Fields

All Backend fields are exported in 0.5.0. Every field reference was updated:

0.2.0 0.5.0
b.rpcClient b.RPCClient
b.ctx b.Ctx
b.clientCtx b.ClientCtx
b.indexer b.Indexer
b.logger b.Logger
b.chainID b.EvmChainID
b.queryClient b.QueryClient

2. MsgEthereumTx.From Type Change

From changed from string in 0.2.0 to []byte in 0.5.0. All test helpers updated accordingly (msg.From = from.Bytes() not from.String(), multi-msg reset uses nil not "").

3. Test Infrastructure Required Complete Rebuild

The original 0.2.0 patch included a test file, but 0.5.0 had no BackendTestSuite or mock helper infrastructure in rpc/backend/. Three new files were created from scratch:

  • rpc/backend/backend_suite_test.go: The setupMockBackend helper (from tx_info_test.go) was reused rather than duplicating manual backend construction from 0.2.0. All field references updated for exported names. A TestMain was added (not present in 0.2.0) because GetEthChainConfig() panics at NewBackend without prior chain config initialization — this was not an issue in the 0.2.0 test setup.

  • rpc/backend/client_test.go: Mock helpers adapted from 0.2.0. The ChainID variable changed from a ChainIDConfig struct to a plain string. The RegisterTraceTransactionWithPredecessors function uses mock.Anything for the TraceTx request argument instead of mock.MatchedBy with a custom proto comparison function — the 0.5.0 MsgEthereumTx.Hash field is a func type which cannot be compared with !=, causing a compile error in the custom matcher.

  • rpc/backend/tracing_test.go: 11 unit test functions ported and adapted from 0.2.0.

4. Pre-existing Bug Fixed: encoding/config.go Missing EVM Type Registration

Discovered during test development: encoding.MakeConfig() did not call evmtypes.RegisterInterfaces(interfaceRegistry), so the TxDecoder could not resolve the /cosmos.evm.vm.v1.MsgEthereumTx proto type URL. This caused the KV indexer to silently skip every EVM tx during IndexBlock. This was a pre-existing 0.5.0 bug (not present in 0.2.0 which used a different encoding setup). Fixed by adding one line to MakeConfig:

enccodec.RegisterInterfaces(interfaceRegistry)
evmtypes.RegisterInterfaces(interfaceRegistry)  // added
eip712.SetEncodingConfig(cdc, interfaceRegistry, evmChainID)

5. Integration Test Corrections (Two Pre-existing Bugs Exposed)

The integration test at tests/integration/rpc/backend/test_tracing.go had two bugs that were only exposed once the chain config initialization and KV indexer registration issues were fixed:

  • "fail - tx not found": The registerMock was func() {} (empty). After our fix, a KV miss on an empty block now correctly falls through to CometBFT TxSearch. Without a registered TxSearch mock, testify panics. Added RegisterTxSearchEmpty(client, query).

  • "pass - transaction found in a block with multiple transactions": The mock expected msgEthereumTx to be its own predecessor (RegisterTraceTransactionWithPredecessors(..., []*evmtypes.MsgEthereumTx{msgEthereumTx})). However, the traced tx is at EthTxIndex=0 — it is the first tx in the block and has zero predecessors. Changed to RegisterTraceTransaction(QueryClient, msgEthereumTx). This was a pre-existing wrong expectation in the test.


Files Changed

File Change
rpc/backend/tracing.go 6 bug fixes in predecessor assembly
encoding/config.go Added evmtypes.RegisterInterfaces (pre-existing bug fix)
rpc/backend/backend_suite_test.go NEW — BackendTestSuite + TestMain
rpc/backend/client_test.go NEW — Mock registration helpers
rpc/backend/tracing_test.go NEW — 11 unit tests
tests/integration/rpc/backend/test_tracing.go TxSearch mock fix + predecessor expectation fix

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 Audit/evm merge trace [F-2026-17754] TraceTransaction mixes Cosmos and Ethereum index domains 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