Skip to content
17 changes: 12 additions & 5 deletions rpc/backend/tx_info.go
Original file line number Diff line number Diff line change
Expand Up @@ -72,7 +72,7 @@
if i > math.MaxInt32 {
return nil, errors.New("tx index overflow")
}
res.EthTxIndex = int32(i)

Check failure on line 75 in rpc/backend/tx_info.go

View workflow job for this annotation

GitHub Actions / Run golangci-lint

G115: integer overflow conversion int -> int32 (gosec)
break
}
}
Expand Down Expand Up @@ -239,11 +239,18 @@
return nil, errors.New("failed to parse receipt")
}

// parse tx logs from events
msgIndex := int(res.MsgIndex) // #nosec G115 -- checked for int overflow already
logs, err := TxLogsFromEvents(blockRes.TxsResults[res.TxIndex].Events, msgIndex)
if err != nil {
b.logger.Debug("failed to parse logs", "hash", hexTx, "error", err.Error())
// Failed transactions yield no logs — consistent with GetTransactionLogs and with
// Ethereum receipt semantics (a reverted tx exposes an empty logs array). Gating
// here also makes the receipt independent of whatever (empty) tx_log event a failed
// derived tx emitted.
var logs []*ethtypes.Log
if !res.Failed {
msgIndex := int(res.MsgIndex) // #nosec G115 -- checked for int overflow already
var err error
logs, err = TxLogsFromEvents(blockRes.TxsResults[res.TxIndex].Events, msgIndex)
if err != nil {
b.logger.Debug("failed to parse logs", "hash", hexTx, "error", err.Error())
}
}

if res.EthTxIndex == -1 {
Expand Down
97 changes: 97 additions & 0 deletions rpc/backend/tx_info_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@ import (

"github.com/ethereum/go-ethereum/common"
"github.com/ethereum/go-ethereum/common/hexutil"
ethtypes "github.com/ethereum/go-ethereum/core/types"
"google.golang.org/grpc/metadata"

abci "github.com/cometbft/cometbft/abci/types"
Expand Down Expand Up @@ -672,3 +673,99 @@ func (suite *BackendTestSuite) TestGetGasUsed() {
})
}
}

// TestFailedTxLogsConsistency verifies that both GetTransactionLogs and
// GetTransactionReceipt return empty logs for a failed EVM transaction, even
// when ghost EventTypeTxLog events exist in the block results. This exercises
// the fix where GetTransactionReceipt now gates TxLogsFromEvents on !res.Failed.
func (suite *BackendTestSuite) TestFailedTxLogsConsistency() {
msgEthereumTx, _ := suite.buildEthereumTx()
// signAndEncodeEthTx signs msgEthereumTx in-place; compute the hash after signing
// so it matches what the KV indexer stores (the signed-tx hash).
txBz := suite.signAndEncodeEthTx(msgEthereumTx)
txHash := msgEthereumTx.AsTransaction().Hash()
block := types.MakeBlock(1, []types.Tx{txBz}, nil, nil)

// Code=0 (Cosmos tx succeeded) but EVM reverted — simulates the inbound-handler
// scenario where EVM errors are swallowed. AttributeKeyEthereumTxFailed marks
// the EVM execution as failed so the indexer stores Failed=true.
revertedBlockResult := []*abci.ExecTxResult{{
Code: 0,
GasUsed: 21000,
Events: []abci.Event{{
Type: evmtypes.EventTypeEthereumTx,
Attributes: []abci.EventAttribute{
{Key: evmtypes.AttributeKeyEthereumTxHash, Value: txHash.Hex()},
{Key: evmtypes.AttributeKeyTxIndex, Value: "0"},
{Key: evmtypes.AttributeKeyTxGasUsed, Value: "21000"},
{Key: evmtypes.AttributeKeyEthereumTxFailed, Value: "execution reverted"},
},
}},
}}

suite.Run("GetTransactionLogs returns nil for failed tx", func() {
suite.SetupTest()

db := dbm.NewMemDB()
suite.backend.indexer = indexer.NewKVIndexer(db, log.NewNopLogger(), suite.backend.clientCtx)
err := suite.backend.indexer.IndexBlock(block, revertedBlockResult)
suite.Require().NoError(err)

logs, err := suite.backend.GetTransactionLogs(txHash)
suite.Require().NoError(err)
suite.Require().Nil(logs)
})

suite.Run("GetTransactionReceipt returns empty logs for failed tx despite ghost log events", func() {
suite.SetupTest()

var header metadata.MD
queryClient := suite.backend.queryClient.QueryClient.(*mocks.EVMQueryClient)
client := suite.backend.clientCtx.Client.(*mocks.Client)
RegisterParams(queryClient, &header, 1)
_, err := RegisterBlock(client, 1, txBz)
suite.Require().NoError(err)
// Ghost EventTypeTxLog events in block results — must not appear in receipt
_, err = RegisterBlockResultsWithEventLog(client, 1)
suite.Require().NoError(err)

db := dbm.NewMemDB()
suite.backend.indexer = indexer.NewKVIndexer(db, log.NewNopLogger(), suite.backend.clientCtx)
err = suite.backend.indexer.IndexBlock(block, revertedBlockResult)
suite.Require().NoError(err)

receipt, err := suite.backend.GetTransactionReceipt(txHash)
suite.Require().NoError(err)
suite.Require().NotNil(receipt)
suite.Require().Equal([][]*ethtypes.Log{}, receipt["logs"])
})

suite.Run("GetTransactionLogs and GetTransactionReceipt agree on empty logs for failed tx", func() {
suite.SetupTest()

var header metadata.MD
queryClient := suite.backend.queryClient.QueryClient.(*mocks.EVMQueryClient)
client := suite.backend.clientCtx.Client.(*mocks.Client)
RegisterParams(queryClient, &header, 1)
_, err := RegisterBlock(client, 1, txBz)
suite.Require().NoError(err)
_, err = RegisterBlockResultsWithEventLog(client, 1)
suite.Require().NoError(err)

db := dbm.NewMemDB()
suite.backend.indexer = indexer.NewKVIndexer(db, log.NewNopLogger(), suite.backend.clientCtx)
err = suite.backend.indexer.IndexBlock(block, revertedBlockResult)
suite.Require().NoError(err)

// GetTransactionLogs returns nil for failed tx (early return on res.Failed)
txLogs, err := suite.backend.GetTransactionLogs(txHash)
suite.Require().NoError(err)
suite.Require().Nil(txLogs)

// GetTransactionReceipt must also produce empty logs — not the ghost events
receipt, err := suite.backend.GetTransactionReceipt(txHash)
suite.Require().NoError(err)
suite.Require().NotNil(receipt)
suite.Require().Equal([][]*ethtypes.Log{}, receipt["logs"])
})
}
Loading