Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
26 changes: 26 additions & 0 deletions app/app.go
Original file line number Diff line number Diff line change
Expand Up @@ -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: 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)

Expand Down
72 changes: 72 additions & 0 deletions giga/tests/giga_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -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(&ethtypes.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()
Expand Down
19 changes: 19 additions & 0 deletions x/evm/keeper/msg_server.go
Original file line number Diff line number Diff line change
Expand Up @@ -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()
Expand Down
100 changes: 100 additions & 0 deletions x/evm/keeper/msg_server_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -238,6 +238,106 @@ 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.
//
// 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()
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)
// 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)

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")

// 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) {
k, ctx := testkeeper.MockEVMKeeper(t)
code, err := os.ReadFile("../../../example/contracts/simplestorage/SimpleStorage.bin")
Expand Down
Loading