diff --git a/app/app.go b/app/app.go index 7bd86810e2..318f117c9e 100644 --- a/app/app.go +++ b/app/app.go @@ -1900,6 +1900,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: effectiveGasPrice, // EIP-1559 effective gas price (not GasFeeCap) + 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/giga/tests/giga_test.go b/giga/tests/giga_test.go index 27aa3853f8..986316a6e2 100644 --- a/giga/tests/giga_test.go +++ b/giga/tests/giga_test.go @@ -1646,6 +1646,91 @@ 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. +// +// 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) + 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) + + 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) + + 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() + 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()