From d987428bc0e2fc11b17b10f742a193ffa1b9107d Mon Sep 17 00:00:00 2001 From: Aayush Date: Wed, 4 Mar 2026 10:25:12 -0500 Subject: [PATCH 1/2] fix(giga): match v2 correctness checks --- app/app.go | 424 +++++++++++++++------ giga/tests/giga_test.go | 796 ++++++++++++++++++++++++++++++++++++++++ 2 files changed, 1100 insertions(+), 120 deletions(-) diff --git a/app/app.go b/app/app.go index e9e645b2f9..ed7704b9bb 100644 --- a/app/app.go +++ b/app/app.go @@ -19,6 +19,7 @@ import ( "sync" "time" + "github.com/ethereum/go-ethereum/common" "github.com/ethereum/go-ethereum/core" "github.com/ethereum/go-ethereum/core/tracing" ethtypes "github.com/ethereum/go-ethereum/core/types" @@ -105,6 +106,7 @@ import ( "github.com/gogo/protobuf/proto" "github.com/gorilla/mux" "github.com/rakyll/statik/fs" + appante "github.com/sei-protocol/sei-chain/app/ante" "github.com/sei-protocol/sei-chain/app/antedecorators" "github.com/sei-protocol/sei-chain/app/benchmark" "github.com/sei-protocol/sei-chain/app/legacyabci" @@ -178,7 +180,6 @@ import ( gigabankkeeper "github.com/sei-protocol/sei-chain/giga/deps/xbank/keeper" gigaevmkeeper "github.com/sei-protocol/sei-chain/giga/deps/xevm/keeper" gigaevmstate "github.com/sei-protocol/sei-chain/giga/deps/xevm/state" - "github.com/sei-protocol/sei-chain/giga/deps/xevm/types/ethtx" ) // this line is used by starport scaffolding # stargate/wasm/app/enabledProposals @@ -1434,8 +1435,36 @@ func (app *App) ProcessTxsSynchronousGiga(ctx sdk.Context, txs [][]byte, typedTx continue } - // Execute EVM transaction through giga executor - result, execErr := app.executeEVMTxWithGigaExecutor(ctx, evmMsg, cache) + // Validate Cosmos SDK envelope (memo, timeoutHeight, signerInfos, etc.) + // This prevents consensus divergence if a malicious proposer includes invalid envelope fields. + if err := appante.EvmStatelessChecks(ctx, typedTxs[i], cache.chainID); err != nil { + codespace, code, log := sdkerrors.ABCIInfo(err, false) + txResults[i] = &abci.ExecTxResult{ + Codespace: codespace, + Code: code, + Log: log, + } + ms.Write() + continue + } + + // Execute EVM transaction through giga executor with panic recovery + // (matches V2's recover behavior in legacyabci/deliver_tx.go) + var result *abci.ExecTxResult + var execErr error + func() { + defer func() { + if r := recover(); r != nil { + logger.Error("panic in giga synchronous executor", "panic", r, "stack", string(debug.Stack())) + result = &abci.ExecTxResult{ + Code: sdkerrors.ErrPanic.ABCICode(), + Log: fmt.Sprintf("panic recovered: %v", r), + } + } + }() + result, execErr = app.executeEVMTxWithGigaExecutor(ctx, evmMsg, cache) + }() + if execErr != nil { // Check if this is a fail-fast error (Cosmos precompile interop detected) if gigautils.ShouldExecutionAbort(execErr) { @@ -1758,8 +1787,8 @@ func (app *App) ProcessBlock(ctx sdk.Context, txs [][]byte, req BlockProcessRequ // The sender address is recovered directly from the transaction signature - no Cosmos SDK ante handlers needed. func (app *App) executeEVMTxWithGigaExecutor(ctx sdk.Context, msg *evmtypes.MsgEVMTransaction, cache *gigaBlockCache) (*abci.ExecTxResult, error) { // Get the Ethereum transaction from the message - ethTx, txData := msg.AsTransaction() - if ethTx == nil || txData == nil { + ethTx, _ := msg.AsTransaction() + if ethTx == nil { return nil, fmt.Errorf("failed to convert to eth transaction") } @@ -1776,122 +1805,25 @@ func (app *App) executeEVMTxWithGigaExecutor(ctx sdk.Context, msg *evmtypes.MsgE _, isAssociated := app.GigaEvmKeeper.GetEVMAddress(ctx, seiAddr) - // ============================================================================ - // Nonce validation (mirrors V2's ante handler check in x/evm/ante/sig.go) - // V2 rejects with ErrWrongSequence if txNonce != expectedNonce, with NO state changes. - // ============================================================================ - expectedNonce := app.GigaEvmKeeper.GetNonce(ctx, sender) - txNonce := ethTx.Nonce() - if txNonce != expectedNonce { - nonceDirection := "too high" - if txNonce < expectedNonce { - nonceDirection = "too low" - } - return &abci.ExecTxResult{ - Code: sdkerrors.ErrWrongSequence.ABCICode(), - GasWanted: int64(ethTx.Gas()), //nolint:gosec - Log: fmt.Sprintf("nonce %s: address %s, tx: %d state: %d: %s", nonceDirection, sender.Hex(), txNonce, expectedNonce, sdkerrors.ErrWrongSequence.Error()), - }, nil - } - - // ============================================================================ - // Fee validation (mirrors V2's ante handler checks in evm_checktx.go) - // NOTE: In V2, failed transactions still increment nonce and charge gas. - // We track validation errors here but don't return early - we still need to - // create stateDB, increment nonce, and finalize state to match V2 behavior. - // ============================================================================ - baseFee := app.GigaEvmKeeper.GetBaseFee(ctx) - if baseFee == nil { - baseFee = new(big.Int) // default to 0 when base fee is unset - } - - // Track validation errors - we'll skip execution but still finalize state - var validationErr *abci.ExecTxResult - - // 1. Fee cap < base fee check (INSUFFICIENT_MAX_FEE_PER_GAS) - // V2: evm_checktx.go line 284-286 - if txData.GetGasFeeCap().Cmp(baseFee) < 0 { - validationErr = &abci.ExecTxResult{ - Code: sdkerrors.ErrInsufficientFee.ABCICode(), - Log: "max fee per gas less than block base fee", - } - } - - // 2. Tip > fee cap check (PRIORITY_GREATER_THAN_MAX_FEE_PER_GAS) - // This is checked in txData.Validate() for DynamicFeeTx, but we also check here - // to ensure consistent rejection before execution. - if validationErr == nil && txData.GetGasTipCap().Cmp(txData.GetGasFeeCap()) > 0 { - validationErr = &abci.ExecTxResult{ - Code: 1, - Log: "max priority fee per gas higher than max fee per gas", - } - } - - // 3. Gas limit * gas price overflow check (GASLIMIT_PRICE_PRODUCT_OVERFLOW) - // V2: Uses IsValidInt256(tx.Fee()) in dynamic_fee_tx.go Validate() - // Fee = GasFeeCap * GasLimit, must fit in 256 bits - if validationErr == nil && !ethtx.IsValidInt256(txData.Fee()) { - validationErr = &abci.ExecTxResult{ - Code: 1, - Log: "fee out of bound", - } - } - - // 4. TX gas limit > block gas limit check (GAS_ALLOWANCE_EXCEEDED) - // V2: x/evm/ante/basic.go lines 63-68 - if validationErr == nil { - if cp := ctx.ConsensusParams(); cp != nil && cp.Block != nil { - if cp.Block.MaxGas > 0 && ethTx.Gas() > uint64(cp.Block.MaxGas) { //nolint:gosec - validationErr = &abci.ExecTxResult{ - Code: sdkerrors.ErrOutOfGas.ABCICode(), - Log: fmt.Sprintf("tx gas limit %d exceeds block max gas %d", ethTx.Gas(), cp.Block.MaxGas), - } - } - } - } - - // 5. Insufficient balance check for gas * price + value (INSUFFICIENT_FUNDS_FOR_TRANSFER) - if validationErr == nil { - // BuyGas checks balance against GasLimit * GasFeeCap + Value (see go-ethereum/core/state_transition.go:264-291) - balanceCheck := new(big.Int).Mul(new(big.Int).SetUint64(ethTx.Gas()), ethTx.GasFeeCap()) - balanceCheck.Add(balanceCheck, ethTx.Value()) - - senderBalance := app.GigaEvmKeeper.GetBalance(ctx, seiAddr) - - // For unassociated addresses, V2's PreprocessDecorator migrates the cast address balance - // BEFORE the fee check (in a CacheMultiStore). We need to include the cast address balance - // in our check to match V2's behavior, even though we defer the actual migration. - if !isAssociated { - // Cast address is the EVM address bytes interpreted as a Sei address - castAddr := sdk.AccAddress(sender[:]) - castBalance := app.GigaEvmKeeper.GetBalance(ctx, castAddr) - senderBalance = new(big.Int).Add(senderBalance, castBalance) - } - - if senderBalance.Cmp(balanceCheck) < 0 { - validationErr = &abci.ExecTxResult{ - Code: sdkerrors.ErrInsufficientFunds.ABCICode(), - Log: fmt.Sprintf("insufficient funds for gas * price + value: address %s have %v want %v: insufficient funds", sender.Hex(), senderBalance, balanceCheck), - } - } - } + // Run validation checks (fee/nonce/balance - stateless checks done earlier) + validation := app.validateGigaEVMTx(ctx, ethTx, sender, seiAddr, isAssociated) // Prepare context for EVM transaction (set infinite gas meter like original flow) ctx = ctx.WithGasMeter(sdk.NewInfiniteGasMeterWithMultiplier(ctx)) - // If validation failed, increment nonce via keeper (matching V2's DeliverTxCallback behavior - // in x/evm/ante/basic.go). V2 does NOT create stateDB or handle surplus for early failures. - if validationErr != nil { - // Match V2 error handling: bump nonce directly via keeper (not stateDB) - currentNonce := app.GigaEvmKeeper.GetNonce(ctx, sender) - app.GigaEvmKeeper.SetNonce(ctx, sender, currentNonce+1) - + if validation.err != nil { + // Validation failed - bump nonce via keeper if it was valid (matches V2's DeliverTxCallback + // behavior where nonce is incremented even on fee validation failures). + // For successful txs, the nonce is bumped by the EVM during execution. + if validation.bumpNonce { + app.GigaEvmKeeper.SetNonce(ctx, sender, validation.currentNonce+1) + } // V2 reports intrinsic gas as gasUsed even on validation failure (for metrics), // but no actual balance is deducted intrinsicGas, _ := core.IntrinsicGas(ethTx.Data(), ethTx.AccessList(), ethTx.SetCodeAuthorizations(), ethTx.To() == nil, true, true, true) - validationErr.GasUsed = int64(intrinsicGas) //nolint:gosec - validationErr.GasWanted = int64(ethTx.Gas()) //nolint:gosec - return validationErr, nil + validation.err.GasUsed = int64(intrinsicGas) //nolint:gosec + validation.err.GasWanted = int64(ethTx.Gas()) //nolint:gosec + return validation.err, nil } if !isAssociated { @@ -1925,7 +1857,7 @@ func (app *App) executeEVMTxWithGigaExecutor(ctx sdk.Context, msg *evmtypes.MsgE // V2 charges fees in the ante handler, then runs the EVM with feeAlreadyCharged=true // which skips buyGas/refundGas/coinbase. Without this, GasUsed differs between Giga // and V2, causing LastResultsHash → AppHash divergence. - effectiveGasPrice := new(big.Int).Add(new(big.Int).Set(ethTx.GasTipCap()), baseFee) + effectiveGasPrice := new(big.Int).Add(new(big.Int).Set(ethTx.GasTipCap()), validation.baseFee) if effectiveGasPrice.Cmp(ethTx.GasFeeCap()) > 0 { effectiveGasPrice.Set(ethTx.GasFeeCap()) } @@ -2075,13 +2007,29 @@ func (app *App) executeEVMTxWithGigaExecutor(ctx sdk.Context, msg *evmtypes.MsgE // makeGigaDeliverTx returns an OCC-compatible deliverTx callback that captures the given // block cache, avoiding mutable state on App for cache lifecycle management. func (app *App) makeGigaDeliverTx(cache *gigaBlockCache) func(sdk.Context, abci.RequestDeliverTxV2, sdk.Tx, [32]byte) abci.ResponseDeliverTx { - return func(ctx sdk.Context, req abci.RequestDeliverTxV2, tx sdk.Tx, checksum [32]byte) abci.ResponseDeliverTx { + return func(ctx sdk.Context, req abci.RequestDeliverTxV2, tx sdk.Tx, checksum [32]byte) (resp abci.ResponseDeliverTx) { defer func() { if r := recover(); r != nil { - // OCC abort panics are expected - the scheduler uses them to detect conflicts - // and reschedule transactions. Don't log these as errors. - if _, isOCCAbort := r.(occ.Abort); !isOCCAbort { - logger.Error("benchmark panic in gigaDeliverTx", "panic", r, "stack", string(debug.Stack())) + // Handle panics as v2 does: ErrOCCAbort, ErrOutOfGas, or ErrPanic + if abort, isOCCAbort := r.(occ.Abort); isOCCAbort { + resp = abci.ResponseDeliverTx{ + Code: sdkerrors.ErrOCCAbort.ABCICode(), + Log: fmt.Sprintf("occ abort occurred with dependent index %d and error: %v", abort.DependentTxIdx, abort.Err), + } + return + } + if oogErr, isOOG := r.(sdk.ErrorOutOfGas); isOOG { + resp = abci.ResponseDeliverTx{ + Code: sdkerrors.ErrOutOfGas.ABCICode(), + Log: fmt.Sprintf("out of gas in location: %v", oogErr.Descriptor), + } + return + } + // For other panics (e.g., nil deref from malformed protobuf), log and return ErrPanic + logger.Error("panic in gigaDeliverTx", "panic", r, "stack", string(debug.Stack())) + resp = abci.ResponseDeliverTx{ + Code: sdkerrors.ErrPanic.ABCICode(), + Log: fmt.Sprintf("recovered: %v\nstack:\n%v", r, string(debug.Stack())), } } }() @@ -2091,6 +2039,17 @@ func (app *App) makeGigaDeliverTx(cache *gigaBlockCache) func(sdk.Context, abci. return abci.ResponseDeliverTx{Code: 1, Log: "not an EVM transaction"} } + // Validate Cosmos SDK envelope (memo, timeoutHeight, signerInfos, etc.) + // This prevents consensus divergence if a malicious proposer includes invalid envelope fields. + if err := appante.EvmStatelessChecks(ctx, tx, cache.chainID); err != nil { + codespace, code, log := sdkerrors.ABCIInfo(err, false) + return abci.ResponseDeliverTx{ + Codespace: codespace, + Code: code, + Log: log, + } + } + result, err := app.executeEVMTxWithGigaExecutor(ctx, evmMsg, cache) if err != nil { // Check if this is a fail-fast error (Cosmos precompile interop detected) @@ -2603,6 +2562,231 @@ func (app *App) inplacetestnetInitializer(pk cryptotypes.PubKey) error { return nil } +// gigaValidationResult holds the result of EVM transaction validation. +type gigaValidationResult struct { + err *abci.ExecTxResult // nil if validation passed + bumpNonce bool // true if tx nonce matches expected nonce + currentNonce uint64 // the expected nonce at time of validation + baseFee *big.Int // the base fee used for validation +} + +// validateGigaEVMTx validates an EVM tx for fee, nonce, and stateless checks. +// Note: Cosmos envelope checks (chain ID, intrinsic gas, etc.) are done earlier via EvmStatelessChecks. +// +// This function handles checks from V2's EVMFeeCheckDecorator + go-ethereum's StatelessChecks: +// 1. Fee cap checks +// 2. Nonce validity (including overflow guard) +// 3. Sender EOA check (unless delegated via EIP-7702) +// 4. Gas fee/tip cap bit length checks +// 5. Tip <= fee cap check +// 6. Set-code tx validation +// 7. Balance check +func (app *App) validateGigaEVMTx( + ctx sdk.Context, + ethTx *ethtypes.Transaction, + sender common.Address, + seiAddr sdk.AccAddress, + isAssociated bool, +) gigaValidationResult { + baseFee := app.GigaEvmKeeper.GetBaseFee(ctx) + if baseFee == nil { + baseFee = new(big.Int) + } + + // Check nonce validity - determines if we bump nonce on fee/balance failures + currentNonce := app.GigaEvmKeeper.GetNonce(ctx, sender) + txNonce := ethTx.Nonce() + bumpNonce := txNonce == currentNonce + + // Fee cap below base fee + if ethTx.GasFeeCap().Cmp(baseFee) < 0 { + return gigaValidationResult{ + err: &abci.ExecTxResult{ + Code: sdkerrors.ErrInsufficientFee.ABCICode(), + Log: "max fee per gas less than block base fee", + }, + bumpNonce: bumpNonce, + currentNonce: currentNonce, + baseFee: baseFee, + } + } + + // Fee cap below minimum fee + minimumFee := app.GigaEvmKeeper.GetMinimumFeePerGas(ctx).TruncateInt().BigInt() + if ethTx.GasFeeCap().Cmp(minimumFee) < 0 { + return gigaValidationResult{ + err: &abci.ExecTxResult{ + Code: sdkerrors.ErrInsufficientFee.ABCICode(), + Log: "max fee per gas less than minimum fee", + }, + bumpNonce: bumpNonce, + currentNonce: currentNonce, + baseFee: baseFee, + } + } + + // ======================================================================== + // go-ethereum StatelessChecks (matches V2's EVMFeeCheckDecorator call to st.StatelessChecks()) + // ======================================================================== + + // Nonce checks (too high, too low, overflow guard) + if txNonce > currentNonce { + return gigaValidationResult{ + err: &abci.ExecTxResult{ + Code: sdkerrors.ErrWrongSequence.ABCICode(), + Log: fmt.Sprintf("nonce too high: address %s, tx: %d state: %d", sender.Hex(), txNonce, currentNonce), + }, + bumpNonce: bumpNonce, + currentNonce: currentNonce, + baseFee: baseFee, + } + } + if txNonce < currentNonce { + return gigaValidationResult{ + err: &abci.ExecTxResult{ + Code: sdkerrors.ErrWrongSequence.ABCICode(), + Log: fmt.Sprintf("nonce too low: address %s, tx: %d state: %d", sender.Hex(), txNonce, currentNonce), + }, + bumpNonce: bumpNonce, + currentNonce: currentNonce, + baseFee: baseFee, + } + } + // Nonce overflow guard (currentNonce + 1 would overflow) + if currentNonce+1 < currentNonce { + return gigaValidationResult{ + err: &abci.ExecTxResult{ + Code: sdkerrors.ErrWrongSequence.ABCICode(), + Log: fmt.Sprintf("nonce max: address %s, nonce: %d", sender.Hex(), currentNonce), + }, + bumpNonce: bumpNonce, + currentNonce: currentNonce, + baseFee: baseFee, + } + } + + // Sender must be EOA unless delegated (EIP-7702) + senderCode := app.GigaEvmKeeper.GetCode(ctx, sender) + if len(senderCode) > 0 { + _, isDelegated := ethtypes.ParseDelegation(senderCode) + if !isDelegated { + return gigaValidationResult{ + err: &abci.ExecTxResult{ + Code: sdkerrors.ErrWrongSequence.ABCICode(), + Log: fmt.Sprintf("sender not an eoa: address %s, len(code): %d", sender.Hex(), len(senderCode)), + }, + bumpNonce: bumpNonce, + currentNonce: currentNonce, + baseFee: baseFee, + } + } + } + + // GasFeeCap bit length must be <= 256 + if l := ethTx.GasFeeCap().BitLen(); l > 256 { + return gigaValidationResult{ + err: &abci.ExecTxResult{ + Code: sdkerrors.ErrWrongSequence.ABCICode(), + Log: fmt.Sprintf("max fee per gas higher than 2^256-1: address %s, maxFeePerGas bit length: %d", sender.Hex(), l), + }, + bumpNonce: bumpNonce, + currentNonce: currentNonce, + baseFee: baseFee, + } + } + + // GasTipCap bit length must be <= 256 + if l := ethTx.GasTipCap().BitLen(); l > 256 { + return gigaValidationResult{ + err: &abci.ExecTxResult{ + Code: sdkerrors.ErrWrongSequence.ABCICode(), + Log: fmt.Sprintf("max priority fee per gas higher than 2^256-1: address %s, maxPriorityFeePerGas bit length: %d", sender.Hex(), l), + }, + bumpNonce: bumpNonce, + currentNonce: currentNonce, + baseFee: baseFee, + } + } + + // GasTipCap must be <= GasFeeCap + if ethTx.GasTipCap().Cmp(ethTx.GasFeeCap()) > 0 { + return gigaValidationResult{ + err: &abci.ExecTxResult{ + Code: sdkerrors.ErrWrongSequence.ABCICode(), + Log: fmt.Sprintf("max priority fee per gas higher than max fee per gas: address %s, maxPriorityFeePerGas: %s, maxFeePerGas: %s", sender.Hex(), ethTx.GasTipCap(), ethTx.GasFeeCap()), + }, + bumpNonce: bumpNonce, + currentNonce: currentNonce, + baseFee: baseFee, + } + } + + // Set-code tx (EIP-7702) validation + if ethTx.Type() == ethtypes.SetCodeTxType { + // Set-code tx must not be contract creation + if ethTx.To() == nil { + return gigaValidationResult{ + err: &abci.ExecTxResult{ + Code: sdkerrors.ErrWrongSequence.ABCICode(), + Log: fmt.Sprintf("set-code transaction must not be a create transaction: sender %s", sender.Hex()), + }, + bumpNonce: bumpNonce, + currentNonce: currentNonce, + baseFee: baseFee, + } + } + // Set-code tx auth list must be non-empty + if len(ethTx.SetCodeAuthorizations()) == 0 { + return gigaValidationResult{ + err: &abci.ExecTxResult{ + Code: sdkerrors.ErrWrongSequence.ABCICode(), + Log: fmt.Sprintf("set-code transaction with empty auth list: sender %s", sender.Hex()), + }, + bumpNonce: bumpNonce, + currentNonce: currentNonce, + baseFee: baseFee, + } + } + } + + // ======================================================================== + // Balance check (matches V2's st.BuyGas()) + // ======================================================================== + + // Insufficient balance for gas + value + balanceCheck := new(big.Int).Mul(new(big.Int).SetUint64(ethTx.Gas()), ethTx.GasFeeCap()) + balanceCheck.Add(balanceCheck, ethTx.Value()) + + senderBalance := app.GigaEvmKeeper.GetBalance(ctx, seiAddr) + + // Include cast address balance for unassociated addresses (matches V2 PreprocessDecorator) + if !isAssociated { + castAddr := sdk.AccAddress(sender[:]) + castBalance := app.GigaEvmKeeper.GetBalance(ctx, castAddr) + senderBalance = new(big.Int).Add(senderBalance, castBalance) + } + + if senderBalance.Cmp(balanceCheck) < 0 { + return gigaValidationResult{ + err: &abci.ExecTxResult{ + Code: sdkerrors.ErrInsufficientFunds.ABCICode(), + Log: fmt.Sprintf("insufficient funds for gas * price + value: address %s have %v want %v: insufficient funds", sender.Hex(), senderBalance, balanceCheck), + }, + bumpNonce: bumpNonce, + currentNonce: currentNonce, + baseFee: baseFee, + } + } + + // All checks passed + return gigaValidationResult{ + err: nil, + bumpNonce: bumpNonce, + currentNonce: currentNonce, + baseFee: baseFee, + } +} + func init() { // override max wasm size to 2MB wasmtypes.MaxWasmSize = 2 * 1024 * 1024 diff --git a/giga/tests/giga_test.go b/giga/tests/giga_test.go index 40da37ae8a..47a3e450b5 100644 --- a/giga/tests/giga_test.go +++ b/giga/tests/giga_test.go @@ -1529,3 +1529,799 @@ func TestGigaSequential_BalanceTransfer(t *testing.T) { t.Logf("Balance transfer verified: sender lost %s (transfer %s + gas %s), recipient gained %s", senderLost.String(), transferAmount.String(), gasCost.String(), recipientGained.String()) } + +// TestGiga_FeeValidationOrder_Phase1_GasLimitExceedsBlock verifies Phase 1 errors don't bump nonce. +func TestGiga_FeeValidationOrder_Phase1_GasLimitExceedsBlock(t *testing.T) { + blockTime := time.Now() + accts := utils.NewTestAccounts(3) + signer := utils.NewSigner() + recipient := utils.NewSigner() + + gigaCtx := NewGigaTestContext(t, accts, blockTime, 1, ModeGigaSequential) + + cp := gigaCtx.Ctx.ConsensusParams() + if cp == nil || cp.Block == nil || cp.Block.MaxGas <= 0 { + t.Skip("Test requires consensus params with MaxGas > 0") + } + + fundAccount(t, gigaCtx, signer.AccountAddress, new(big.Int).Mul(big.NewInt(1e18), big.NewInt(1000))) + + initialNonce := gigaCtx.TestApp.GigaEvmKeeper.GetNonce(gigaCtx.Ctx, signer.EvmAddress) + hugeGasLimit := uint64(cp.Block.MaxGas) + 1000000 + + to := recipient.EvmAddress + tx := createCustomEVMTx(t, gigaCtx, signer, &to, big.NewInt(1), hugeGasLimit, + big.NewInt(100000000000), big.NewInt(100000000000), initialNonce) + + _, results, err := RunBlock(t, gigaCtx, [][]byte{tx}) + require.NoError(t, err) + require.Len(t, results, 1) + require.NotEqual(t, uint32(0), results[0].Code) + + finalNonce := gigaCtx.TestApp.GigaEvmKeeper.GetNonce(gigaCtx.Ctx, signer.EvmAddress) + require.Equal(t, initialNonce, finalNonce, "Nonce should NOT be bumped for Phase 1 errors") +} + +// TestGiga_FeeValidationOrder_Phase3_FeeCapBelowBaseFee verifies Phase 3 errors DO bump nonce. +func TestGiga_FeeValidationOrder_Phase3_FeeCapBelowBaseFee(t *testing.T) { + blockTime := time.Now() + accts := utils.NewTestAccounts(3) + signer := utils.NewSigner() + recipient := utils.NewSigner() + + gigaCtx := NewGigaTestContext(t, accts, blockTime, 1, ModeGigaSequential) + fundAccount(t, gigaCtx, signer.AccountAddress, new(big.Int).Mul(big.NewInt(1e18), big.NewInt(1000))) + + baseFee := gigaCtx.TestApp.GigaEvmKeeper.GetBaseFee(gigaCtx.Ctx) + if baseFee == nil || baseFee.Sign() == 0 { + baseFee = big.NewInt(1000000000) + } + + lowFeeCap := new(big.Int).Sub(baseFee, big.NewInt(1)) + if lowFeeCap.Sign() < 0 { + lowFeeCap = big.NewInt(0) + } + + initialNonce := gigaCtx.TestApp.GigaEvmKeeper.GetNonce(gigaCtx.Ctx, signer.EvmAddress) + + to := recipient.EvmAddress + tx := createCustomEVMTx(t, gigaCtx, signer, &to, big.NewInt(1), 21000, lowFeeCap, lowFeeCap, initialNonce) + + _, results, err := RunBlock(t, gigaCtx, [][]byte{tx}) + require.NoError(t, err) + require.Len(t, results, 1) + require.NotEqual(t, uint32(0), results[0].Code) + + finalNonce := gigaCtx.TestApp.GigaEvmKeeper.GetNonce(gigaCtx.Ctx, signer.EvmAddress) + require.Equal(t, initialNonce+1, finalNonce, "Nonce SHOULD be bumped for Phase 3 errors with valid nonce") +} + +// TestGiga_FeeValidationOrder_Phase3_InsufficientBalance verifies insufficient balance bumps nonce. +func TestGiga_FeeValidationOrder_Phase3_InsufficientBalance(t *testing.T) { + blockTime := time.Now() + accts := utils.NewTestAccounts(3) + signer := utils.NewSigner() + recipient := utils.NewSigner() + + gigaCtx := NewGigaTestContext(t, accts, blockTime, 1, ModeGigaSequential) + fundAccount(t, gigaCtx, signer.AccountAddress, big.NewInt(1e12)) + + initialNonce := gigaCtx.TestApp.GigaEvmKeeper.GetNonce(gigaCtx.Ctx, signer.EvmAddress) + largeValue := new(big.Int).Mul(big.NewInt(1e18), big.NewInt(1000)) + + to := recipient.EvmAddress + tx := createCustomEVMTx(t, gigaCtx, signer, &to, largeValue, 21000, + big.NewInt(100000000000), big.NewInt(100000000000), initialNonce) + + _, results, err := RunBlock(t, gigaCtx, [][]byte{tx}) + require.NoError(t, err) + require.Len(t, results, 1) + require.NotEqual(t, uint32(0), results[0].Code) + + finalNonce := gigaCtx.TestApp.GigaEvmKeeper.GetNonce(gigaCtx.Ctx, signer.EvmAddress) + require.Equal(t, initialNonce+1, finalNonce, "Nonce SHOULD be bumped for Phase 3 errors with valid nonce") +} + +// TestGiga_FeeValidationOrder_WrongNonce_NoNonceBump verifies wrong nonce doesn't bump nonce. +func TestGiga_FeeValidationOrder_WrongNonce_NoNonceBump(t *testing.T) { + blockTime := time.Now() + accts := utils.NewTestAccounts(3) + signer := utils.NewSigner() + recipient := utils.NewSigner() + + gigaCtx := NewGigaTestContext(t, accts, blockTime, 1, ModeGigaSequential) + fundAccount(t, gigaCtx, signer.AccountAddress, new(big.Int).Mul(big.NewInt(1e18), big.NewInt(1000))) + + initialNonce := gigaCtx.TestApp.GigaEvmKeeper.GetNonce(gigaCtx.Ctx, signer.EvmAddress) + wrongNonce := initialNonce + 5 + + to := recipient.EvmAddress + tx := createCustomEVMTx(t, gigaCtx, signer, &to, big.NewInt(1), 21000, + big.NewInt(100000000000), big.NewInt(100000000000), wrongNonce) + + _, results, err := RunBlock(t, gigaCtx, [][]byte{tx}) + require.NoError(t, err) + require.Len(t, results, 1) + require.NotEqual(t, uint32(0), results[0].Code) + + finalNonce := gigaCtx.TestApp.GigaEvmKeeper.GetNonce(gigaCtx.Ctx, signer.EvmAddress) + require.Equal(t, initialNonce, finalNonce, "Nonce should NOT be bumped when tx nonce is wrong") +} + +// TestGigaVsGeth_FeeValidationOrder compares Giga and Geth nonce bump behavior. +func TestGigaVsGeth_FeeValidationOrder(t *testing.T) { + blockTime := time.Now() + accts := utils.NewTestAccounts(3) + signer := utils.NewSigner() + recipient := utils.NewSigner() + + // Geth + gethCtx := NewGigaTestContext(t, accts, blockTime, 1, ModeV2withOCC) + fundAccount(t, gethCtx, signer.AccountAddress, new(big.Int).Mul(big.NewInt(1e18), big.NewInt(1000))) + gethCtx.TestApp.EvmKeeper.SetAddressMapping(gethCtx.Ctx, signer.AccountAddress, signer.EvmAddress) + + gethInitialNonce := gethCtx.TestApp.EvmKeeper.GetNonce(gethCtx.Ctx, signer.EvmAddress) + + to := recipient.EvmAddress + gethTx := createCustomEVMTx(t, gethCtx, signer, &to, + new(big.Int).Mul(big.NewInt(1e18), big.NewInt(10000)), 21000, + big.NewInt(100000000000), big.NewInt(100000000000), gethInitialNonce) + + _, gethResults, err := RunBlock(t, gethCtx, [][]byte{gethTx}) + require.NoError(t, err) + require.Len(t, gethResults, 1) + + gethFinalNonce := gethCtx.TestApp.EvmKeeper.GetNonce(gethCtx.Ctx, signer.EvmAddress) + + // Giga + 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) + + gigaInitialNonce := gigaCtx.TestApp.GigaEvmKeeper.GetNonce(gigaCtx.Ctx, signer.EvmAddress) + + gigaTx := createCustomEVMTx(t, gigaCtx, signer, &to, + new(big.Int).Mul(big.NewInt(1e18), big.NewInt(10000)), 21000, + big.NewInt(100000000000), big.NewInt(100000000000), gigaInitialNonce) + + _, gigaResults, err := RunBlock(t, gigaCtx, [][]byte{gigaTx}) + require.NoError(t, err) + require.Len(t, gigaResults, 1) + + gigaFinalNonce := gigaCtx.TestApp.GigaEvmKeeper.GetNonce(gigaCtx.Ctx, signer.EvmAddress) + + require.Equal(t, gethResults[0].Code, gigaResults[0].Code, "Error codes should match") + + gethNonceBumped := gethFinalNonce > gethInitialNonce + gigaNonceBumped := gigaFinalNonce > gigaInitialNonce + require.Equal(t, gethNonceBumped, gigaNonceBumped, "Nonce bump behavior should match") +} + +func createCustomEVMTx( + t testing.TB, + tCtx *GigaTestContext, + signer utils.TestAcct, + to *common.Address, + value *big.Int, + gasLimit uint64, + gasFeeCap *big.Int, + gasTipCap *big.Int, + nonce uint64, +) []byte { + tc := app.MakeEncodingConfig().TxConfig + tCtx.TestApp.EvmKeeper.SetAddressMapping(tCtx.Ctx, signer.AccountAddress, signer.EvmAddress) + + signedTx, err := ethtypes.SignTx(ethtypes.NewTx(ðtypes.DynamicFeeTx{ + GasFeeCap: gasFeeCap, + GasTipCap: gasTipCap, + Gas: gasLimit, + ChainID: big.NewInt(config.DefaultChainID), + To: to, + Value: value, + Nonce: nonce, + }), signer.EvmSigner, signer.EvmPrivateKey) + require.NoError(t, err) + + txData, err := ethtx.NewTxDataFromTx(signedTx) + require.NoError(t, err) + + msg, err := types.NewMsgEVMTransaction(txData) + require.NoError(t, err) + + txBuilder := tc.NewTxBuilder() + err = txBuilder.SetMsgs(msg) + require.NoError(t, err) + txBuilder.SetGasLimit(10000000000) + txBuilder.SetFeeAmount(sdk.NewCoins(sdk.NewCoin("usei", sdk.NewInt(10000000000)))) + + txBytes, err := tc.TxEncoder()(txBuilder.GetTx()) + require.NoError(t, err) + + return txBytes +} + +func fundAccount(t testing.TB, tCtx *GigaTestContext, addr sdk.AccAddress, amount *big.Int) { + useiAmount := new(big.Int).Div(amount, big.NewInt(1e12)) + if useiAmount.Sign() == 0 { + useiAmount = big.NewInt(1) + } + + amounts := sdk.NewCoins(sdk.NewCoin("usei", sdk.NewIntFromBigInt(useiAmount))) + err := tCtx.TestApp.BankKeeper.MintCoins(tCtx.Ctx, "mint", amounts) + require.NoError(t, err) + err = tCtx.TestApp.BankKeeper.SendCoinsFromModuleToAccount(tCtx.Ctx, "mint", addr, amounts) + require.NoError(t, err) +} + +// TestGigaOCC_PanicRecovery verifies that the Giga OCC executor handles errors gracefully. +// This tests the panic recovery mechanism by running multiple transactions through OCC mode. +func TestGigaOCC_PanicRecovery(t *testing.T) { + blockTime := time.Now() + accts := utils.NewTestAccounts(5) + workers := 4 + txCount := 10 + + transfers := GenerateNonConflictingTransfers(txCount) + + gigaOCCCtx := NewGigaTestContext(t, accts, blockTime, workers, ModeGigaOCC) + gigaOCCTxs := CreateEVMTransferTxs(t, gigaOCCCtx, transfers, true) + _, gigaOCCResults, err := RunBlock(t, gigaOCCCtx, gigaOCCTxs) + + // The key assertion: the test completes without crashing (panic recovery working) + require.NoError(t, err, "Giga OCC should not return error") + require.Len(t, gigaOCCResults, txCount, "Should have results for all transactions") + + for i, result := range gigaOCCResults { + require.Equal(t, uint32(0), result.Code, "tx[%d] should succeed, got code=%d log=%s", i, result.Code, result.Log) + } +} + +// TestGigaOCC_ValidationErrorsHandledGracefully verifies that validation errors +// are returned as proper error codes, not panics. +func TestGigaOCC_ValidationErrorsHandledGracefully(t *testing.T) { + blockTime := time.Now() + accts := utils.NewTestAccounts(3) + workers := 4 + + signer := utils.NewSigner() + recipient := utils.NewSigner() + + gigaCtx := NewGigaTestContext(t, accts, blockTime, workers, ModeGigaOCC) + fundAccount(t, gigaCtx, signer.AccountAddress, big.NewInt(1e12)) // Small amount + + initialNonce := gigaCtx.TestApp.GigaEvmKeeper.GetNonce(gigaCtx.Ctx, signer.EvmAddress) + largeValue := new(big.Int).Mul(big.NewInt(1e18), big.NewInt(1000)) + + to := recipient.EvmAddress + tx := createCustomEVMTx(t, gigaCtx, signer, &to, largeValue, 21000, + big.NewInt(100000000000), big.NewInt(100000000000), initialNonce) + + // Run through OCC - should handle validation error gracefully (not panic) + _, results, err := RunBlock(t, gigaCtx, [][]byte{tx}) + require.NoError(t, err, "Block processing should not panic") + require.Len(t, results, 1) + + // Should fail with proper error code, not crash + require.NotEqual(t, uint32(0), results[0].Code, "Should fail validation") + require.NotEqual(t, uint32(111222), results[0].Code, "Should not be panic error code") +} + +// TestGigaOCC_MixedValidAndInvalidTxs verifies OCC handles a mix of valid and invalid transactions. +func TestGigaOCC_MixedValidAndInvalidTxs(t *testing.T) { + blockTime := time.Now() + accts := utils.NewTestAccounts(5) + workers := 4 + + // Create valid transfers + validTransfers := GenerateNonConflictingTransfers(5) + + gigaCtx := NewGigaTestContext(t, accts, blockTime, workers, ModeGigaOCC) + validTxs := CreateEVMTransferTxs(t, gigaCtx, validTransfers, true) + + // Create an invalid tx (insufficient balance) + poorSigner := utils.NewSigner() + recipient := utils.NewSigner() + fundAccount(t, gigaCtx, poorSigner.AccountAddress, big.NewInt(1e12)) + to := recipient.EvmAddress + invalidTx := createCustomEVMTx(t, gigaCtx, poorSigner, &to, + new(big.Int).Mul(big.NewInt(1e18), big.NewInt(1000)), 21000, + big.NewInt(100000000000), big.NewInt(100000000000), 0) + + // Interleave valid and invalid + allTxs := make([][]byte, 0, len(validTxs)+1) + allTxs = append(allTxs, validTxs[:2]...) + allTxs = append(allTxs, invalidTx) + allTxs = append(allTxs, validTxs[2:]...) + + _, results, err := RunBlock(t, gigaCtx, allTxs) + require.NoError(t, err, "Block processing should complete without panic") + require.Len(t, results, len(allTxs)) + + // Valid txs should succeed + for i := 0; i < 2; i++ { + require.Equal(t, uint32(0), results[i].Code, "Valid tx[%d] should succeed", i) + } + // Invalid tx should fail gracefully + require.NotEqual(t, uint32(0), results[2].Code, "Invalid tx should fail") + // Remaining valid txs should succeed + for i := 3; i < len(results); i++ { + require.Equal(t, uint32(0), results[i].Code, "Valid tx[%d] should succeed", i) + } +} + +// TestGigaSequential_PanicRecovery verifies the synchronous path handles errors gracefully. +func TestGigaSequential_PanicRecovery(t *testing.T) { + blockTime := time.Now() + accts := utils.NewTestAccounts(5) + txCount := 10 + + transfers := GenerateNonConflictingTransfers(txCount) + + gigaCtx := NewGigaTestContext(t, accts, blockTime, 1, ModeGigaSequential) + gigaTxs := CreateEVMTransferTxs(t, gigaCtx, transfers, true) + _, gigaResults, err := RunBlock(t, gigaCtx, gigaTxs) + + require.NoError(t, err, "Giga sequential should not return error") + require.Len(t, gigaResults, txCount) + + for i, result := range gigaResults { + require.Equal(t, uint32(0), result.Code, "tx[%d] should succeed", i) + } +} + +// TestGigaVsGeth_OCCBehavior compares Giga OCC vs Geth OCC for consistency. +func TestGigaVsGeth_OCCBehavior(t *testing.T) { + blockTime := time.Now() + accts := utils.NewTestAccounts(5) + workers := 4 + txCount := 10 + + transfers := GenerateNonConflictingTransfers(txCount) + + // Geth OCC + gethCtx := NewGigaTestContext(t, accts, blockTime, workers, ModeV2withOCC) + gethTxs := CreateEVMTransferTxs(t, gethCtx, transfers, false) + _, gethResults, gethErr := RunBlock(t, gethCtx, gethTxs) + require.NoError(t, gethErr) + + // Giga OCC + gigaCtx := NewGigaTestContext(t, accts, blockTime, workers, ModeGigaOCC) + gigaTxs := CreateEVMTransferTxs(t, gigaCtx, transfers, true) + _, gigaResults, gigaErr := RunBlock(t, gigaCtx, gigaTxs) + require.NoError(t, gigaErr) + + require.Len(t, gethResults, txCount) + require.Len(t, gigaResults, txCount) + + // Compare error codes + for i := range gethResults { + require.Equal(t, gethResults[i].Code, gigaResults[i].Code, + "tx[%d] code mismatch: Geth=%d, Giga=%d", i, gethResults[i].Code, gigaResults[i].Code) + } + + CompareLastResultsHash(t, "GigaOCC_vs_GethOCC", gethResults, gigaResults) +} + +// ============================================================================= +// Validation Tests - Test all validateGigaEVMTx checks against V2 behavior +// ============================================================================= + +// TestGigaValidation_FeeCapBelowBaseFee tests that fee cap < base fee is rejected +// with the correct error code and nonce behavior matching V2. +func TestGigaValidation_FeeCapBelowBaseFee(t *testing.T) { + blockTime := time.Now() + accts := utils.NewTestAccounts(3) + + signer := utils.NewSigner() + recipient := utils.NewSigner() + + // Run with V2 (baseline) + v2Ctx := NewGigaTestContext(t, accts, blockTime, 1, ModeV2Sequential) + fundAccount(t, v2Ctx, signer.AccountAddress, big.NewInt(1e18)) + v2Ctx.TestApp.EvmKeeper.SetAddressMapping(v2Ctx.Ctx, signer.AccountAddress, signer.EvmAddress) + + // Run with Giga + gigaCtx := NewGigaTestContext(t, accts, blockTime, 1, ModeGigaSequential) + fundAccount(t, gigaCtx, signer.AccountAddress, big.NewInt(1e18)) + gigaCtx.TestApp.GigaEvmKeeper.SetAddressMapping(gigaCtx.Ctx, signer.AccountAddress, signer.EvmAddress) + + // Create tx with fee cap below base fee (base fee is typically ~1 gwei) + to := recipient.EvmAddress + lowFeeCap := big.NewInt(1) // 1 wei, way below base fee + v2Tx := createCustomEVMTx(t, v2Ctx, signer, &to, big.NewInt(1000), 21000, lowFeeCap, lowFeeCap, 0) + gigaTx := createCustomEVMTx(t, gigaCtx, signer, &to, big.NewInt(1000), 21000, lowFeeCap, lowFeeCap, 0) + + _, v2Results, _ := RunBlock(t, v2Ctx, [][]byte{v2Tx}) + _, gigaResults, _ := RunBlock(t, gigaCtx, [][]byte{gigaTx}) + + require.Len(t, v2Results, 1) + require.Len(t, gigaResults, 1) + + // Both should fail with ErrInsufficientFee (code 13) + require.NotEqual(t, uint32(0), v2Results[0].Code, "V2 should reject low fee cap") + require.NotEqual(t, uint32(0), gigaResults[0].Code, "Giga should reject low fee cap") + require.Equal(t, v2Results[0].Code, gigaResults[0].Code, + "Error codes should match: V2=%d Giga=%d", v2Results[0].Code, gigaResults[0].Code) + + // Check nonce was bumped (fee validation fails after nonce is validated) + v2Nonce := v2Ctx.TestApp.EvmKeeper.GetNonce(v2Ctx.Ctx, signer.EvmAddress) + gigaNonce := gigaCtx.TestApp.GigaEvmKeeper.GetNonce(gigaCtx.Ctx, signer.EvmAddress) + require.Equal(t, v2Nonce, gigaNonce, "Nonce should match after fee validation failure") +} + +// TestGigaValidation_NonceTooHigh tests that nonce too high is rejected correctly. +func TestGigaValidation_NonceTooHigh(t *testing.T) { + blockTime := time.Now() + accts := utils.NewTestAccounts(3) + + signer := utils.NewSigner() + recipient := utils.NewSigner() + + // V2 + v2Ctx := NewGigaTestContext(t, accts, blockTime, 1, ModeV2Sequential) + fundAccount(t, v2Ctx, signer.AccountAddress, big.NewInt(1e18)) + v2Ctx.TestApp.EvmKeeper.SetAddressMapping(v2Ctx.Ctx, signer.AccountAddress, signer.EvmAddress) + + // Giga + gigaCtx := NewGigaTestContext(t, accts, blockTime, 1, ModeGigaSequential) + fundAccount(t, gigaCtx, signer.AccountAddress, big.NewInt(1e18)) + gigaCtx.TestApp.GigaEvmKeeper.SetAddressMapping(gigaCtx.Ctx, signer.AccountAddress, signer.EvmAddress) + + // Create tx with nonce too high (nonce 5 when current is 0) + to := recipient.EvmAddress + normalFee := big.NewInt(100000000000) // 100 gwei + v2Tx := createCustomEVMTx(t, v2Ctx, signer, &to, big.NewInt(1000), 21000, normalFee, normalFee, 5) + gigaTx := createCustomEVMTx(t, gigaCtx, signer, &to, big.NewInt(1000), 21000, normalFee, normalFee, 5) + + _, v2Results, _ := RunBlock(t, v2Ctx, [][]byte{v2Tx}) + _, gigaResults, _ := RunBlock(t, gigaCtx, [][]byte{gigaTx}) + + require.Len(t, v2Results, 1) + require.Len(t, gigaResults, 1) + + // Both should fail with ErrWrongSequence (code 32) + require.NotEqual(t, uint32(0), v2Results[0].Code, "V2 should reject high nonce") + require.NotEqual(t, uint32(0), gigaResults[0].Code, "Giga should reject high nonce") + require.Equal(t, v2Results[0].Code, gigaResults[0].Code, + "Error codes should match: V2=%d Giga=%d", v2Results[0].Code, gigaResults[0].Code) + + // Check nonce was NOT bumped (nonce mismatch should not bump) + v2Nonce := v2Ctx.TestApp.EvmKeeper.GetNonce(v2Ctx.Ctx, signer.EvmAddress) + gigaNonce := gigaCtx.TestApp.GigaEvmKeeper.GetNonce(gigaCtx.Ctx, signer.EvmAddress) + require.Equal(t, uint64(0), v2Nonce, "V2 nonce should not be bumped") + require.Equal(t, uint64(0), gigaNonce, "Giga nonce should not be bumped") +} + +// TestGigaValidation_NonceTooLow tests that nonce too low is rejected correctly. +func TestGigaValidation_NonceTooLow(t *testing.T) { + blockTime := time.Now() + accts := utils.NewTestAccounts(3) + + signer := utils.NewSigner() + recipient := utils.NewSigner() + + // V2 + v2Ctx := NewGigaTestContext(t, accts, blockTime, 1, ModeV2Sequential) + fundAccount(t, v2Ctx, signer.AccountAddress, big.NewInt(1e18)) + v2Ctx.TestApp.EvmKeeper.SetAddressMapping(v2Ctx.Ctx, signer.AccountAddress, signer.EvmAddress) + // Set nonce to 5 + v2Ctx.TestApp.EvmKeeper.SetNonce(v2Ctx.Ctx, signer.EvmAddress, 5) + + // Giga + gigaCtx := NewGigaTestContext(t, accts, blockTime, 1, ModeGigaSequential) + fundAccount(t, gigaCtx, signer.AccountAddress, big.NewInt(1e18)) + gigaCtx.TestApp.GigaEvmKeeper.SetAddressMapping(gigaCtx.Ctx, signer.AccountAddress, signer.EvmAddress) + // Set nonce to 5 + gigaCtx.TestApp.GigaEvmKeeper.SetNonce(gigaCtx.Ctx, signer.EvmAddress, 5) + + // Create tx with nonce too low (nonce 2 when current is 5) + to := recipient.EvmAddress + normalFee := big.NewInt(100000000000) + v2Tx := createCustomEVMTx(t, v2Ctx, signer, &to, big.NewInt(1000), 21000, normalFee, normalFee, 2) + gigaTx := createCustomEVMTx(t, gigaCtx, signer, &to, big.NewInt(1000), 21000, normalFee, normalFee, 2) + + _, v2Results, _ := RunBlock(t, v2Ctx, [][]byte{v2Tx}) + _, gigaResults, _ := RunBlock(t, gigaCtx, [][]byte{gigaTx}) + + require.Len(t, v2Results, 1) + require.Len(t, gigaResults, 1) + + // Both should fail with ErrWrongSequence (code 32) + require.NotEqual(t, uint32(0), v2Results[0].Code, "V2 should reject low nonce") + require.NotEqual(t, uint32(0), gigaResults[0].Code, "Giga should reject low nonce") + require.Equal(t, v2Results[0].Code, gigaResults[0].Code, + "Error codes should match: V2=%d Giga=%d", v2Results[0].Code, gigaResults[0].Code) + + // Nonce should remain at 5 (not bumped) + v2Nonce := v2Ctx.TestApp.EvmKeeper.GetNonce(v2Ctx.Ctx, signer.EvmAddress) + gigaNonce := gigaCtx.TestApp.GigaEvmKeeper.GetNonce(gigaCtx.Ctx, signer.EvmAddress) + require.Equal(t, uint64(5), v2Nonce, "V2 nonce should stay at 5") + require.Equal(t, uint64(5), gigaNonce, "Giga nonce should stay at 5") +} + +// TestGigaValidation_InsufficientBalance tests that insufficient balance is rejected. +func TestGigaValidation_InsufficientBalance(t *testing.T) { + blockTime := time.Now() + accts := utils.NewTestAccounts(3) + + signer := utils.NewSigner() + recipient := utils.NewSigner() + + // V2 - fund with small amount + v2Ctx := NewGigaTestContext(t, accts, blockTime, 1, ModeV2Sequential) + fundAccount(t, v2Ctx, signer.AccountAddress, big.NewInt(1e12)) // 0.000001 sei in wei + v2Ctx.TestApp.EvmKeeper.SetAddressMapping(v2Ctx.Ctx, signer.AccountAddress, signer.EvmAddress) + + // Giga - fund with same small amount + gigaCtx := NewGigaTestContext(t, accts, blockTime, 1, ModeGigaSequential) + fundAccount(t, gigaCtx, signer.AccountAddress, big.NewInt(1e12)) + gigaCtx.TestApp.GigaEvmKeeper.SetAddressMapping(gigaCtx.Ctx, signer.AccountAddress, signer.EvmAddress) + + // Try to send way more than we have + to := recipient.EvmAddress + normalFee := big.NewInt(100000000000) + largeValue := new(big.Int).Mul(big.NewInt(1e18), big.NewInt(1000)) // 1000 sei + v2Tx := createCustomEVMTx(t, v2Ctx, signer, &to, largeValue, 21000, normalFee, normalFee, 0) + gigaTx := createCustomEVMTx(t, gigaCtx, signer, &to, largeValue, 21000, normalFee, normalFee, 0) + + _, v2Results, _ := RunBlock(t, v2Ctx, [][]byte{v2Tx}) + _, gigaResults, _ := RunBlock(t, gigaCtx, [][]byte{gigaTx}) + + require.Len(t, v2Results, 1) + require.Len(t, gigaResults, 1) + + // Both should fail with ErrInsufficientFunds (code 5) + require.NotEqual(t, uint32(0), v2Results[0].Code, "V2 should reject insufficient balance") + require.NotEqual(t, uint32(0), gigaResults[0].Code, "Giga should reject insufficient balance") + require.Equal(t, v2Results[0].Code, gigaResults[0].Code, + "Error codes should match: V2=%d Giga=%d", v2Results[0].Code, gigaResults[0].Code) + + // Nonce should be bumped (balance check happens after nonce validation passes) + v2Nonce := v2Ctx.TestApp.EvmKeeper.GetNonce(v2Ctx.Ctx, signer.EvmAddress) + gigaNonce := gigaCtx.TestApp.GigaEvmKeeper.GetNonce(gigaCtx.Ctx, signer.EvmAddress) + require.Equal(t, v2Nonce, gigaNonce, "Nonce should match after balance validation failure") + require.Equal(t, uint64(1), gigaNonce, "Nonce should be bumped to 1") +} + +// Note: TestGigaValidation_TipCapGreaterThanFeeCap is not possible because +// go-ethereum's SignTx validates tip <= feeCap at signing time (client-side validation). +// The check in validateGigaEVMTx exists for defense-in-depth but can't be tested +// through the normal transaction creation flow. + +// TestGigaValidation_GasReportedOnFailure tests that gas is reported on validation failure. +// Note: V2 and Giga may report different GasUsed values on validation failures due to +// differences in how the ante handler chain reports gas consumption. The critical parity +// requirement is that error codes match (for consensus on tx success/failure). +func TestGigaValidation_GasReportedOnFailure(t *testing.T) { + blockTime := time.Now() + accts := utils.NewTestAccounts(3) + + signer := utils.NewSigner() + recipient := utils.NewSigner() + + // V2 + v2Ctx := NewGigaTestContext(t, accts, blockTime, 1, ModeV2Sequential) + fundAccount(t, v2Ctx, signer.AccountAddress, big.NewInt(1e12)) + v2Ctx.TestApp.EvmKeeper.SetAddressMapping(v2Ctx.Ctx, signer.AccountAddress, signer.EvmAddress) + + // Giga + gigaCtx := NewGigaTestContext(t, accts, blockTime, 1, ModeGigaSequential) + fundAccount(t, gigaCtx, signer.AccountAddress, big.NewInt(1e12)) + gigaCtx.TestApp.GigaEvmKeeper.SetAddressMapping(gigaCtx.Ctx, signer.AccountAddress, signer.EvmAddress) + + // Create failing tx (insufficient balance) + to := recipient.EvmAddress + normalFee := big.NewInt(100000000000) + largeValue := new(big.Int).Mul(big.NewInt(1e18), big.NewInt(1000)) + v2Tx := createCustomEVMTx(t, v2Ctx, signer, &to, largeValue, 21000, normalFee, normalFee, 0) + gigaTx := createCustomEVMTx(t, gigaCtx, signer, &to, largeValue, 21000, normalFee, normalFee, 0) + + _, v2Results, _ := RunBlock(t, v2Ctx, [][]byte{v2Tx}) + _, gigaResults, _ := RunBlock(t, gigaCtx, [][]byte{gigaTx}) + + require.Len(t, v2Results, 1) + require.Len(t, gigaResults, 1) + + // Error codes must match (critical for consensus) + require.Equal(t, v2Results[0].Code, gigaResults[0].Code, + "Error codes should match: V2=%d Giga=%d", v2Results[0].Code, gigaResults[0].Code) + + // Both should report non-zero gas values + require.Greater(t, gigaResults[0].GasUsed, int64(0), "Giga should report GasUsed > 0") + require.Greater(t, gigaResults[0].GasWanted, int64(0), "Giga should report GasWanted > 0") + + // Log the difference for visibility (not a failure) + if v2Results[0].GasUsed != gigaResults[0].GasUsed { + t.Logf("Note: GasUsed differs on validation failure - V2=%d, Giga=%d (expected due to ante handler differences)", + v2Results[0].GasUsed, gigaResults[0].GasUsed) + } +} + +// TestGigaValidation_ErrorCodeParity tests that validation failures produce +// identical error codes between V2 and Giga (critical for consensus on tx success/failure). +// Note: GasUsed may differ between V2 and Giga on validation failures due to ante handler +// differences, but this doesn't affect consensus on whether a tx succeeded or failed. +func TestGigaValidation_ErrorCodeParity(t *testing.T) { + blockTime := time.Now() + accts := utils.NewTestAccounts(5) + + signer1 := utils.NewSigner() + signer2 := utils.NewSigner() + signer3 := utils.NewSigner() + recipient := utils.NewSigner() + + // V2 + v2Ctx := NewGigaTestContext(t, accts, blockTime, 1, ModeV2Sequential) + fundAccount(t, v2Ctx, signer1.AccountAddress, big.NewInt(1e18)) + fundAccount(t, v2Ctx, signer2.AccountAddress, big.NewInt(1e12)) // insufficient for large transfer + fundAccount(t, v2Ctx, signer3.AccountAddress, big.NewInt(1e18)) + v2Ctx.TestApp.EvmKeeper.SetAddressMapping(v2Ctx.Ctx, signer1.AccountAddress, signer1.EvmAddress) + v2Ctx.TestApp.EvmKeeper.SetAddressMapping(v2Ctx.Ctx, signer2.AccountAddress, signer2.EvmAddress) + v2Ctx.TestApp.EvmKeeper.SetAddressMapping(v2Ctx.Ctx, signer3.AccountAddress, signer3.EvmAddress) + + // Giga + gigaCtx := NewGigaTestContext(t, accts, blockTime, 1, ModeGigaSequential) + fundAccount(t, gigaCtx, signer1.AccountAddress, big.NewInt(1e18)) + fundAccount(t, gigaCtx, signer2.AccountAddress, big.NewInt(1e12)) + fundAccount(t, gigaCtx, signer3.AccountAddress, big.NewInt(1e18)) + gigaCtx.TestApp.GigaEvmKeeper.SetAddressMapping(gigaCtx.Ctx, signer1.AccountAddress, signer1.EvmAddress) + gigaCtx.TestApp.GigaEvmKeeper.SetAddressMapping(gigaCtx.Ctx, signer2.AccountAddress, signer2.EvmAddress) + gigaCtx.TestApp.GigaEvmKeeper.SetAddressMapping(gigaCtx.Ctx, signer3.AccountAddress, signer3.EvmAddress) + + to := recipient.EvmAddress + normalFee := big.NewInt(100000000000) + largeValue := new(big.Int).Mul(big.NewInt(1e18), big.NewInt(1000)) + + // Mix of valid and invalid txs + v2Txs := [][]byte{ + createCustomEVMTx(t, v2Ctx, signer1, &to, big.NewInt(1000), 21000, normalFee, normalFee, 0), // valid + createCustomEVMTx(t, v2Ctx, signer2, &to, largeValue, 21000, normalFee, normalFee, 0), // insufficient balance + createCustomEVMTx(t, v2Ctx, signer3, &to, big.NewInt(1000), 21000, normalFee, normalFee, 5), // nonce too high + } + gigaTxs := [][]byte{ + createCustomEVMTx(t, gigaCtx, signer1, &to, big.NewInt(1000), 21000, normalFee, normalFee, 0), + createCustomEVMTx(t, gigaCtx, signer2, &to, largeValue, 21000, normalFee, normalFee, 0), + createCustomEVMTx(t, gigaCtx, signer3, &to, big.NewInt(1000), 21000, normalFee, normalFee, 5), + } + + _, v2Results, _ := RunBlock(t, v2Ctx, v2Txs) + _, gigaResults, _ := RunBlock(t, gigaCtx, gigaTxs) + + require.Len(t, v2Results, 3) + require.Len(t, gigaResults, 3) + + // Compare error codes (critical for consensus) + for i := range v2Results { + require.Equal(t, v2Results[i].Code, gigaResults[i].Code, + "tx[%d] Code mismatch: V2=%d Giga=%d", i, v2Results[i].Code, gigaResults[i].Code) + + // Log gas differences for visibility + if v2Results[i].GasUsed != gigaResults[i].GasUsed { + t.Logf("tx[%d] GasUsed differs: V2=%d Giga=%d (expected for validation failures)", + i, v2Results[i].GasUsed, gigaResults[i].GasUsed) + } + } + + // Verify expected outcomes + require.Equal(t, uint32(0), v2Results[0].Code, "tx[0] should succeed") + require.NotEqual(t, uint32(0), v2Results[1].Code, "tx[1] should fail (insufficient balance)") + require.NotEqual(t, uint32(0), v2Results[2].Code, "tx[2] should fail (nonce too high)") +} + +// TestGigaValidation_FeeCapBelowMinimumFee tests that fee cap < minimum fee is rejected. +func TestGigaValidation_FeeCapBelowMinimumFee(t *testing.T) { + blockTime := time.Now() + accts := utils.NewTestAccounts(3) + + signer := utils.NewSigner() + recipient := utils.NewSigner() + + // V2 + v2Ctx := NewGigaTestContext(t, accts, blockTime, 1, ModeV2Sequential) + fundAccount(t, v2Ctx, signer.AccountAddress, big.NewInt(1e18)) + v2Ctx.TestApp.EvmKeeper.SetAddressMapping(v2Ctx.Ctx, signer.AccountAddress, signer.EvmAddress) + + // Giga + gigaCtx := NewGigaTestContext(t, accts, blockTime, 1, ModeGigaSequential) + fundAccount(t, gigaCtx, signer.AccountAddress, big.NewInt(1e18)) + gigaCtx.TestApp.GigaEvmKeeper.SetAddressMapping(gigaCtx.Ctx, signer.AccountAddress, signer.EvmAddress) + + // Get minimum fee from V2 and use a value just below it + minFee := v2Ctx.TestApp.EvmKeeper.GetMinimumFeePerGas(v2Ctx.Ctx) + lowFeeCap := minFee.TruncateInt().BigInt() + lowFeeCap = new(big.Int).Sub(lowFeeCap, big.NewInt(1)) // Just below minimum + if lowFeeCap.Sign() <= 0 { + t.Skip("minimum fee is 0 or 1, cannot test below-minimum") + } + + to := recipient.EvmAddress + v2Tx := createCustomEVMTx(t, v2Ctx, signer, &to, big.NewInt(1000), 21000, lowFeeCap, lowFeeCap, 0) + gigaTx := createCustomEVMTx(t, gigaCtx, signer, &to, big.NewInt(1000), 21000, lowFeeCap, lowFeeCap, 0) + + _, v2Results, _ := RunBlock(t, v2Ctx, [][]byte{v2Tx}) + _, gigaResults, _ := RunBlock(t, gigaCtx, [][]byte{gigaTx}) + + require.Len(t, v2Results, 1) + require.Len(t, gigaResults, 1) + + // Both should fail + require.NotEqual(t, uint32(0), v2Results[0].Code, "V2 should reject below-minimum fee") + require.NotEqual(t, uint32(0), gigaResults[0].Code, "Giga should reject below-minimum fee") + require.Equal(t, v2Results[0].Code, gigaResults[0].Code, + "Error codes should match: V2=%d Giga=%d", v2Results[0].Code, gigaResults[0].Code) +} + +// TestGigaValidation_AllModes tests validation errors across all executor modes. +func TestGigaValidation_AllModes(t *testing.T) { + blockTime := time.Now() + accts := utils.NewTestAccounts(5) + workers := 4 + + testCases := []struct { + name string + setupTx func(tCtx *GigaTestContext, signer, recipient utils.TestAcct) []byte + }{ + { + name: "InsufficientBalance", + setupTx: func(tCtx *GigaTestContext, signer, recipient utils.TestAcct) []byte { + to := recipient.EvmAddress + return createCustomEVMTx(t, tCtx, signer, &to, + new(big.Int).Mul(big.NewInt(1e18), big.NewInt(1000)), // way more than funded + 21000, big.NewInt(100000000000), big.NewInt(100000000000), 0) + }, + }, + { + name: "NonceTooHigh", + setupTx: func(tCtx *GigaTestContext, signer, recipient utils.TestAcct) []byte { + to := recipient.EvmAddress + return createCustomEVMTx(t, tCtx, signer, &to, + big.NewInt(1000), 21000, + big.NewInt(100000000000), big.NewInt(100000000000), 99) // nonce 99 when current is 0 + }, + }, + } + + modes := []ExecutorMode{ModeV2Sequential, ModeV2withOCC, ModeGigaSequential, ModeGigaOCC} + + for _, tc := range testCases { + t.Run(tc.name, func(t *testing.T) { + var baseResult *abci.ExecTxResult + + for _, mode := range modes { + t.Run(mode.String(), func(t *testing.T) { + signer := utils.NewSigner() + recipient := utils.NewSigner() + + tCtx := NewGigaTestContext(t, accts, blockTime, workers, mode) + fundAccount(t, tCtx, signer.AccountAddress, big.NewInt(1e12)) // Small funding + + // Set address mapping based on mode + if mode == ModeGigaSequential || mode == ModeGigaOCC { + tCtx.TestApp.GigaEvmKeeper.SetAddressMapping(tCtx.Ctx, signer.AccountAddress, signer.EvmAddress) + } else { + tCtx.TestApp.EvmKeeper.SetAddressMapping(tCtx.Ctx, signer.AccountAddress, signer.EvmAddress) + } + + tx := tc.setupTx(tCtx, signer, recipient) + _, results, err := RunBlock(t, tCtx, [][]byte{tx}) + require.NoError(t, err) + require.Len(t, results, 1) + + // Should fail + require.NotEqual(t, uint32(0), results[0].Code, + "%s: should fail in mode %s", tc.name, mode) + + // First result becomes baseline + if baseResult == nil { + baseResult = results[0] + } else { + // All modes should produce same error code + require.Equal(t, baseResult.Code, results[0].Code, + "%s: code mismatch in mode %s (expected %d, got %d)", + tc.name, mode, baseResult.Code, results[0].Code) + } + }) + } + }) + } +} From 5f624e737e850e159d53dcece2e1c2ad0f1a60ab Mon Sep 17 00:00:00 2001 From: Aayush Date: Fri, 13 Mar 2026 14:55:09 -0400 Subject: [PATCH 2/2] handle OutOfGas panics separately --- app/app.go | 9 +++++++++ 1 file changed, 9 insertions(+) diff --git a/app/app.go b/app/app.go index ed7704b9bb..6ea16f5e2a 100644 --- a/app/app.go +++ b/app/app.go @@ -1455,6 +1455,15 @@ func (app *App) ProcessTxsSynchronousGiga(ctx sdk.Context, txs [][]byte, typedTx func() { defer func() { if r := recover(); r != nil { + // Handle panics by type (matches OCC path in makeGigaDeliverTx) + if oogErr, isOOG := r.(sdk.ErrorOutOfGas); isOOG { + result = &abci.ExecTxResult{ + Code: sdkerrors.ErrOutOfGas.ABCICode(), + Log: fmt.Sprintf("out of gas in location: %v", oogErr.Descriptor), + } + return + } + // For other panics (e.g., nil deref from malformed protobuf), log and return ErrPanic logger.Error("panic in giga synchronous executor", "panic", r, "stack", string(debug.Stack())) result = &abci.ExecTxResult{ Code: sdkerrors.ErrPanic.ABCICode(),