From ebae55d3d1aeaf8e099abd7d3aa3560b371fa648 Mon Sep 17 00:00:00 2001 From: Cursor Agent Date: Thu, 16 Jul 2026 14:07:03 +0000 Subject: [PATCH 1/2] fix(giga): fall back to v2 on execution errors Co-authored-by: Philip Su --- app/app.go | 63 +++------------------ giga/executor/utils/errors.go | 15 +++++ giga/tests/giga_test.go | 102 +++++++++++++++++----------------- 3 files changed, 74 insertions(+), 106 deletions(-) diff --git a/app/app.go b/app/app.go index caea548446..ee5f8be1de 100644 --- a/app/app.go +++ b/app/app.go @@ -2016,6 +2016,7 @@ func (app *App) ProcessBlock(ctx sdk.Context, txs [][]byte, req *BlockProcessReq // Fallback-cause labels for the app_giga_fallback_to_v2 metric. const ( gigaFallbackValidationFailed = "validation_failed" + gigaFallbackExecutionFailed = "execution_failed" gigaFallbackBalanceMigration = "balance_migration" gigaFallbackSelfDestruct = "self_destruct" gigaFallbackInvalidPrecompile = "invalid_precompile" @@ -2030,6 +2031,8 @@ func gigaFallbackReason(err error) string { switch err.(type) { case *gigautils.ValidationFailedAbortError: return gigaFallbackValidationFailed + case *gigautils.ExecutionFailedAbortError: + return gigaFallbackExecutionFailed case *gigaprecompiles.BalanceMigrationAbortError: return gigaFallbackBalanceMigration case *gigaprecompiles.SelfDestructAbortError: @@ -2126,61 +2129,11 @@ func (app *App) executeEVMTxWithGigaExecutor(ctx sdk.Context, msg *evmtypes.MsgE } if execErr != nil { - // Match V2 error handling: bump nonce, commit fee deduction, track surplus - stateDB.SetNonce(sender, stateDB.GetNonce(sender)+1, tracing.NonceChangeEoACall) - surplus, ferr := stateDB.Finalize() - if ferr != nil { - // stateDB.Finalize is not expected to fail in practice. If it - // does, the nonce bump above may not have been persisted, so per - // the receipt-iff-nonce-bumped invariant we cannot claim the tx - // happened: skip the receipt + deferred-info writes and return. - logger.Error("giga: failed to finalize stateDB on consensus error", - "tx-hash", ethTx.Hash(), - "error", ferr, - ) - return &abci.ExecTxResult{ - Code: 1, - GasWanted: int64(ethTx.Gas()), //nolint:gosec - Log: fmt.Sprintf("giga: failed to finalize stateDB on consensus error: %v", ferr), - }, nil - } - - // Receipt-iff-nonce-bumped invariant: this tx bumped the sender's - // nonce on the line above, so it 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). Without an explicit WriteReceipt the receipt - // store stays empty for this tx hash — Giga's - // AppendToEvmTxDeferredInfo call below doesn't propagate the - // error, so EndBlock's synthetic-receipt path skips it — and - // 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) - - return &abci.ExecTxResult{ - Code: 1, - GasWanted: int64(ethTx.Gas()), //nolint:gosec - Log: fmt.Sprintf("giga executor apply message error: %v", execErr), - }, nil + // State-transition failures are rare and their canonical fee, nonce, + // receipt, and ABCI result behavior lives in v2. The stateDB has not + // been finalized, so its transaction-local cache is discarded by + // Cleanup before the caller re-runs the transaction through v2. + return nil, gigautils.ErrExecutionFailed } // Check if the execution hit a fail-fast precompile (Cosmos interop detected) diff --git a/giga/executor/utils/errors.go b/giga/executor/utils/errors.go index b42276fad0..3f7529fbcf 100644 --- a/giga/executor/utils/errors.go +++ b/giga/executor/utils/errors.go @@ -27,6 +27,21 @@ func (e *ValidationFailedAbortError) IsAbortError() bool { var ErrValidationFailed error = &ValidationFailedAbortError{} +// ExecutionFailedAbortError signals a state-transition error from geth's +// Execute path. V2 owns the canonical fee, nonce, receipt, and ABCI result +// handling for these failures, so the caller should fall back to v2. +type ExecutionFailedAbortError struct{} + +func (e *ExecutionFailedAbortError) Error() string { + return "EVM execution failed; v2 produces the canonical failure result" +} + +func (e *ExecutionFailedAbortError) IsAbortError() bool { + return true +} + +var ErrExecutionFailed error = &ExecutionFailedAbortError{} + // ShouldExecutionAbort checks if the given error is an AbortError that should // cause Giga execution to abort and fall back to standard execution. func ShouldExecutionAbort(err error) bool { diff --git a/giga/tests/giga_test.go b/giga/tests/giga_test.go index 11210b1e21..48f9e95fb7 100644 --- a/giga/tests/giga_test.go +++ b/giga/tests/giga_test.go @@ -1892,45 +1892,26 @@ 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 locks in the receipt-iff-nonce-bumped -// invariant for the Giga state-transition-error path: a tx that fails inside -// go-ethereum's Execute() (notably EIP-7623 floor-data-gas insufficient, which -// post-Pectra fires in normal operation) bumps the sender's nonce in -// executeEVMTxWithGigaExecutor's execErr branch and therefore must produce a -// status=0 receipt. The test asserts both the nonce bump and the receipt write. -// -// Without the explicit WriteReceipt in app.go, the receipt was dropped for this -// case — Giga's AppendToEvmTxDeferredInfo call doesn't propagate the error, so -// EndBlock's synthetic-receipt fallback (gated on GetNonceBumped) doesn't fire — -// and eth_getTransactionReceipt returned null forever for a nonce-bumping tx, -// hanging any client that polls. -// -// V2 doesn't need a counterpart fix: BasicDecorator.WithDeliverTxCallback bumps -// the nonce + calls SetNonceBumped on every DeliverTx, and EndBlock's synthetic- -// receipt path (x/evm/keeper/abci.go) writes a receipt via GetAllEVMTxDeferredInfo's -// txRes.Log fallback, gated on GetNonceBumped — together implementing the same -// invariant on the V2 path. -func TestGiga_FailedExecution_ProducesReceipt(t *testing.T) { +// TestGiga_FailedExecutionFallsBackToV2 verifies that state-transition errors +// are handled identically by v2, sequential Giga, and OCC Giga. EIP-7623's +// floor-data-gas check happens inside go-ethereum's Execute() after Sei's ante +// checks, so it exercises the execution-error fallback rather than validation. +func TestGiga_FailedExecutionFallsBackToV2(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. + // go-ethereum's floor-data-gas check inside Execute() (<31000). 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), + Nonce: 0, GasFeeCap: big.NewInt(100000000000), GasTipCap: big.NewInt(100000000000), Gas: gasLimit, @@ -1951,32 +1932,51 @@ 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) + type outcome struct { + ctx *GigaTestContext + results []*abci.ExecTxResult + nonce uint64 + receipt *types.Receipt + usei sdk.Int + wei sdk.Int + } + run := func(mode ExecutorMode) outcome { + testCtx := NewGigaTestContext(t, accts, blockTime, 1, mode) + fundAccount(t, testCtx, signer.AccountAddress, new(big.Int).Mul(big.NewInt(1e18), big.NewInt(1000))) + testCtx.TestApp.EvmKeeper.SetAddressMapping(testCtx.Ctx, signer.AccountAddress, signer.EvmAddress) + + _, results, runErr := RunBlock(t, testCtx, [][]byte{txBytes}) + require.NoError(t, runErr) + require.Len(t, results, 1) + require.NotEqual(t, uint32(0), results[0].Code) + require.Contains(t, results[0].Log, "floor data gas") + + receipt, receiptErr := testCtx.GetTransientReceipt(t, signedTx.Hash(), 0) + require.NoError(t, receiptErr) + require.Equal(t, uint32(ethtypes.ReceiptStatusFailed), receipt.Status) + + return outcome{ + ctx: testCtx, + results: results, + nonce: testCtx.TestApp.EvmKeeper.GetNonce(testCtx.Ctx, signer.EvmAddress), + receipt: receipt, + usei: testCtx.TestApp.BankKeeper.GetBalance(testCtx.Ctx, signer.AccountAddress, "usei").Amount, + wei: testCtx.TestApp.BankKeeper.GetWeiBalance(testCtx.Ctx, signer.AccountAddress), + } + } - 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 (matching the failed-tx convention used elsewhere in Sei)") - 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") + v2 := run(ModeV2Sequential) + require.Equal(t, uint64(1), v2.nonce) + for _, mode := range []ExecutorMode{ModeGigaSequential, ModeGigaOCC} { + got := run(mode) + name := mode.String() + CompareDeterministicFields(t, name, v2.results, got.results) + CompareLastResultsHash(t, name, v2.results, got.results) + assertReceiptsEqual(t, name, 0, v2.ctx.Mode, got.ctx.Mode, v2.receipt, got.receipt) + require.Equal(t, v2.nonce, got.nonce, "%s: nonce", name) + require.True(t, v2.usei.Equal(got.usei), "%s: usei balance", name) + require.True(t, v2.wei.Equal(got.wei), "%s: wei balance", name) + } } // TestGigaVsGeth_FeeValidationOrder compares Giga and Geth nonce bump behavior. From 6ab6c68c48a18688fe98c72ee9a45a4d869c6a63 Mon Sep 17 00:00:00 2001 From: Cursor Agent Date: Thu, 16 Jul 2026 15:13:28 +0000 Subject: [PATCH 2/2] test(giga): cover fallback fee destinations Co-authored-by: FromTheRain --- app/app.go | 9 ++++---- giga/tests/giga_test.go | 51 +++++++++++++++++++++++++++-------------- 2 files changed, 39 insertions(+), 21 deletions(-) diff --git a/app/app.go b/app/app.go index ee5f8be1de..fe274d4923 100644 --- a/app/app.go +++ b/app/app.go @@ -2129,10 +2129,11 @@ func (app *App) executeEVMTxWithGigaExecutor(ctx sdk.Context, msg *evmtypes.MsgE } if execErr != nil { - // State-transition failures are rare and their canonical fee, nonce, - // receipt, and ABCI result behavior lives in v2. The stateDB has not - // been finalized, so its transaction-local cache is discarded by - // Cleanup before the caller re-runs the transaction through v2. + // EIP-7623's floor-data-gas check is the known natural failure here: + // other Execute() failure classes are pre-checked upstream. Canonical + // fee, nonce, receipt, and ABCI result behavior lives in v2. The + // stateDB has not been finalized, so Cleanup discards its + // transaction-local cache before the caller re-runs the tx through v2. return nil, gigautils.ErrExecutionFailed } diff --git a/giga/tests/giga_test.go b/giga/tests/giga_test.go index 48f9e95fb7..2377f088f9 100644 --- a/giga/tests/giga_test.go +++ b/giga/tests/giga_test.go @@ -3,9 +3,6 @@ package giga_test import ( "bytes" "fmt" - - "github.com/sei-protocol/sei-chain/sei-tendermint/crypto/merkle" - "math/big" "testing" "time" @@ -17,9 +14,12 @@ import ( "github.com/sei-protocol/sei-chain/occ_tests/utils" "github.com/sei-protocol/sei-chain/sei-cosmos/baseapp" sdk "github.com/sei-protocol/sei-chain/sei-cosmos/types" + authtypes "github.com/sei-protocol/sei-chain/sei-cosmos/x/auth/types" abci "github.com/sei-protocol/sei-chain/sei-tendermint/abci/types" + "github.com/sei-protocol/sei-chain/sei-tendermint/crypto/merkle" tmproto "github.com/sei-protocol/sei-chain/sei-tendermint/proto/tendermint/types" "github.com/sei-protocol/sei-chain/x/evm/config" + evmstate "github.com/sei-protocol/sei-chain/x/evm/state" "github.com/sei-protocol/sei-chain/x/evm/types" "github.com/sei-protocol/sei-chain/x/evm/types/ethtx" "github.com/stretchr/testify/require" @@ -1932,13 +1932,18 @@ func TestGiga_FailedExecutionFallsBackToV2(t *testing.T) { txBytes, err := tc.TxEncoder()(txBuilder.GetTx()) require.NoError(t, err) + type balance struct { + usei sdk.Int + wei sdk.Int + } type outcome struct { - ctx *GigaTestContext - results []*abci.ExecTxResult - nonce uint64 - receipt *types.Receipt - usei sdk.Int - wei sdk.Int + ctx *GigaTestContext + results []*abci.ExecTxResult + nonce uint64 + receipt *types.Receipt + sender balance + feeCollector balance + coinbase balance } run := func(mode ExecutorMode) outcome { testCtx := NewGigaTestContext(t, accts, blockTime, 1, mode) @@ -1955,16 +1960,27 @@ func TestGiga_FailedExecutionFallsBackToV2(t *testing.T) { require.NoError(t, receiptErr) require.Equal(t, uint32(ethtypes.ReceiptStatusFailed), receipt.Status) + getBalance := func(address sdk.AccAddress) balance { + return balance{ + usei: testCtx.TestApp.BankKeeper.GetBalance(testCtx.Ctx, address, "usei").Amount, + wei: testCtx.TestApp.BankKeeper.GetWeiBalance(testCtx.Ctx, address), + } + } return outcome{ - ctx: testCtx, - results: results, - nonce: testCtx.TestApp.EvmKeeper.GetNonce(testCtx.Ctx, signer.EvmAddress), - receipt: receipt, - usei: testCtx.TestApp.BankKeeper.GetBalance(testCtx.Ctx, signer.AccountAddress, "usei").Amount, - wei: testCtx.TestApp.BankKeeper.GetWeiBalance(testCtx.Ctx, signer.AccountAddress), + ctx: testCtx, + results: results, + nonce: testCtx.TestApp.EvmKeeper.GetNonce(testCtx.Ctx, signer.EvmAddress), + receipt: receipt, + sender: getBalance(signer.AccountAddress), + feeCollector: getBalance(testCtx.TestApp.AccountKeeper.GetModuleAddress(authtypes.FeeCollectorName)), + coinbase: getBalance(evmstate.GetCoinbaseAddress(0)), } } + assertBalanceEqual := func(name, account string, want, got balance) { + require.True(t, want.usei.Equal(got.usei), "%s: %s usei balance", name, account) + require.True(t, want.wei.Equal(got.wei), "%s: %s wei balance", name, account) + } v2 := run(ModeV2Sequential) require.Equal(t, uint64(1), v2.nonce) for _, mode := range []ExecutorMode{ModeGigaSequential, ModeGigaOCC} { @@ -1974,8 +1990,9 @@ func TestGiga_FailedExecutionFallsBackToV2(t *testing.T) { CompareLastResultsHash(t, name, v2.results, got.results) assertReceiptsEqual(t, name, 0, v2.ctx.Mode, got.ctx.Mode, v2.receipt, got.receipt) require.Equal(t, v2.nonce, got.nonce, "%s: nonce", name) - require.True(t, v2.usei.Equal(got.usei), "%s: usei balance", name) - require.True(t, v2.wei.Equal(got.wei), "%s: wei balance", name) + assertBalanceEqual(name, "sender", v2.sender, got.sender) + assertBalanceEqual(name, "fee collector", v2.feeCollector, got.feeCollector) + assertBalanceEqual(name, "tx coinbase", v2.coinbase, got.coinbase) } }