From 7743d8f1df240e0b4d0095277930559f8502eb11 Mon Sep 17 00:00:00 2001 From: Wen Date: Mon, 4 May 2026 16:18:41 -0700 Subject: [PATCH 1/9] evm: write status=0 receipt for state-transition errors MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Tx that's included in a block must produce an EVM receipt. The msg_server err-not-VM-error branch (V2: x/evm/keeper/msg_server.go; Giga: app/app.go executeEVMTxWithGigaExecutor) used to drop the receipt and log only — leaving eth_getTransactionByHash and eth_getTransactionReceipt returning null forever for included-but-failed txs. Write a status=0 receipt with gasUsed=gasLimit instead. Co-Authored-By: Claude Opus 4.7 (1M context) --- app/app.go | 26 ++++++++++++ x/evm/keeper/msg_server.go | 19 +++++++++ x/evm/keeper/msg_server_test.go | 72 +++++++++++++++++++++++++++++++++ 3 files changed, 117 insertions(+) diff --git a/app/app.go b/app/app.go index 1c4b435b69..6a3c5064f5 100644 --- a/app/app.go +++ b/app/app.go @@ -1892,6 +1892,32 @@ func (app *App) executeEVMTxWithGigaExecutor(ctx sdk.Context, msg *evmtypes.MsgE "error", ferr, ) } + + // EVM-spec compliance: any tx included in a block must produce a + // receipt. State-transition errors land here when Execute() bails + // before any opcode ran (notably EIP-7623's floor-data-gas check, + // which happens inside go-ethereum's Execute() rather than the + // Sei antehandler). Originally V2's matching branch dropped the + // receipt because the case was thought to be unreachable; Pectra + // made it observable. Without a receipt, eth_getTransactionReceipt + // returns null forever, hanging any client that polls for it. + evmMsg := &core.Message{ + Nonce: ethTx.Nonce(), + GasLimit: ethTx.Gas(), + GasPrice: ethTx.GasPrice(), + GasFeeCap: ethTx.GasFeeCap(), + GasTipCap: ethTx.GasTipCap(), + To: ethTx.To(), + Value: ethTx.Value(), + Data: ethTx.Data(), + From: sender, + } + if _, rerr := app.GigaEvmKeeper.WriteReceipt(ctx, stateDB, evmMsg, uint32(ethTx.Type()), ethTx.Hash(), ethTx.Gas(), execErr.Error()); rerr != nil { + logger.Error("giga: failed to write failed-tx receipt", + "tx-hash", ethTx.Hash(), + "error", rerr, + ) + } bloom := ethtypes.Bloom{} app.EvmKeeper.AppendToEvmTxDeferredInfo(ctx, bloom, ethTx.Hash(), surplus) diff --git a/x/evm/keeper/msg_server.go b/x/evm/keeper/msg_server.go index d7f55a62d0..d4ab414ee2 100644 --- a/x/evm/keeper/msg_server.go +++ b/x/evm/keeper/msg_server.go @@ -96,6 +96,25 @@ func (server msgServer) EVMTransaction(goCtx context.Context, msg *types.MsgEVMT telemetry.NewLabel("type", err.Error()), }, ) + + // EVM-spec compliance: any tx included in a block must produce a + // receipt. State-transition errors land here when Execute() bails + // before any opcode ran (notably EIP-7623's floor-data-gas check, + // which happens inside go-ethereum's Execute() rather than the + // Sei antehandler). Originally this branch was thought to be + // unreachable because the antehandler covered all pre-execution + // checks, so dropping the receipt was harmless. Pectra/EIP-7623 + // changed that — the branch now fires in normal operation, and a + // missing receipt makes eth_getTransactionReceipt return null + // forever, hanging any client that polls for it. + // + // Write a status=0 receipt with gasUsed=gasLimit (matching EVM + // semantics for an "included but failed before execution" tx). + // The receipt store is separate from the SC apphash, so this + // doesn't affect consensus state. + if _, rerr := server.WriteReceipt(ctx, stateDB, emsg, uint32(tx.Type()), tx.Hash(), tx.Gas(), err.Error()); rerr != nil { + logger.Error("failed to write failed-tx receipt", "tx", tx.Hash(), "err", rerr) + } return } extraSurplus := sdk.ZeroInt() diff --git a/x/evm/keeper/msg_server_test.go b/x/evm/keeper/msg_server_test.go index 6e7bb4fbe9..51838ae1e8 100644 --- a/x/evm/keeper/msg_server_test.go +++ b/x/evm/keeper/msg_server_test.go @@ -238,6 +238,78 @@ func TestEVMTransactionInsufficientGas(t *testing.T) { require.Equal(t, sdk.ZeroInt(), k.BankKeeper().GetBalance(ctx, evmAddr[:], k.GetBaseDenom(ctx)).Amount) // fee should be charged } +// TestEVMTransactionStateTransitionErrorProducesReceipt asserts that a tx +// which fails with an "EVM state transition error" (Execute() returns err +// before any opcode runs — e.g. intrinsic gas too low, EIP-7623 floor data +// gas insufficient) still has a status=0 receipt written to the transient +// receipt store with gasUsed=gasLimit. Per EVM spec, any tx included in a +// block must produce a receipt; without one, eth_getTransactionByHash and +// eth_getTransactionReceipt return null forever for the user, hanging any +// client that polls. (Under V2+CometBFT this was masked by the Cosmos tx +// indexer fallback; under Autobahn there is no such fallback, which is why +// the bug surfaced there first.) +func TestEVMTransactionStateTransitionErrorProducesReceipt(t *testing.T) { + k, ctx := testkeeper.MockEVMKeeper(t) + code, err := os.ReadFile("../../../example/contracts/simplestorage/SimpleStorage.bin") + require.Nil(t, err) + bz, err := hex.DecodeString(string(code)) + require.Nil(t, err) + privKey := testkeeper.MockPrivateKey() + testPrivHex := hex.EncodeToString(privKey.Bytes()) + key, _ := crypto.HexToECDSA(testPrivHex) + // Gas: 1000 is far below intrinsic (>=53k for creation), so go-ethereum's + // Execute() returns ErrIntrinsicGas before any opcode runs. This is the + // state-transition-error case our fix targets. + const gasLimit uint64 = 1000 + txData := ethtypes.LegacyTx{ + GasPrice: big.NewInt(1000000000000), + Gas: gasLimit, + To: nil, + Value: big.NewInt(0), + Data: bz, + Nonce: 0, + } + chainID := k.ChainID(ctx) + chainCfg := types.DefaultChainConfig() + ethCfg := chainCfg.EthereumConfig(chainID) + blockNum := big.NewInt(ctx.BlockHeight()) + signer := ethtypes.MakeSigner(ethCfg, blockNum, uint64(ctx.BlockTime().Unix())) + tx, err := ethtypes.SignTx(ethtypes.NewTx(&txData), signer, key) + require.Nil(t, err) + txwrapper, err := ethtx.NewLegacyTx(tx) + require.Nil(t, err) + req, err := types.NewMsgEVMTransaction(txwrapper) + require.Nil(t, err) + + _, evmAddr := testkeeper.PrivateKeyToAddresses(privKey) + amt := sdk.NewCoins(sdk.NewCoin(k.GetBaseDenom(ctx), sdk.NewInt(1000))) + require.NoError(t, k.BankKeeper().MintCoins(ctx, types.ModuleName, sdk.NewCoins(sdk.NewCoin(k.GetBaseDenom(ctx), sdk.NewInt(1000))))) + require.NoError(t, k.BankKeeper().SendCoinsFromModuleToAccount(ctx, types.ModuleName, evmAddr[:], amt)) + + msgServer := keeper.NewMsgServerImpl(k) + + ante.Preprocess(ctx, req, k.ChainID(ctx), false) + ctx, err = ante.NewEVMFeeCheckDecorator(k, &testkeeper.EVMTestApp.UpgradeKeeper).AnteHandle(ctx, mockTx{msgs: []sdk.Msg{req}}, false, func(sdk.Context, sdk.Tx, bool) (sdk.Context, error) { + return ctx, nil + }) + require.Nil(t, err) + _, err = msgServer.EVMTransaction(sdk.WrapSDKContext(ctx), req) + require.NotNil(t, err) + require.Contains(t, err.Error(), "intrinsic gas too low") + + // The fix: msg_server's err != nil branch now writes a status=0 receipt + // to the transient receipt store before returning. Verify it landed. + txHash := tx.Hash() + receipt, rerr := k.GetTransientReceipt(ctx, txHash, uint64(ctx.TxIndex())) + require.Nil(t, rerr, "transient receipt must exist for state-transition-error tx") + require.NotNil(t, receipt) + require.Equal(t, uint32(ethtypes.ReceiptStatusFailed), receipt.Status, "state-transition-error tx must have status=0 receipt") + require.Equal(t, gasLimit, receipt.GasUsed, "state-transition-error tx must report gasUsed=gasLimit per EVM spec") + require.Equal(t, txHash.Hex(), receipt.TxHashHex) + require.NotEmpty(t, receipt.VmError, "VmError should capture the state-transition error reason") + require.Contains(t, receipt.VmError, "intrinsic gas too low") +} + func TestEVMDynamicFeeTransaction(t *testing.T) { k, ctx := testkeeper.MockEVMKeeper(t) code, err := os.ReadFile("../../../example/contracts/simplestorage/SimpleStorage.bin") From bb0079ce2f3f1adfc363d8244714dabff133ee40 Mon Sep 17 00:00:00 2001 From: Wen Date: Mon, 4 May 2026 16:35:28 -0700 Subject: [PATCH 2/9] test(giga): cover failed-tx receipt write in Giga executor path MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Adds TestGiga_FailedExecution_ProducesReceipt — the Giga-path counterpart to TestEVMTransactionStateTransitionErrorProducesReceipt. Triggers the EIP-7623 floor-data-gas check inside go-ethereum's Execute() (intrinsic gas passes EvmStatelessChecks but floor data gas fails inside Execute()), then asserts the transient receipt store contains a status=0 receipt with gasUsed=gasLimit and a populated VmError. Without the app.go fix, the receipt is dropped and the assertion fails with "receipt not found"; with the fix it passes. Co-Authored-By: Claude Opus 4.7 (1M context) --- giga/tests/giga_test.go | 72 +++++++++++++++++++++++++++++++++++++++++ 1 file changed, 72 insertions(+) diff --git a/giga/tests/giga_test.go b/giga/tests/giga_test.go index 27aa3853f8..8920aa138d 100644 --- a/giga/tests/giga_test.go +++ b/giga/tests/giga_test.go @@ -1646,6 +1646,78 @@ func TestGiga_FeeValidationOrder_WrongNonce_NoNonceBump(t *testing.T) { require.Equal(t, initialNonce, finalNonce, "Nonce should NOT be bumped when tx nonce is wrong") } +// TestGiga_FailedExecution_ProducesReceipt asserts that an EVM tx executed via the +// Giga path which fails with a state-transition error inside go-ethereum's Execute() +// — Execute() returns err before any opcode runs (notably EIP-7623 floor-data-gas +// insufficient, which post-Pectra fires in normal operation) — still has a status=0 +// receipt written to the transient receipt store with gasUsed=gasLimit. +// +// Without the fix in app.go's executeEVMTxWithGigaExecutor, the receipt was dropped +// for these "should not happen" cases, so eth_getTransactionReceipt returned null +// forever for an included tx, hanging any client that polls. This is the Giga-path +// counterpart to TestEVMTransactionStateTransitionErrorProducesReceipt in +// x/evm/keeper/msg_server_test.go (which covers the matching V2 fix). +func TestGiga_FailedExecution_ProducesReceipt(t *testing.T) { + blockTime := time.Now() + accts := utils.NewTestAccounts(3) + signer := utils.NewSigner() + + gigaCtx := NewGigaTestContext(t, accts, blockTime, 1, ModeGigaSequential) + fundAccount(t, gigaCtx, signer.AccountAddress, new(big.Int).Mul(big.NewInt(1e18), big.NewInt(1000))) + gigaCtx.TestApp.EvmKeeper.SetAddressMapping(gigaCtx.Ctx, signer.AccountAddress, signer.EvmAddress) + + // EIP-7623 floor: 21000 + 10 * data-tokens (zero byte = 1 token). + // Intrinsic (EIP-2028): 21000 + 4 * data-tokens. + // 1000 zero-byte payload → intrinsic=25000, floor=31000. + // gasLimit=27500 passes EvmStatelessChecks' intrinsic check (>=25000) but fails + // go-ethereum's floor-data-gas check inside Execute() (<31000), which is exactly + // the state-transition-error branch the fix targets. + const dataLen = 1000 + const gasLimit uint64 = 27500 + to := common.HexToAddress("0x0000000000000000000000000000000000001234") + signedTx, err := ethtypes.SignTx(ethtypes.NewTx(ðtypes.DynamicFeeTx{ + ChainID: big.NewInt(config.DefaultChainID), + Nonce: gigaCtx.TestApp.GigaEvmKeeper.GetNonce(gigaCtx.Ctx, signer.EvmAddress), + GasFeeCap: big.NewInt(100000000000), + GasTipCap: big.NewInt(100000000000), + Gas: gasLimit, + To: &to, + Value: big.NewInt(0), + Data: make([]byte, dataLen), + }), signer.EvmSigner, signer.EvmPrivateKey) + require.NoError(t, err) + + tc := app.MakeEncodingConfig().TxConfig + txData, err := ethtx.NewTxDataFromTx(signedTx) + require.NoError(t, err) + msg, err := types.NewMsgEVMTransaction(txData) + require.NoError(t, err) + txBuilder := tc.NewTxBuilder() + require.NoError(t, txBuilder.SetMsgs(msg)) + txBuilder.SetGasLimit(10000000000) + txBytes, err := tc.TxEncoder()(txBuilder.GetTx()) + require.NoError(t, err) + + _, results, err := RunBlock(t, gigaCtx, [][]byte{txBytes}) + require.NoError(t, err) + require.Len(t, results, 1) + + require.Equal(t, uint32(1), results[0].Code, "tx must fail with code=1: log=%q", results[0].Log) + require.Contains(t, results[0].Log, "floor data gas", "expected floor-data-gas error: %s", results[0].Log) + + // The fix: executeEVMTxWithGigaExecutor's execErr != nil branch now writes a + // status=0 receipt to the transient store before returning. Verify it landed. + txHash := signedTx.Hash() + receipt, rerr := gigaCtx.TestApp.GigaEvmKeeper.GetTransientReceipt(gigaCtx.Ctx, txHash, 0) + require.Nil(t, rerr, "transient receipt must exist for state-transition-error tx (Giga path)") + require.NotNil(t, receipt) + require.Equal(t, uint32(ethtypes.ReceiptStatusFailed), receipt.Status, "state-transition-error tx must have status=0 receipt") + require.Equal(t, gasLimit, receipt.GasUsed, "state-transition-error tx must report gasUsed=gasLimit per EVM spec") + require.Equal(t, txHash.Hex(), receipt.TxHashHex) + require.NotEmpty(t, receipt.VmError, "VmError should capture the state-transition error reason") + require.Contains(t, receipt.VmError, "floor data gas") +} + // TestGigaVsGeth_FeeValidationOrder compares Giga and Geth nonce bump behavior. func TestGigaVsGeth_FeeValidationOrder(t *testing.T) { blockTime := time.Now() From b44cef54295db8e342661d887385cd426e64a6e7 Mon Sep 17 00:00:00 2001 From: Wen Date: Mon, 4 May 2026 17:16:21 -0700 Subject: [PATCH 3/9] giga: use effective gas price in failure-branch receipt Mirror the success-branch evmMsg construction (PR #3384): use effectiveGasPrice (computed at line 1866) for receipt's EffectiveGasPrice field instead of ethTx.GasPrice() which returns GasFeeCap for dynamic-fee txs. Co-Authored-By: Claude Opus 4.7 (1M context) --- app/app.go | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/app/app.go b/app/app.go index 6a3c5064f5..8b2f49befe 100644 --- a/app/app.go +++ b/app/app.go @@ -1904,7 +1904,7 @@ func (app *App) executeEVMTxWithGigaExecutor(ctx sdk.Context, msg *evmtypes.MsgE evmMsg := &core.Message{ Nonce: ethTx.Nonce(), GasLimit: ethTx.Gas(), - GasPrice: ethTx.GasPrice(), + GasPrice: effectiveGasPrice, // EIP-1559 effective price; same pattern as success branch (PR #3384) GasFeeCap: ethTx.GasFeeCap(), GasTipCap: ethTx.GasTipCap(), To: ethTx.To(), From 751c0ae9357198041cffd2cf371a9ba8a173df8a Mon Sep 17 00:00:00 2001 From: Wen Date: Mon, 4 May 2026 17:21:54 -0700 Subject: [PATCH 4/9] test: switch state-transition test to dynamic-fee tx LegacyTx had GasPrice == GasFeeCap == GasTipCap, so the EffectiveGasPrice assertion would pass equally whether the receipt stored maxFee or the EIP-1559 effective price. Switch to a DynamicFeeTx where tip < cap so the assertion discriminates: if anyone reverts the err-branch evmMsg to hand-roll with ethTx.GasPrice() (returns GasFeeCap for dynamic-fee txs), the assertion catches it. Co-Authored-By: Claude Opus 4.7 (1M context) --- x/evm/keeper/msg_server_test.go | 50 +++++++++++++++++++++++++-------- 1 file changed, 39 insertions(+), 11 deletions(-) diff --git a/x/evm/keeper/msg_server_test.go b/x/evm/keeper/msg_server_test.go index 51838ae1e8..30133e3e81 100644 --- a/x/evm/keeper/msg_server_test.go +++ b/x/evm/keeper/msg_server_test.go @@ -260,14 +260,25 @@ func TestEVMTransactionStateTransitionErrorProducesReceipt(t *testing.T) { // Gas: 1000 is far below intrinsic (>=53k for creation), so go-ethereum's // Execute() returns ErrIntrinsicGas before any opcode runs. This is the // state-transition-error case our fix targets. - const gasLimit uint64 = 1000 - txData := ethtypes.LegacyTx{ - GasPrice: big.NewInt(1000000000000), - Gas: gasLimit, - To: nil, - Value: big.NewInt(0), - Data: bz, - Nonce: 0, + // + // Use a DynamicFeeTx with GasTipCap < GasFeeCap so that effective gas price + // (min(baseFee+tip, maxFee)) is provably different from maxFee. That makes + // the EffectiveGasPrice assertion below discriminate: if a future change + // reverts to passing ethTx.GasPrice() (which returns GasFeeCap = maxFee for + // dynamic-fee txs) instead of the EIP-1559 effective price, the test fails. + const ( + gasLimit uint64 = 1000 + feeCap int64 = 10_000_000_000 // 10 gwei (max) + tipCap int64 = 1_000_000_000 // 1 gwei (priority) + ) + txData := ethtypes.DynamicFeeTx{ + GasFeeCap: big.NewInt(feeCap), + GasTipCap: big.NewInt(tipCap), + Gas: gasLimit, + To: nil, + Value: big.NewInt(0), + Data: bz, + Nonce: 0, } chainID := k.ChainID(ctx) chainCfg := types.DefaultChainConfig() @@ -276,14 +287,15 @@ func TestEVMTransactionStateTransitionErrorProducesReceipt(t *testing.T) { signer := ethtypes.MakeSigner(ethCfg, blockNum, uint64(ctx.BlockTime().Unix())) tx, err := ethtypes.SignTx(ethtypes.NewTx(&txData), signer, key) require.Nil(t, err) - txwrapper, err := ethtx.NewLegacyTx(tx) + txwrapper, err := ethtx.NewDynamicFeeTx(tx) require.Nil(t, err) req, err := types.NewMsgEVMTransaction(txwrapper) require.Nil(t, err) _, evmAddr := testkeeper.PrivateKeyToAddresses(privKey) - amt := sdk.NewCoins(sdk.NewCoin(k.GetBaseDenom(ctx), sdk.NewInt(1000))) - require.NoError(t, k.BankKeeper().MintCoins(ctx, types.ModuleName, sdk.NewCoins(sdk.NewCoin(k.GetBaseDenom(ctx), sdk.NewInt(1000))))) + // Fund the sender enough to cover the upper-bound fee charge (gasLimit * maxFee). + amt := sdk.NewCoins(sdk.NewCoin(k.GetBaseDenom(ctx), sdk.NewInt(1_000_000_000_000))) + require.NoError(t, k.BankKeeper().MintCoins(ctx, types.ModuleName, amt)) require.NoError(t, k.BankKeeper().SendCoinsFromModuleToAccount(ctx, types.ModuleName, evmAddr[:], amt)) msgServer := keeper.NewMsgServerImpl(k) @@ -308,6 +320,22 @@ func TestEVMTransactionStateTransitionErrorProducesReceipt(t *testing.T) { require.Equal(t, txHash.Hex(), receipt.TxHashHex) require.NotEmpty(t, receipt.VmError, "VmError should capture the state-transition error reason") require.Contains(t, receipt.VmError, "intrinsic gas too low") + + // EIP-1559 effective gas price contract: receipt.EffectiveGasPrice must + // equal min(baseFee+tip, maxFee), not maxFee. The V2 path constructs the + // receipt's core.Message via GetEVMMessage which already applies this + // adjustment; this assertion is a regression guard so that if anyone + // later changes the err-branch to hand-roll an evmMsg using + // ethTx.GasPrice() (which returns GasFeeCap = maxFee for dynamic-fee + // txs) the test catches it. + expectedEffective := tipCap + k.GetBaseFee(ctx).Int64() + if expectedEffective > feeCap { + expectedEffective = feeCap + } + require.Equal(t, uint64(expectedEffective), receipt.EffectiveGasPrice, + "receipt.EffectiveGasPrice must be min(baseFee+tip, maxFee), not maxFee=%d", feeCap) + require.NotEqual(t, uint64(feeCap), receipt.EffectiveGasPrice, + "sanity: this test is configured so effective != maxFee (otherwise the assertion above wouldn't discriminate)") } func TestEVMDynamicFeeTransaction(t *testing.T) { From b540480ced1be60a25704693156d2e85734a97df Mon Sep 17 00:00:00 2001 From: Wen Date: Mon, 4 May 2026 17:25:21 -0700 Subject: [PATCH 5/9] Drop PR cross-reference from inline comment Co-Authored-By: Claude Opus 4.7 (1M context) --- app/app.go | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/app/app.go b/app/app.go index 8b2f49befe..3cb28d92b1 100644 --- a/app/app.go +++ b/app/app.go @@ -1904,7 +1904,7 @@ func (app *App) executeEVMTxWithGigaExecutor(ctx sdk.Context, msg *evmtypes.MsgE evmMsg := &core.Message{ Nonce: ethTx.Nonce(), GasLimit: ethTx.Gas(), - GasPrice: effectiveGasPrice, // EIP-1559 effective price; same pattern as success branch (PR #3384) + GasPrice: effectiveGasPrice, // EIP-1559 effective gas price (not GasFeeCap) GasFeeCap: ethTx.GasFeeCap(), GasTipCap: ethTx.GasTipCap(), To: ethTx.To(), From 118004fcfc988ec48807b61eaf890ad32630fe94 Mon Sep 17 00:00:00 2001 From: Wen Date: Mon, 4 May 2026 17:44:26 -0700 Subject: [PATCH 6/9] gofmt -s test file --- x/evm/keeper/msg_server_test.go | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/x/evm/keeper/msg_server_test.go b/x/evm/keeper/msg_server_test.go index 30133e3e81..0a2f6cce73 100644 --- a/x/evm/keeper/msg_server_test.go +++ b/x/evm/keeper/msg_server_test.go @@ -267,9 +267,9 @@ func TestEVMTransactionStateTransitionErrorProducesReceipt(t *testing.T) { // reverts to passing ethTx.GasPrice() (which returns GasFeeCap = maxFee for // dynamic-fee txs) instead of the EIP-1559 effective price, the test fails. const ( - gasLimit uint64 = 1000 - feeCap int64 = 10_000_000_000 // 10 gwei (max) - tipCap int64 = 1_000_000_000 // 1 gwei (priority) + gasLimit uint64 = 1000 + feeCap int64 = 10_000_000_000 // 10 gwei (max) + tipCap int64 = 1_000_000_000 // 1 gwei (priority) ) txData := ethtypes.DynamicFeeTx{ GasFeeCap: big.NewInt(feeCap), From a88912528ecd927c3bebc4ea51db37b3d5ee1267 Mon Sep 17 00:00:00 2001 From: Wen Date: Wed, 6 May 2026 09:30:37 -0700 Subject: [PATCH 7/9] evm: drop redundant V2 receipt-write; assert nonce-bump invariant V2's existing EndBlock synthetic-receipt path (x/evm/keeper/abci.go:100) already writes a receipt for state-transition errors: the GetAllEVMTxDeferredInfo fallback synthesizes a DeferredInfo from txRes.Log when none was appended, and EndBlock's loop gates the synthetic-receipt write on GetNonceBumped. BasicDecorator's WithDeliverTxCallback bumps the nonce + calls SetNonceBumped on every DeliverTx (including failures), so the rule "receipt iff the tx bumped the sender's nonce" already holds for V2. The explicit WriteReceipt added to msg_server.go was redundant and also bypassed the GetNonceBumped gate, so it's removed. The Giga path is still missing the receipt because its AppendToEvmTxDeferredInfo call in the err-branch doesn't propagate the Error string, so EndBlock's `if deferredInfo.Error != ""` check fails and skips the synthetic-receipt write. Giga's explicit WriteReceipt in app.go is kept and is gated on the explicit nonce bump it already does at app.go:1887. Both tests are extended with nonce-before/after assertions to lock in the invariant. The V2 test is rewritten to drive the full production sequence (BasicDecorator -> msgServer -> deliverTxCallback -> SetMsgs/SetTxResults -> EndBlock) so it exercises the synthetic path end-to-end for an EIP-7623 floor-data-gas-underflow tx. Co-Authored-By: Claude Opus 4.7 (1M context) --- giga/tests/giga_test.go | 8 +++ x/evm/keeper/msg_server.go | 18 ----- x/evm/keeper/msg_server_test.go | 112 ++++++++++++++++---------------- 3 files changed, 65 insertions(+), 73 deletions(-) diff --git a/giga/tests/giga_test.go b/giga/tests/giga_test.go index 8920aa138d..4452c55404 100644 --- a/giga/tests/giga_test.go +++ b/giga/tests/giga_test.go @@ -1698,6 +1698,7 @@ func TestGiga_FailedExecution_ProducesReceipt(t *testing.T) { txBytes, err := tc.TxEncoder()(txBuilder.GetTx()) require.NoError(t, err) + nonceBefore := gigaCtx.TestApp.GigaEvmKeeper.GetNonce(gigaCtx.Ctx, signer.EvmAddress) _, results, err := RunBlock(t, gigaCtx, [][]byte{txBytes}) require.NoError(t, err) require.Len(t, results, 1) @@ -1705,6 +1706,13 @@ func TestGiga_FailedExecution_ProducesReceipt(t *testing.T) { require.Equal(t, uint32(1), results[0].Code, "tx must fail with code=1: log=%q", results[0].Log) require.Contains(t, results[0].Log, "floor data gas", "expected floor-data-gas error: %s", results[0].Log) + // Receipts are only valid for txs that bumped the sender's nonce. Giga's + // executeEVMTxWithGigaExecutor explicitly bumps the nonce in its execErr + // branch (app/app.go) before writing the receipt — assert the bump + // happened so this invariant is locked in for any future refactor. + nonceAfter := gigaCtx.TestApp.GigaEvmKeeper.GetNonce(gigaCtx.Ctx, signer.EvmAddress) + require.Equal(t, nonceBefore+1, nonceAfter, "Giga must bump the nonce on state-transition error, otherwise no receipt should be written") + // The fix: executeEVMTxWithGigaExecutor's execErr != nil branch now writes a // status=0 receipt to the transient store before returning. Verify it landed. txHash := signedTx.Hash() diff --git a/x/evm/keeper/msg_server.go b/x/evm/keeper/msg_server.go index d4ab414ee2..8a4f5324af 100644 --- a/x/evm/keeper/msg_server.go +++ b/x/evm/keeper/msg_server.go @@ -97,24 +97,6 @@ func (server msgServer) EVMTransaction(goCtx context.Context, msg *types.MsgEVMT }, ) - // EVM-spec compliance: any tx included in a block must produce a - // receipt. State-transition errors land here when Execute() bails - // before any opcode ran (notably EIP-7623's floor-data-gas check, - // which happens inside go-ethereum's Execute() rather than the - // Sei antehandler). Originally this branch was thought to be - // unreachable because the antehandler covered all pre-execution - // checks, so dropping the receipt was harmless. Pectra/EIP-7623 - // changed that — the branch now fires in normal operation, and a - // missing receipt makes eth_getTransactionReceipt return null - // forever, hanging any client that polls for it. - // - // Write a status=0 receipt with gasUsed=gasLimit (matching EVM - // semantics for an "included but failed before execution" tx). - // The receipt store is separate from the SC apphash, so this - // doesn't affect consensus state. - if _, rerr := server.WriteReceipt(ctx, stateDB, emsg, uint32(tx.Type()), tx.Hash(), tx.Gas(), err.Error()); rerr != nil { - logger.Error("failed to write failed-tx receipt", "tx", tx.Hash(), "err", rerr) - } return } extraSurplus := sdk.ZeroInt() diff --git a/x/evm/keeper/msg_server_test.go b/x/evm/keeper/msg_server_test.go index 0a2f6cce73..b0a4caa02b 100644 --- a/x/evm/keeper/msg_server_test.go +++ b/x/evm/keeper/msg_server_test.go @@ -238,46 +238,45 @@ func TestEVMTransactionInsufficientGas(t *testing.T) { require.Equal(t, sdk.ZeroInt(), k.BankKeeper().GetBalance(ctx, evmAddr[:], k.GetBaseDenom(ctx)).Amount) // fee should be charged } -// TestEVMTransactionStateTransitionErrorProducesReceipt asserts that a tx -// which fails with an "EVM state transition error" (Execute() returns err -// before any opcode runs — e.g. intrinsic gas too low, EIP-7623 floor data -// gas insufficient) still has a status=0 receipt written to the transient -// receipt store with gasUsed=gasLimit. Per EVM spec, any tx included in a -// block must produce a receipt; without one, eth_getTransactionByHash and -// eth_getTransactionReceipt return null forever for the user, hanging any -// client that polls. (Under V2+CometBFT this was masked by the Cosmos tx -// indexer fallback; under Autobahn there is no such fallback, which is why -// the bug surfaced there first.) +// TestEVMTransactionStateTransitionErrorProducesReceipt drives the V2 production +// sequence end-to-end (BasicDecorator → msgServer → deliverTxCallback → +// SetMsgs/SetTxResults → EndBlock) for an EIP-7623 floor-data-gas-underflow +// tx — a case Pectra introduced where Execute() fails inside go-ethereum +// after the Sei antehandler accepts the tx. It locks in the contract that: +// +// 1. The deliverTxCallback registered by BasicDecorator bumps the sender's +// nonce and sets the NonceBumped flag, even when msgServer returns err. +// 2. EndBlock's synthetic-receipt path (x/evm/keeper/abci.go:100-113) writes +// a receipt for this tx via the GetAllEVMTxDeferredInfo fallback (which +// synthesizes a DeferredInfo from txRes.Log when none was appended), and +// gates that write on GetNonceBumped — implementing the rule +// "receipt iff the tx bumped the sender's nonce." func TestEVMTransactionStateTransitionErrorProducesReceipt(t *testing.T) { k, ctx := testkeeper.MockEVMKeeper(t) - code, err := os.ReadFile("../../../example/contracts/simplestorage/SimpleStorage.bin") - require.Nil(t, err) - bz, err := hex.DecodeString(string(code)) - require.Nil(t, err) privKey := testkeeper.MockPrivateKey() testPrivHex := hex.EncodeToString(privKey.Bytes()) key, _ := crypto.HexToECDSA(testPrivHex) - // Gas: 1000 is far below intrinsic (>=53k for creation), so go-ethereum's - // Execute() returns ErrIntrinsicGas before any opcode runs. This is the - // state-transition-error case our fix targets. - // - // Use a DynamicFeeTx with GasTipCap < GasFeeCap so that effective gas price - // (min(baseFee+tip, maxFee)) is provably different from maxFee. That makes - // the EffectiveGasPrice assertion below discriminate: if a future change - // reverts to passing ethTx.GasPrice() (which returns GasFeeCap = maxFee for - // dynamic-fee txs) instead of the EIP-1559 effective price, the test fails. + + // EIP-7623 floor: 21000 + 10 * data-tokens (zero byte = 1 token). + // Intrinsic (EIP-2028): 21000 + 4 * data-tokens. + // 1000 zero-byte payload → intrinsic=25000, floor=31000. + // gasLimit=27500 passes BasicDecorator's intrinsic check (>=25000) but + // fails go-ethereum's floor-data-gas check inside Execute() (<31000) — + // exactly the state-transition-error branch we're investigating. const ( - gasLimit uint64 = 1000 - feeCap int64 = 10_000_000_000 // 10 gwei (max) - tipCap int64 = 1_000_000_000 // 1 gwei (priority) + dataLen = 1000 + gasLimit uint64 = 27500 + feeCap int64 = 100_000_000_000 + tipCap int64 = 100_000_000_000 ) + to := common.HexToAddress("0x0000000000000000000000000000000000001234") txData := ethtypes.DynamicFeeTx{ GasFeeCap: big.NewInt(feeCap), GasTipCap: big.NewInt(tipCap), Gas: gasLimit, - To: nil, + To: &to, Value: big.NewInt(0), - Data: bz, + Data: make([]byte, dataLen), Nonce: 0, } chainID := k.ChainID(ctx) @@ -293,49 +292,52 @@ func TestEVMTransactionStateTransitionErrorProducesReceipt(t *testing.T) { require.Nil(t, err) _, evmAddr := testkeeper.PrivateKeyToAddresses(privKey) - // Fund the sender enough to cover the upper-bound fee charge (gasLimit * maxFee). - amt := sdk.NewCoins(sdk.NewCoin(k.GetBaseDenom(ctx), sdk.NewInt(1_000_000_000_000))) + amt := sdk.NewCoins(sdk.NewCoin(k.GetBaseDenom(ctx), sdk.NewInt(1_000_000_000_000_000))) require.NoError(t, k.BankKeeper().MintCoins(ctx, types.ModuleName, amt)) require.NoError(t, k.BankKeeper().SendCoinsFromModuleToAccount(ctx, types.ModuleName, evmAddr[:], amt)) msgServer := keeper.NewMsgServerImpl(k) ante.Preprocess(ctx, req, k.ChainID(ctx), false) + ctx = ctx.WithIsCheckTx(false).WithIsReCheckTx(false) + ctx, err = ante.NewBasicDecorator(k).AnteHandle(ctx, mockTx{msgs: []sdk.Msg{req}}, false, func(c sdk.Context, _ sdk.Tx, _ bool) (sdk.Context, error) { + return c, nil + }) + require.Nil(t, err, "BasicDecorator should pass — intrinsic check is satisfied") ctx, err = ante.NewEVMFeeCheckDecorator(k, &testkeeper.EVMTestApp.UpgradeKeeper).AnteHandle(ctx, mockTx{msgs: []sdk.Msg{req}}, false, func(sdk.Context, sdk.Tx, bool) (sdk.Context, error) { return ctx, nil }) - require.Nil(t, err) + require.Nil(t, err, "EVMFeeCheckDecorator should pass") + + k.BeginBlock(ctx) + + nonceBefore := k.GetNonce(ctx, evmAddr) _, err = msgServer.EVMTransaction(sdk.WrapSDKContext(ctx), req) require.NotNil(t, err) - require.Contains(t, err.Error(), "intrinsic gas too low") + require.Contains(t, err.Error(), "floor data gas") - // The fix: msg_server's err != nil branch now writes a status=0 receipt - // to the transient receipt store before returning. Verify it landed. + // Fire the deliverTxCallback the SDK runs post-DeliverTx — this is where + // BasicDecorator's nonce bump happens in production. + if cb := ctx.DeliverTxCallback(); cb != nil { + cb(ctx) + } + nonceAfter := k.GetNonce(ctx, evmAddr) + require.Equal(t, nonceBefore+1, nonceAfter, "nonce must be bumped post-DeliverTx for the receipt rule to allow a receipt") + require.True(t, k.GetNonceBumped(ctx, uint32(ctx.TxIndex())), "SetNonceBumped must be set so EndBlock writes the synthetic receipt") + + // Simulate the rest of FinalizeBlock (app.go:1787-1802). + txRes := &abci.ExecTxResult{Code: 1, Log: err.Error(), GasWanted: int64(gasLimit)} //nolint:gosec + k.SetTxResults([]*abci.ExecTxResult{txRes}) + k.SetMsgs([]*types.MsgEVMTransaction{req}) + k.EndBlock(ctx, ctx.BlockHeight(), 0) + + // Hypothesis: the receipt should be created by EndBlock's synthetic path. txHash := tx.Hash() receipt, rerr := k.GetTransientReceipt(ctx, txHash, uint64(ctx.TxIndex())) - require.Nil(t, rerr, "transient receipt must exist for state-transition-error tx") + require.Nil(t, rerr, "EndBlock should have written a synthetic receipt for the floor-data-gas tx (nonce was bumped)") require.NotNil(t, receipt) - require.Equal(t, uint32(ethtypes.ReceiptStatusFailed), receipt.Status, "state-transition-error tx must have status=0 receipt") - require.Equal(t, gasLimit, receipt.GasUsed, "state-transition-error tx must report gasUsed=gasLimit per EVM spec") require.Equal(t, txHash.Hex(), receipt.TxHashHex) - require.NotEmpty(t, receipt.VmError, "VmError should capture the state-transition error reason") - require.Contains(t, receipt.VmError, "intrinsic gas too low") - - // EIP-1559 effective gas price contract: receipt.EffectiveGasPrice must - // equal min(baseFee+tip, maxFee), not maxFee. The V2 path constructs the - // receipt's core.Message via GetEVMMessage which already applies this - // adjustment; this assertion is a regression guard so that if anyone - // later changes the err-branch to hand-roll an evmMsg using - // ethTx.GasPrice() (which returns GasFeeCap = maxFee for dynamic-fee - // txs) the test catches it. - expectedEffective := tipCap + k.GetBaseFee(ctx).Int64() - if expectedEffective > feeCap { - expectedEffective = feeCap - } - require.Equal(t, uint64(expectedEffective), receipt.EffectiveGasPrice, - "receipt.EffectiveGasPrice must be min(baseFee+tip, maxFee), not maxFee=%d", feeCap) - require.NotEqual(t, uint64(feeCap), receipt.EffectiveGasPrice, - "sanity: this test is configured so effective != maxFee (otherwise the assertion above wouldn't discriminate)") + require.Contains(t, receipt.VmError, "floor data gas", "synthetic receipt should carry the err.Error() captured in txRes.Log") } func TestEVMDynamicFeeTransaction(t *testing.T) { From 2ffebc5441281c6475a2bfb9d17591c44717e5c6 Mon Sep 17 00:00:00 2001 From: Wen Date: Wed, 6 May 2026 09:39:10 -0700 Subject: [PATCH 8/9] Drop V2 changes from PR scope MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit V2's msg_server.go is unchanged on this branch (the EndBlock synthetic path already handles state-transition errors), so the V2 test added earlier — useful as the experiment that proved the V2 path works for floor-data-gas-underflow — is out of scope for a Giga-only PR. The existing tests in x/evm/keeper/abci_test.go (TestEndBlock_NoReceipt ForNonceMismatch, TestEndBlock_ReceiptCreatedWhenNonceBumped) already cover the EndBlock synthetic-receipt mechanism and its GetNonceBumped gate. Co-Authored-By: Claude Opus 4.7 (1M context) --- x/evm/keeper/msg_server.go | 1 - x/evm/keeper/msg_server_test.go | 102 -------------------------------- 2 files changed, 103 deletions(-) diff --git a/x/evm/keeper/msg_server.go b/x/evm/keeper/msg_server.go index 8a4f5324af..d7f55a62d0 100644 --- a/x/evm/keeper/msg_server.go +++ b/x/evm/keeper/msg_server.go @@ -96,7 +96,6 @@ func (server msgServer) EVMTransaction(goCtx context.Context, msg *types.MsgEVMT telemetry.NewLabel("type", err.Error()), }, ) - return } extraSurplus := sdk.ZeroInt() diff --git a/x/evm/keeper/msg_server_test.go b/x/evm/keeper/msg_server_test.go index b0a4caa02b..6e7bb4fbe9 100644 --- a/x/evm/keeper/msg_server_test.go +++ b/x/evm/keeper/msg_server_test.go @@ -238,108 +238,6 @@ func TestEVMTransactionInsufficientGas(t *testing.T) { require.Equal(t, sdk.ZeroInt(), k.BankKeeper().GetBalance(ctx, evmAddr[:], k.GetBaseDenom(ctx)).Amount) // fee should be charged } -// TestEVMTransactionStateTransitionErrorProducesReceipt drives the V2 production -// sequence end-to-end (BasicDecorator → msgServer → deliverTxCallback → -// SetMsgs/SetTxResults → EndBlock) for an EIP-7623 floor-data-gas-underflow -// tx — a case Pectra introduced where Execute() fails inside go-ethereum -// after the Sei antehandler accepts the tx. It locks in the contract that: -// -// 1. The deliverTxCallback registered by BasicDecorator bumps the sender's -// nonce and sets the NonceBumped flag, even when msgServer returns err. -// 2. EndBlock's synthetic-receipt path (x/evm/keeper/abci.go:100-113) writes -// a receipt for this tx via the GetAllEVMTxDeferredInfo fallback (which -// synthesizes a DeferredInfo from txRes.Log when none was appended), and -// gates that write on GetNonceBumped — implementing the rule -// "receipt iff the tx bumped the sender's nonce." -func TestEVMTransactionStateTransitionErrorProducesReceipt(t *testing.T) { - k, ctx := testkeeper.MockEVMKeeper(t) - privKey := testkeeper.MockPrivateKey() - testPrivHex := hex.EncodeToString(privKey.Bytes()) - key, _ := crypto.HexToECDSA(testPrivHex) - - // EIP-7623 floor: 21000 + 10 * data-tokens (zero byte = 1 token). - // Intrinsic (EIP-2028): 21000 + 4 * data-tokens. - // 1000 zero-byte payload → intrinsic=25000, floor=31000. - // gasLimit=27500 passes BasicDecorator's intrinsic check (>=25000) but - // fails go-ethereum's floor-data-gas check inside Execute() (<31000) — - // exactly the state-transition-error branch we're investigating. - const ( - dataLen = 1000 - gasLimit uint64 = 27500 - feeCap int64 = 100_000_000_000 - tipCap int64 = 100_000_000_000 - ) - to := common.HexToAddress("0x0000000000000000000000000000000000001234") - txData := ethtypes.DynamicFeeTx{ - GasFeeCap: big.NewInt(feeCap), - GasTipCap: big.NewInt(tipCap), - Gas: gasLimit, - To: &to, - Value: big.NewInt(0), - Data: make([]byte, dataLen), - Nonce: 0, - } - chainID := k.ChainID(ctx) - chainCfg := types.DefaultChainConfig() - ethCfg := chainCfg.EthereumConfig(chainID) - blockNum := big.NewInt(ctx.BlockHeight()) - signer := ethtypes.MakeSigner(ethCfg, blockNum, uint64(ctx.BlockTime().Unix())) - tx, err := ethtypes.SignTx(ethtypes.NewTx(&txData), signer, key) - require.Nil(t, err) - txwrapper, err := ethtx.NewDynamicFeeTx(tx) - require.Nil(t, err) - req, err := types.NewMsgEVMTransaction(txwrapper) - require.Nil(t, err) - - _, evmAddr := testkeeper.PrivateKeyToAddresses(privKey) - amt := sdk.NewCoins(sdk.NewCoin(k.GetBaseDenom(ctx), sdk.NewInt(1_000_000_000_000_000))) - require.NoError(t, k.BankKeeper().MintCoins(ctx, types.ModuleName, amt)) - require.NoError(t, k.BankKeeper().SendCoinsFromModuleToAccount(ctx, types.ModuleName, evmAddr[:], amt)) - - msgServer := keeper.NewMsgServerImpl(k) - - ante.Preprocess(ctx, req, k.ChainID(ctx), false) - ctx = ctx.WithIsCheckTx(false).WithIsReCheckTx(false) - ctx, err = ante.NewBasicDecorator(k).AnteHandle(ctx, mockTx{msgs: []sdk.Msg{req}}, false, func(c sdk.Context, _ sdk.Tx, _ bool) (sdk.Context, error) { - return c, nil - }) - require.Nil(t, err, "BasicDecorator should pass — intrinsic check is satisfied") - ctx, err = ante.NewEVMFeeCheckDecorator(k, &testkeeper.EVMTestApp.UpgradeKeeper).AnteHandle(ctx, mockTx{msgs: []sdk.Msg{req}}, false, func(sdk.Context, sdk.Tx, bool) (sdk.Context, error) { - return ctx, nil - }) - require.Nil(t, err, "EVMFeeCheckDecorator should pass") - - k.BeginBlock(ctx) - - nonceBefore := k.GetNonce(ctx, evmAddr) - _, err = msgServer.EVMTransaction(sdk.WrapSDKContext(ctx), req) - require.NotNil(t, err) - require.Contains(t, err.Error(), "floor data gas") - - // Fire the deliverTxCallback the SDK runs post-DeliverTx — this is where - // BasicDecorator's nonce bump happens in production. - if cb := ctx.DeliverTxCallback(); cb != nil { - cb(ctx) - } - nonceAfter := k.GetNonce(ctx, evmAddr) - require.Equal(t, nonceBefore+1, nonceAfter, "nonce must be bumped post-DeliverTx for the receipt rule to allow a receipt") - require.True(t, k.GetNonceBumped(ctx, uint32(ctx.TxIndex())), "SetNonceBumped must be set so EndBlock writes the synthetic receipt") - - // Simulate the rest of FinalizeBlock (app.go:1787-1802). - txRes := &abci.ExecTxResult{Code: 1, Log: err.Error(), GasWanted: int64(gasLimit)} //nolint:gosec - k.SetTxResults([]*abci.ExecTxResult{txRes}) - k.SetMsgs([]*types.MsgEVMTransaction{req}) - k.EndBlock(ctx, ctx.BlockHeight(), 0) - - // Hypothesis: the receipt should be created by EndBlock's synthetic path. - txHash := tx.Hash() - receipt, rerr := k.GetTransientReceipt(ctx, txHash, uint64(ctx.TxIndex())) - require.Nil(t, rerr, "EndBlock should have written a synthetic receipt for the floor-data-gas tx (nonce was bumped)") - require.NotNil(t, receipt) - require.Equal(t, txHash.Hex(), receipt.TxHashHex) - require.Contains(t, receipt.VmError, "floor data gas", "synthetic receipt should carry the err.Error() captured in txRes.Log") -} - func TestEVMDynamicFeeTransaction(t *testing.T) { k, ctx := testkeeper.MockEVMKeeper(t) code, err := os.ReadFile("../../../example/contracts/simplestorage/SimpleStorage.bin") From 0a9d027cce8247d2fd12123d5ab9ee5bac1b8e12 Mon Sep 17 00:00:00 2001 From: Wen Date: Wed, 6 May 2026 15:16:52 -0700 Subject: [PATCH 9/9] test(giga): drop stale V2-test reference; explain V2-vs-Giga asymmetry MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit PR review feedback (arajasek): the doc comment on TestGiga_FailedExecution_ProducesReceipt still pointed at TestEVMTransactionStateTransitionErrorProducesReceipt as the V2-side counterpart, but the prior commit (a88912528) deleted that test along with the redundant V2 WriteReceipt. Replace the stale reference with the actual reason V2 doesn't need a test: EndBlock's synthetic-receipt path (GetAllEVMTxDeferredInfo + GetNonceBumped) already covers V2 state-transition errors. Giga doesn't because its execErr branch passes an empty Error string to AppendToEvmTxDeferredInfo, so the EndBlock fallback never fires for it — hence the explicit WriteReceipt this test guards. Co-Authored-By: Claude Opus 4.7 (1M context) --- giga/tests/giga_test.go | 11 ++++++++--- 1 file changed, 8 insertions(+), 3 deletions(-) diff --git a/giga/tests/giga_test.go b/giga/tests/giga_test.go index 4452c55404..986316a6e2 100644 --- a/giga/tests/giga_test.go +++ b/giga/tests/giga_test.go @@ -1654,9 +1654,14 @@ func TestGiga_FeeValidationOrder_WrongNonce_NoNonceBump(t *testing.T) { // // Without the fix in app.go's executeEVMTxWithGigaExecutor, the receipt was dropped // for these "should not happen" cases, so eth_getTransactionReceipt returned null -// forever for an included tx, hanging any client that polls. This is the Giga-path -// counterpart to TestEVMTransactionStateTransitionErrorProducesReceipt in -// x/evm/keeper/msg_server_test.go (which covers the matching V2 fix). +// forever for an included tx, hanging any client that polls. +// +// V2 doesn't need a counterpart fix: x/evm/keeper/abci.go EndBlock already writes a +// synthetic receipt for state-transition errors via GetAllEVMTxDeferredInfo + +// GetNonceBumped, since BasicDecorator.WithDeliverTxCallback bumps the nonce + calls +// SetNonceBumped on every DeliverTx. Giga's failure branch doesn't propagate +// deferredInfo.Error, so EndBlock's synthetic-receipt fallback doesn't fire — hence +// the explicit WriteReceipt in app.go. func TestGiga_FailedExecution_ProducesReceipt(t *testing.T) { blockTime := time.Now() accts := utils.NewTestAccounts(3)