diff --git a/app/app.go b/app/app.go index a54473a593..d583516baa 100644 --- a/app/app.go +++ b/app/app.go @@ -1610,7 +1610,10 @@ func (app *App) ProcessTxsSynchronousGiga(ctx sdk.Context, txs [][]byte, typedTx // Store-iteration panic: re-run via v2 so the result matches v2 exactly. if fallbackToV2 { utilmetrics.IncrGigaFallbackToV2Counter() // TODO(PLT-327): remove once app_giga_fallback_to_v2_total verified - appMetrics.gigaFallback.Add(ctx.Context(), 1) + appMetrics.gigaFallback.Add(ctx.Context(), 1, + otelmetric.WithAttributes( + attribute.String("reason", gigaFallbackStoreIterator), + attribute.String("scope", "tx"))) res := app.DeliverTxWithResult(ctx, tx, typedTxs[i]) txResults[i] = res ms.Write() @@ -1618,8 +1621,14 @@ func (app *App) ProcessTxsSynchronousGiga(ctx sdk.Context, txs [][]byte, typedTx } if execErr != nil { - // Check if this is a fail-fast error (Cosmos precompile interop detected) + // Abort errors (validation failure, balance migration, self-destruct, + // cosmos-precompile interop) re-run this tx via v2. if gigautils.ShouldExecutionAbort(execErr) { + utilmetrics.IncrGigaFallbackToV2Counter() // TODO(PLT-327): remove once app_giga_fallback_to_v2_total verified + appMetrics.gigaFallback.Add(ctx.Context(), 1, + otelmetric.WithAttributes( + attribute.String("reason", gigaFallbackReason(execErr)), + attribute.String("scope", "tx"))) res := app.DeliverTxWithResult(ctx, tx, typedTxs[i]) txResults[i] = res ms.Write() @@ -1837,16 +1846,23 @@ func (app *App) ProcessTXsWithOCCGiga(ctx sdk.Context, txs [][]byte, typedTxs [] return nil, ctx } + fallbackReason := gigaFallbackOther for _, r := range evmBatchResult { if r.Code == gigautils.GigaAbortCode && r.Codespace == gigautils.GigaAbortCodespace { fallbackToV2 = true + if r.Info != "" { + fallbackReason = r.Info + } break } } if fallbackToV2 { utilmetrics.IncrGigaFallbackToV2Counter() // TODO(PLT-327): remove once app_giga_fallback_to_v2_total verified - appMetrics.gigaFallback.Add(ctx.Context(), 1) + appMetrics.gigaFallback.Add(ctx.Context(), 1, + otelmetric.WithAttributes( + attribute.String("reason", fallbackReason), + attribute.String("scope", "batch"))) // Discard all EVM changes by skipping cache writes, then re-run all txs via DeliverTx. evmBatchResult = nil v2Entries = make([]*sdk.DeliverTxEntry, len(txs)) @@ -1996,6 +2012,34 @@ func (app *App) ProcessBlock(ctx sdk.Context, txs [][]byte, req *BlockProcessReq return events, txResults, endBlockResp, nil } +// Fallback-cause labels for the app_giga_fallback_to_v2 metric. +const ( + gigaFallbackValidationFailed = "validation_failed" + gigaFallbackBalanceMigration = "balance_migration" + gigaFallbackSelfDestruct = "self_destruct" + gigaFallbackInvalidPrecompile = "invalid_precompile" + gigaFallbackStoreIterator = "store_iterator" + gigaFallbackOther = "other" +) + +// gigaFallbackReason labels the app_giga_fallback_to_v2 metric with which +// abort sentinel routed a tx to v2, so operators can tell a validation-failure +// wave from balance-migration churn. +func gigaFallbackReason(err error) string { + switch err.(type) { + case *gigautils.ValidationFailedAbortError: + return gigaFallbackValidationFailed + case *gigaprecompiles.BalanceMigrationAbortError: + return gigaFallbackBalanceMigration + case *gigaprecompiles.SelfDestructAbortError: + return gigaFallbackSelfDestruct + case *gigaprecompiles.InvalidPrecompileCallError: + return gigaFallbackInvalidPrecompile + default: + return gigaFallbackOther + } +} + // executeEVMTxWithGigaExecutor executes a single EVM transaction using the giga executor. // 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) { @@ -2025,19 +2069,11 @@ func (app *App) executeEVMTxWithGigaExecutor(ctx sdk.Context, msg *evmtypes.MsgE ctx = ctx.WithGasMeter(sdk.NewInfiniteGasMeterWithMultiplier(ctx)) 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) - app.EvmKeeper.SetNonceBumped(ctx) - } - // 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) - validation.err.GasUsed = int64(intrinsicGas) //nolint:gosec - validation.err.GasWanted = int64(ethTx.Gas()) //nolint:gosec - return validation.err, nil + // v2 rejects fee/nonce/balance failures in its ante chain, whose + // receipt gas fields giga cannot reconstruct; a giga-side receipt here + // diverges LastResultsHash on mixed fleets (CON-368). Fall back to v2, + // which also owns the nonce bump. + return nil, gigautils.ErrValidationFailed } if !isAssociated { @@ -2277,7 +2313,7 @@ func (app *App) makeGigaDeliverTx(cache *gigaBlockCache) func(sdk.Context, abci. resp = abci.ResponseDeliverTx{ Code: gigautils.GigaAbortCode, Codespace: gigautils.GigaAbortCodespace, - Info: gigautils.GigaAbortInfo, + Info: gigaFallbackStoreIterator, Log: "giga executor abort: store iteration unsupported, fall back to v2", } return @@ -2316,7 +2352,7 @@ func (app *App) makeGigaDeliverTx(cache *gigaBlockCache) func(sdk.Context, abci. return abci.ResponseDeliverTx{ Code: gigautils.GigaAbortCode, Codespace: gigautils.GigaAbortCodespace, - Info: gigautils.GigaAbortInfo, + Info: gigaFallbackReason(err), Log: "giga executor abort: fall back to v2", } } @@ -3006,10 +3042,8 @@ func (app *App) inplacetestnetInitializer(pk cryptotypes.PubKey) error { // 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 + err *abci.ExecTxResult // nil if validation passed + baseFee *big.Int // the base fee used for validation } // validateGigaEVMTx validates an EVM tx for fee, nonce, and stateless checks. @@ -3035,10 +3069,8 @@ func (app *App) validateGigaEVMTx( 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 { @@ -3047,9 +3079,7 @@ func (app *App) validateGigaEVMTx( Code: sdkerrors.ErrInsufficientFee.ABCICode(), Log: "max fee per gas less than block base fee", }, - bumpNonce: bumpNonce, - currentNonce: currentNonce, - baseFee: baseFee, + baseFee: baseFee, } } @@ -3061,9 +3091,7 @@ func (app *App) validateGigaEVMTx( Code: sdkerrors.ErrInsufficientFee.ABCICode(), Log: "max fee per gas less than minimum fee", }, - bumpNonce: bumpNonce, - currentNonce: currentNonce, - baseFee: baseFee, + baseFee: baseFee, } } @@ -3078,9 +3106,7 @@ func (app *App) validateGigaEVMTx( 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, + baseFee: baseFee, } } if txNonce < currentNonce { @@ -3089,9 +3115,7 @@ func (app *App) validateGigaEVMTx( 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, + baseFee: baseFee, } } // Nonce overflow guard (currentNonce + 1 would overflow) @@ -3101,9 +3125,7 @@ func (app *App) validateGigaEVMTx( Code: sdkerrors.ErrWrongSequence.ABCICode(), Log: fmt.Sprintf("nonce max: address %s, nonce: %d", sender.Hex(), currentNonce), }, - bumpNonce: bumpNonce, - currentNonce: currentNonce, - baseFee: baseFee, + baseFee: baseFee, } } @@ -3117,9 +3139,7 @@ func (app *App) validateGigaEVMTx( 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, + baseFee: baseFee, } } } @@ -3131,9 +3151,7 @@ func (app *App) validateGigaEVMTx( 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, + baseFee: baseFee, } } @@ -3144,9 +3162,7 @@ func (app *App) validateGigaEVMTx( 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, + baseFee: baseFee, } } @@ -3157,9 +3173,7 @@ func (app *App) validateGigaEVMTx( 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, + baseFee: baseFee, } } @@ -3172,9 +3186,7 @@ func (app *App) validateGigaEVMTx( 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, + baseFee: baseFee, } } // Set-code tx auth list must be non-empty @@ -3184,9 +3196,7 @@ func (app *App) validateGigaEVMTx( Code: sdkerrors.ErrWrongSequence.ABCICode(), Log: fmt.Sprintf("set-code transaction with empty auth list: sender %s", sender.Hex()), }, - bumpNonce: bumpNonce, - currentNonce: currentNonce, - baseFee: baseFee, + baseFee: baseFee, } } } @@ -3214,18 +3224,14 @@ func (app *App) validateGigaEVMTx( 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, + baseFee: baseFee, } } // All checks passed return gigaValidationResult{ - err: nil, - bumpNonce: bumpNonce, - currentNonce: currentNonce, - baseFee: baseFee, + err: nil, + baseFee: baseFee, } } diff --git a/app/test_helpers.go b/app/test_helpers.go index 8d25f981b0..bdaa60f237 100644 --- a/app/test_helpers.go +++ b/app/test_helpers.go @@ -141,10 +141,7 @@ func NewTestWrapperWithSc(t *testing.T, tm time.Time, valPub cryptotypes.PubKey, } func NewGigaTestWrapper(t *testing.T, tm time.Time, valPub cryptotypes.PubKey, enableEVMCustomPrecompiles bool, useOcc bool, baseAppOptions ...func(*bam.BaseApp)) *TestWrapper { - wrapper := newTestWrapper(t, tm, valPub, enableEVMCustomPrecompiles, true, TestAppOpts{UseSc: true, EnableGiga: true, EnableGigaOCC: useOcc}, baseAppOptions...) - genState := evmtypes.DefaultGenesis() - wrapper.App.EvmKeeper.InitGenesis(wrapper.Ctx, *genState) - return wrapper + return newTestWrapper(t, tm, valPub, enableEVMCustomPrecompiles, true, TestAppOpts{UseSc: true, EnableGiga: true, EnableGigaOCC: useOcc}, baseAppOptions...) } // NewGigaTestWrapperWithRegularStore creates a test wrapper that runs Giga executor @@ -177,10 +174,6 @@ func NewGigaTestWrapperWithRegularStore(t *testing.T, tm time.Time, valPub crypt } } - // Init genesis for GigaEvmKeeper (now uses regular KVStore) - genState := evmtypes.DefaultGenesis() - wrapper.App.EvmKeeper.InitGenesis(wrapper.Ctx, *genState) - return wrapper } diff --git a/giga/executor/utils/errors.go b/giga/executor/utils/errors.go index 182bd998a1..b42276fad0 100644 --- a/giga/executor/utils/errors.go +++ b/giga/executor/utils/errors.go @@ -6,12 +6,27 @@ import ( const ( // GigaAbortCodespace and GigaAbortCode identify a sentinel ResponseDeliverTx - // that signals the caller to fall back to the v2 execution path. + // that signals the caller to fall back to the v2 execution path. The + // response's Info field carries the abort cause for the fallback metric. GigaAbortCodespace = "giga" GigaAbortCode = uint32(1) - GigaAbortInfo = "giga_fallback_to_v2" ) +// ValidationFailedAbortError signals an EVM validation failure (fee/nonce/ +// balance), whose canonical failure receipt only v2's ante chain can produce; +// the caller should fall back to v2. +type ValidationFailedAbortError struct{} + +func (e *ValidationFailedAbortError) Error() string { + return "EVM validation failed; v2 produces the canonical failure receipt" +} + +func (e *ValidationFailedAbortError) IsAbortError() bool { + return true +} + +var ErrValidationFailed error = &ValidationFailedAbortError{} + // 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 715eb14187..11210b1e21 100644 --- a/giga/tests/giga_test.go +++ b/giga/tests/giga_test.go @@ -2411,6 +2411,12 @@ func TestGigaValidation_InsufficientBalance(t *testing.T) { 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") + + // The failure receipt must be byte-identical across executors: its + // Code/Data/GasWanted/GasUsed feed LastResultsHash, and any divergence + // splits consensus on a mixed fleet (CON-368). + CompareDeterministicFields(t, "InsufficientBalance", v2Results, gigaResults) + CompareLastResultsHash(t, "InsufficientBalance", v2Results, gigaResults) } // Note: TestGigaValidation_TipCapGreaterThanFeeCap is not possible because @@ -2418,11 +2424,12 @@ func TestGigaValidation_InsufficientBalance(t *testing.T) { // 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) { +// TestGigaValidation_FailureReceiptParity asserts a validation-failed tx +// produces a byte-identical receipt under both executors. Giga routes +// validation failures to v2 (per-tx fallback, same doctrine as balance +// migration), so the ante-abort receipt (including the gas fields that feed +// LastResultsHash) matches by construction (CON-368). +func TestGigaValidation_FailureReceiptParity(t *testing.T) { blockTime := time.Now() accts := utils.NewTestAccounts(3) @@ -2452,26 +2459,15 @@ func TestGigaValidation_GasReportedOnFailure(t *testing.T) { 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) - } + require.NotEqual(t, uint32(0), v2Results[0].Code, "tx should fail validation") + CompareDeterministicFields(t, "FailureReceiptParity", v2Results, gigaResults) + CompareLastResultsHash(t, "FailureReceiptParity", v2Results, gigaResults) } -// 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) { +// TestGigaValidation_MixedBlockReceiptParity runs a mixed valid/invalid block +// and asserts the full receipts match between V2 and Giga, since every +// deterministic receipt field feeds LastResultsHash. +func TestGigaValidation_MixedBlockReceiptParity(t *testing.T) { blockTime := time.Now() accts := utils.NewTestAccounts(5) @@ -2520,17 +2516,10 @@ func TestGigaValidation_ErrorCodeParity(t *testing.T) { 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) - } - } + // Receipts must be byte-identical: Code/Data/GasWanted/GasUsed feed + // LastResultsHash (CON-368). + CompareDeterministicFields(t, "MixedBlockReceiptParity", v2Results, gigaResults) + CompareLastResultsHash(t, "MixedBlockReceiptParity", v2Results, gigaResults) // Verify expected outcomes require.Equal(t, uint32(0), v2Results[0].Code, "tx[0] should succeed") @@ -2538,6 +2527,198 @@ func TestGigaValidation_ErrorCodeParity(t *testing.T) { require.NotEqual(t, uint32(0), v2Results[2].Code, "tx[2] should fail (nonce too high)") } +// TestGigaValidation_DrainRace_LastResultsHash reproduces CON-368's block +// shape: tx0 drains the sender far enough that tx1 (same sender, next nonce) +// fails its fee check at delivery, a tx that passes admission but fails in +// the block. Receipts must hash identically across executors; under OCC the +// giga batch re-runs through the v2 OCC scheduler. +func TestGigaValidation_DrainRace_LastResultsHash(t *testing.T) { + runDrainRace := func(t *testing.T, gigaMode ExecutorMode) { + blockTime := time.Now() + accts := utils.NewTestAccounts(3) + signer := utils.NewSigner() + sink := utils.NewSigner() + to := sink.EvmAddress + + normalFee := big.NewInt(100000000000) // 21000 gas => 2.1e15 wei max fee + fund := big.NewInt(3e15) // covers tx0's value+fee, not tx1's fee + drain := big.NewInt(5e14) + + v2Ctx := NewGigaTestContext(t, accts, blockTime, 1, ModeV2Sequential) + fundAccount(t, v2Ctx, signer.AccountAddress, fund) + v2Ctx.TestApp.EvmKeeper.SetAddressMapping(v2Ctx.Ctx, signer.AccountAddress, signer.EvmAddress) + v2Txs := [][]byte{ + createCustomEVMTx(t, v2Ctx, signer, &to, drain, 21000, normalFee, normalFee, 0), + createCustomEVMTx(t, v2Ctx, signer, &to, big.NewInt(1), 21000, normalFee, normalFee, 1), + } + _, v2Results, _ := RunBlock(t, v2Ctx, v2Txs) + + gigaCtx := NewGigaTestContext(t, accts, blockTime, 1, gigaMode) + fundAccount(t, gigaCtx, signer.AccountAddress, fund) + gigaCtx.TestApp.GigaEvmKeeper.SetAddressMapping(gigaCtx.Ctx, signer.AccountAddress, signer.EvmAddress) + gigaTxs := [][]byte{ + createCustomEVMTx(t, gigaCtx, signer, &to, drain, 21000, normalFee, normalFee, 0), + createCustomEVMTx(t, gigaCtx, signer, &to, big.NewInt(1), 21000, normalFee, normalFee, 1), + } + _, gigaResults, _ := RunBlock(t, gigaCtx, gigaTxs) + + require.Len(t, v2Results, 2) + require.Len(t, gigaResults, 2) + require.Equal(t, uint32(0), v2Results[0].Code, "tx0 (drain) should succeed") + require.NotEqual(t, uint32(0), v2Results[1].Code, "tx1 should fail its fee check at delivery") + CompareDeterministicFields(t, "DrainRace", v2Results, gigaResults) + CompareLastResultsHash(t, "DrainRace", v2Results, gigaResults) + } + for _, mode := range []ExecutorMode{ModeGigaSequential, ModeGigaOCC} { + t.Run(mode.String(), func(t *testing.T) { runDrainRace(t, mode) }) + } +} + +// TestGigaValidation_ValueExceedsBalance_LastResultsHash covers the balance +// check's scope mismatch: giga's pre-check rejects fee+value > balance while +// v2's ante only requires the fee, running the tx to its EVM outcome. The +// fallback makes giga adopt v2's receipt, keeping the executors code- and +// hash-identical for this class too. +func TestGigaValidation_ValueExceedsBalance_LastResultsHash(t *testing.T) { + blockTime := time.Now() + accts := utils.NewTestAccounts(3) + signer := utils.NewSigner() + recipient := utils.NewSigner() + to := recipient.EvmAddress + + normalFee := big.NewInt(100000000000) + fund := big.NewInt(3e15) // covers the 2.1e15 max fee... + value := big.NewInt(2e15) // ...but not fee+value + + v2Ctx := NewGigaTestContext(t, accts, blockTime, 1, ModeV2Sequential) + fundAccount(t, v2Ctx, signer.AccountAddress, fund) + v2Ctx.TestApp.EvmKeeper.SetAddressMapping(v2Ctx.Ctx, signer.AccountAddress, signer.EvmAddress) + v2Tx := createCustomEVMTx(t, v2Ctx, signer, &to, value, 21000, normalFee, normalFee, 0) + _, v2Results, _ := RunBlock(t, v2Ctx, [][]byte{v2Tx}) + + gigaCtx := NewGigaTestContext(t, accts, blockTime, 1, ModeGigaSequential) + fundAccount(t, gigaCtx, signer.AccountAddress, fund) + gigaCtx.TestApp.GigaEvmKeeper.SetAddressMapping(gigaCtx.Ctx, signer.AccountAddress, signer.EvmAddress) + gigaTx := createCustomEVMTx(t, gigaCtx, signer, &to, value, 21000, normalFee, normalFee, 0) + _, gigaResults, _ := RunBlock(t, gigaCtx, [][]byte{gigaTx}) + + require.Len(t, v2Results, 1) + require.Len(t, gigaResults, 1) + CompareDeterministicFields(t, "ValueExceedsBalance", v2Results, gigaResults) + CompareLastResultsHash(t, "ValueExceedsBalance", v2Results, gigaResults) +} + +// TestGigaValidation_FailureClassParity_AllModes is the forward-looking +// coverage net for CON-368's class: every delivery-time EVM failure it can +// construct through the normal signing flow, run under both giga modes, +// asserting full receipt and LastResultsHash parity against v2. New +// validation branches in validateGigaEVMTx should gain a case here. +// +// Not constructible through this harness (documented, not silently skipped): +// tip > feeCap (go-ethereum rejects at signing), sender-not-EOA (requires +// code at a signing address), and fee-cap-below-base-fee when the test +// chain's base fee is zero. +func TestGigaValidation_FailureClassParity_AllModes(t *testing.T) { + require.False(t, app.MockBalancesEnabled, + "parity tests are meaningless under the mock_balances build tag: it tops off the very accounts whose failures are under test") + + type failureCase struct { + name string + // build returns the tx bytes for a context; called once per context so + // signing uses each context's chain setup. + build func(t *testing.T, tCtx *GigaTestContext, signer utils.TestAcct, to common.Address) [][]byte + } + + normalFee := big.NewInt(100000000000) // 21000 gas => 2.1e15 wei max fee + + cases := []failureCase{ + { + // fee alone exceeds balance: v2's ante-abort receipt (CON-368's tx) + name: "FeeExceedsBalance", + build: func(t *testing.T, tCtx *GigaTestContext, signer utils.TestAcct, to common.Address) [][]byte { + fundAccount(t, tCtx, signer.AccountAddress, big.NewInt(1e12)) + return [][]byte{createCustomEVMTx(t, tCtx, signer, &to, big.NewInt(1), 21000, normalFee, normalFee, 0)} + }, + }, + { + // fee affordable, fee+value not: giga's pre-check scope differs + // from v2's ante (fee-only), so parity relies on the fallback + name: "ValueExceedsBalance", + build: func(t *testing.T, tCtx *GigaTestContext, signer utils.TestAcct, to common.Address) [][]byte { + fundAccount(t, tCtx, signer.AccountAddress, big.NewInt(3e15)) + return [][]byte{createCustomEVMTx(t, tCtx, signer, &to, big.NewInt(2e15), 21000, normalFee, normalFee, 0)} + }, + }, + { + name: "NonceTooHigh", + build: func(t *testing.T, tCtx *GigaTestContext, signer utils.TestAcct, to common.Address) [][]byte { + fundAccount(t, tCtx, signer.AccountAddress, big.NewInt(1e18)) + return [][]byte{createCustomEVMTx(t, tCtx, signer, &to, big.NewInt(1), 21000, normalFee, normalFee, 7)} + }, + }, + { + // nonce 0 succeeds, then a second tx replays nonce 0 + name: "NonceTooLow", + build: func(t *testing.T, tCtx *GigaTestContext, signer utils.TestAcct, to common.Address) [][]byte { + fundAccount(t, tCtx, signer.AccountAddress, big.NewInt(1e18)) + return [][]byte{ + createCustomEVMTx(t, tCtx, signer, &to, big.NewInt(1), 21000, normalFee, normalFee, 0), + createCustomEVMTx(t, tCtx, signer, &to, big.NewInt(2), 21000, normalFee, normalFee, 0), + } + }, + }, + { + // CON-368's exact block shape: tx0 drains the fee balance out + // from under tx1 + name: "DrainRace", + build: func(t *testing.T, tCtx *GigaTestContext, signer utils.TestAcct, to common.Address) [][]byte { + fundAccount(t, tCtx, signer.AccountAddress, big.NewInt(3e15)) + return [][]byte{ + createCustomEVMTx(t, tCtx, signer, &to, big.NewInt(5e14), 21000, normalFee, normalFee, 0), + createCustomEVMTx(t, tCtx, signer, &to, big.NewInt(1), 21000, normalFee, normalFee, 1), + } + }, + }, + } + + gigaModes := []ExecutorMode{ModeGigaSequential, ModeGigaOCC} + + for _, tc := range cases { + t.Run(tc.name, func(t *testing.T) { + for _, gm := range gigaModes { + t.Run(gm.String(), func(t *testing.T) { + blockTime := time.Now() + accts := utils.NewTestAccounts(3) + signer := utils.NewSigner() + recipient := utils.NewSigner() + to := recipient.EvmAddress + + v2Ctx := NewGigaTestContext(t, accts, blockTime, 1, ModeV2Sequential) + v2Ctx.TestApp.EvmKeeper.SetAddressMapping(v2Ctx.Ctx, signer.AccountAddress, signer.EvmAddress) + v2Txs := tc.build(t, v2Ctx, signer, to) + _, v2Results, _ := RunBlock(t, v2Ctx, v2Txs) + + gigaCtx := NewGigaTestContext(t, accts, blockTime, 1, gm) + gigaCtx.TestApp.GigaEvmKeeper.SetAddressMapping(gigaCtx.Ctx, signer.AccountAddress, signer.EvmAddress) + gigaTxs := tc.build(t, gigaCtx, signer, to) + _, gigaResults, _ := RunBlock(t, gigaCtx, gigaTxs) + + require.Len(t, gigaResults, len(v2Results)) + failed := 0 + for _, r := range v2Results { + if r.Code != 0 { + failed++ + } + } + require.Greater(t, failed, 0, "case must produce at least one failed tx under v2, or it covers nothing") + CompareDeterministicFields(t, tc.name+"/"+gm.String(), v2Results, gigaResults) + CompareLastResultsHash(t, tc.name+"/"+gm.String(), v2Results, gigaResults) + }) + } + }) + } +} + // TestGigaValidation_FeeCapBelowMinimumFee tests that fee cap < minimum fee is rejected. func TestGigaValidation_FeeCapBelowMinimumFee(t *testing.T) { blockTime := time.Now()