From 1f9d2ca4a9db222ee34317177bafc2e88be5f965 Mon Sep 17 00:00:00 2001 From: Arya Lanjewar <102943033+AryaLanjewar3005@users.noreply.github.com> Date: Mon, 8 Jun 2026 15:41:45 +0530 Subject: [PATCH 1/5] fix(vm): wire commit flag through DerivedEVMCallWithData to prevent unintended state persistence --- x/vm/keeper/call_evm.go | 5 ++--- 1 file changed, 2 insertions(+), 3 deletions(-) diff --git a/x/vm/keeper/call_evm.go b/x/vm/keeper/call_evm.go index 341c30d359..c7f97b9d73 100644 --- a/x/vm/keeper/call_evm.go +++ b/x/vm/keeper/call_evm.go @@ -239,13 +239,12 @@ func (k Keeper) DerivedEVMCallWithData( // thus restricted to be used only inside `ApplyMessage`. tmpCtx, commitState := ctx.CacheContext() - // pass true to commit the StateDB - res, err := k.ApplyMessageWithConfig(tmpCtx, msg, nil, true, cfg, txConfig) + res, err := k.ApplyMessageWithConfig(tmpCtx, msg, nil, commit, cfg, txConfig) if err != nil { return nil, err } - if !res.Failed() { + if commit && !res.Failed() { commitState() } From 7cac8239441cfc91b29aa0d1f9449b1ad21954a5 Mon Sep 17 00:00:00 2001 From: Arya Lanjewar <102943033+AryaLanjewar3005@users.noreply.github.com> Date: Mon, 8 Jun 2026 15:59:55 +0530 Subject: [PATCH 2/5] fix(vm): gate tx log events and block bloom updates on successful derived EVM execution --- x/vm/keeper/call_evm.go | 49 +++++++++++++++++++++++------------------ 1 file changed, 27 insertions(+), 22 deletions(-) diff --git a/x/vm/keeper/call_evm.go b/x/vm/keeper/call_evm.go index c7f97b9d73..fef91b5bb5 100644 --- a/x/vm/keeper/call_evm.go +++ b/x/vm/keeper/call_evm.go @@ -274,16 +274,6 @@ func (k Keeper) DerivedEVMCallWithData( attrs = append(attrs, sdk.NewAttribute(types.AttributeKeyEthereumTxFailed, res.VmError)) } - txLogAttrs := make([]sdk.Attribute, len(res.Logs)) - for i, log := range res.Logs { - log.TxHash = ethTxHash - value, err := json.Marshal(log) - if err != nil { - return nil, errorsmod.Wrap(err, "failed to encode log") - } - txLogAttrs[i] = sdk.NewAttribute(types.AttributeKeyTxLog, string(value)) - } - // adding txData for more info in rpc methods in order to parse derived txs attrs = append(attrs, sdk.NewAttribute(types.AttributeKeyTxData, hexutil.Encode(msg.Data()))) // adding nonce for more info in rpc methods in order to parse derived txs @@ -294,10 +284,6 @@ func (k Keeper) DerivedEVMCallWithData( types.EventTypeEthereumTx, attrs..., ), - sdk.NewEvent( - types.EventTypeTxLog, - txLogAttrs..., - ), sdk.NewEvent( sdk.EventTypeMessage, sdk.NewAttribute(sdk.AttributeKeyModule, types.ModuleName), @@ -306,14 +292,33 @@ func (k Keeper) DerivedEVMCallWithData( ), }) - logs := types.LogsToEthereum(res.Logs) - var bloomReceipt ethtypes.Bloom - if len(logs) > 0 { - bloom := k.GetBlockBloomTransient(ctx) - bloom.Or(bloom, big.NewInt(0).SetBytes(ethtypes.LogsBloom(logs))) - bloomReceipt = ethtypes.BytesToBloom(bloom.Bytes()) - k.SetBlockBloomTransient(ctx, bloomReceipt.Big()) - k.SetLogSizeTransient(ctx, (k.GetLogSizeTransient(ctx))+uint64(len(logs))) + if !res.Failed() { + txLogAttrs := make([]sdk.Attribute, len(res.Logs)) + for i, log := range res.Logs { + log.TxHash = ethTxHash + value, err := json.Marshal(log) + if err != nil { + return nil, errorsmod.Wrap(err, "failed to encode log") + } + txLogAttrs[i] = sdk.NewAttribute(types.AttributeKeyTxLog, string(value)) + } + + ctx.EventManager().EmitEvents(sdk.Events{ + sdk.NewEvent( + types.EventTypeTxLog, + txLogAttrs..., + ), + }) + + logs := types.LogsToEthereum(res.Logs) + var bloomReceipt ethtypes.Bloom + if len(logs) > 0 { + bloom := k.GetBlockBloomTransient(ctx) + bloom.Or(bloom, big.NewInt(0).SetBytes(ethtypes.LogsBloom(logs))) + bloomReceipt = ethtypes.BytesToBloom(bloom.Bytes()) + k.SetBlockBloomTransient(ctx, bloomReceipt.Big()) + k.SetLogSizeTransient(ctx, (k.GetLogSizeTransient(ctx))+uint64(len(logs))) + } } } From 50a2dce8e1345de9e25cad42a7c856c4a15a5d5d Mon Sep 17 00:00:00 2001 From: Arya Lanjewar <102943033+AryaLanjewar3005@users.noreply.github.com> Date: Tue, 9 Jun 2026 14:56:55 +0530 Subject: [PATCH 3/5] fix(rpc): fallback to event-query reconstruction on KV indexer miss for derived transaction lookups --- rpc/backend/tx_info.go | 12 ++++++------ 1 file changed, 6 insertions(+), 6 deletions(-) diff --git a/rpc/backend/tx_info.go b/rpc/backend/tx_info.go index 1531c5a774..6f7428607a 100644 --- a/rpc/backend/tx_info.go +++ b/rpc/backend/tx_info.go @@ -410,10 +410,10 @@ func (b *Backend) GetTransactionByBlockNumberAndIndex(blockNum rpctypes.BlockNum func (b *Backend) GetTxByEthHash(hash common.Hash) (*types.TxResult, *rpctypes.TxResultAdditionalFields, error) { if b.indexer != nil { txRes, err := b.indexer.GetByTxHash(hash) - if err != nil { - return nil, nil, err + if err == nil { + return txRes, nil, nil } - return txRes, nil, nil + // indexer miss or no derived-tx metadata — fall through to event-query reconstruction } // fallback to tendermint tx indexer @@ -430,10 +430,10 @@ func (b *Backend) GetTxByEthHash(hash common.Hash) (*types.TxResult, *rpctypes.T func (b *Backend) GetTxByEthHashAndMsgIndex(hash common.Hash, index int) (*types.TxResult, *rpctypes.TxResultAdditionalFields, error) { if b.indexer != nil { txRes, err := b.indexer.GetByTxHash(hash) - if err != nil { - return nil, nil, err + if err == nil { + return txRes, nil, nil } - return txRes, nil, nil + // indexer miss or no derived-tx metadata — fall through to event-query reconstruction } // fallback to tendermint tx indexer From 9321243ef6fa6f787014cb9bff714bde914d3185 Mon Sep 17 00:00:00 2001 From: Arya Lanjewar <102943033+AryaLanjewar3005@users.noreply.github.com> Date: Tue, 9 Jun 2026 15:23:15 +0530 Subject: [PATCH 4/5] fix(vm): replace hardcoded DerivedTxIndex with sequential transient counter for derived EVM executions --- x/vm/keeper/call_evm.go | 5 +++-- x/vm/types/events.go | 3 +-- 2 files changed, 4 insertions(+), 4 deletions(-) diff --git a/x/vm/keeper/call_evm.go b/x/vm/keeper/call_evm.go index fef91b5bb5..8cbadd6681 100644 --- a/x/vm/keeper/call_evm.go +++ b/x/vm/keeper/call_evm.go @@ -260,8 +260,7 @@ func (k Keeper) DerivedEVMCallWithData( sdk.NewAttribute(sdk.AttributeKeyAmount, value.String()), // add event for ethereum transaction hash format; sdk.NewAttribute(types.AttributeKeyEthereumTxHash, ethTxHash), - // add event for index of valid ethereum tx; NOTE: default txindex for derivedTx - sdk.NewAttribute(types.AttributeKeyTxIndex, strconv.FormatUint(types.DerivedTxIndex, 10)), + sdk.NewAttribute(types.AttributeKeyTxIndex, strconv.FormatUint(uint64(txConfig.TxIndex), 10)), // add event for eth tx gas used, we can't get it from cosmos tx result when it contains multiple eth tx msgs. sdk.NewAttribute(types.AttributeKeyTxGasUsed, strconv.FormatUint(gasUsed, 10)), }...) @@ -320,6 +319,8 @@ func (k Keeper) DerivedEVMCallWithData( k.SetLogSizeTransient(ctx, (k.GetLogSizeTransient(ctx))+uint64(len(logs))) } } + + k.SetTxIndexTransient(ctx, uint64(txConfig.TxIndex)+1) } if res.Failed() { diff --git a/x/vm/types/events.go b/x/vm/types/events.go index 489635e41d..15e5ddf046 100644 --- a/x/vm/types/events.go +++ b/x/vm/types/events.go @@ -30,6 +30,5 @@ const ( ) const ( - DerivedTxIndex = 9999 - DerivedTxType = 99 + DerivedTxType = 99 ) From 72e42fc0f876c5c2497ea945b3fdcc91ed842207 Mon Sep 17 00:00:00 2001 From: Arya Lanjewar <102943033+AryaLanjewar3005@users.noreply.github.com> Date: Wed, 10 Jun 2026 16:15:24 +0530 Subject: [PATCH 5/5] fix(rpc): gate GetTransactionReceipt log parsing on tx success to match GetTransactionLogs semantics --- rpc/backend/tx_info.go | 14 ++++-- rpc/backend/tx_info_test.go | 97 +++++++++++++++++++++++++++++++++++++ 2 files changed, 106 insertions(+), 5 deletions(-) diff --git a/rpc/backend/tx_info.go b/rpc/backend/tx_info.go index 6f7428607a..e924b85b65 100644 --- a/rpc/backend/tx_info.go +++ b/rpc/backend/tx_info.go @@ -239,11 +239,15 @@ func (b *Backend) GetTransactionReceipt(hash common.Hash) (map[string]interface{ 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 + 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 { diff --git a/rpc/backend/tx_info_test.go b/rpc/backend/tx_info_test.go index d05d96afe9..181bd8b53f 100644 --- a/rpc/backend/tx_info_test.go +++ b/rpc/backend/tx_info_test.go @@ -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" @@ -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"]) + }) +}