Skip to content

fix(giga): write receipt for state-transition errors that bump the nonce (CON-256)#3383

Open
wen-coding wants to merge 11 commits intomainfrom
wen/write_evm_receipt_for_failed_tx
Open

fix(giga): write receipt for state-transition errors that bump the nonce (CON-256)#3383
wen-coding wants to merge 11 commits intomainfrom
wen/write_evm_receipt_for_failed_tx

Conversation

@wen-coding
Copy link
Copy Markdown
Contributor

@wen-coding wen-coding commented May 4, 2026

Invariant

A receipt is written for an EVM tx iff that tx bumped the sender's nonce. Equivalently: any tx the chain treats as having advanced the sender's account state — and only those — is observable via eth_getTransactionByHash / eth_getTransactionReceipt. Txs included in a block but rejected before the nonce bump (e.g. ante-handler failures, pre-execution state-transition errors) leave no receipt and are invisible to standard EVM clients, matching go-ethereum semantics.

This PR fixes a place where the Giga executor violated the invariant: a state-transition error inside Execute() (notably EIP-7623 floor-data-gas underflow, post-Pectra) bumped the sender's nonce but produced no receipt, so eth_getTransactionReceipt returned null forever and clients that polled for it hung. The invariant is now enforced symmetrically across V2 and Giga.

How the invariant is enforced

V2 path (no change in this PR — already correct):

  1. BasicDecorator.AnteHandle registers a WithDeliverTxCallback (x/evm/ante/basic.go:33-39) that fires after DeliverTx regardless of success/failure. The callback bumps the sender's nonce and calls SetNonceBumped.
  2. After msgServer.EVMTransaction returns err, the SDK populates ExecTxResult.Code != 0 and ExecTxResult.Log = err.Error().
  3. At EndBlock, GetAllEVMTxDeferredInfo (x/evm/keeper/deferred.go:24) synthesizes a DeferredInfo{TxHash, Error: txRes.Log} for each tx that didn't append its own.
  4. The EndBlock loop (x/evm/keeper/abci.go:100-113) writes a synthetic receipt for entries with Error != "" — gated on GetNonceBumped, which is exactly the invariant. Existing coverage: TestEndBlock_NoReceiptForNonceMismatch and TestEndBlock_ReceiptCreatedWhenNonceBumped in x/evm/keeper/abci_test.go.

Giga path (fixed in this PR):

  1. executeEVMTxWithGigaExecutor already explicitly bumps the sender's nonce in its execErr != nil branch (app/app.go:1887).
  2. It also calls AppendToEvmTxDeferredInfo for surplus tracking — but with Error == "", so the EndBlock synthetic-receipt loop above skips it.
  3. Fix: call WriteReceipt directly in the err-branch, gated on the explicit nonce bump it already does. EIP-1559 effective gas price is used (not GasFeeCap) so the receipt's EffectiveGasPrice matches go-ethereum semantics for dynamic-fee txs.

Tests

  • TestGiga_FailedExecution_ProducesReceipt (giga/tests/giga_test.go) drives the EIP-7623 floor-data-gas-underflow case through the Giga executor, asserts the explicit nonce bump (nonceBefore+1 == nonceAfter) to lock in the invariant, and asserts the receipt was written with status=0, gasUsed=gasLimit, and the floor-data-gas error in VmError.

Things done

  • Giga path (app/app.go): write receipt in execErr != nil branch using EIP-1559 effective gas price
  • Giga test: nonce-bump assertion + receipt assertion
  • gofmt -s clean

🤖 Generated with Claude Code

Tx that's included in a block must produce an EVM receipt. The msg_server
err-not-VM-error branch (V2: x/evm/keeper/msg_server.go; Giga:
app/app.go executeEVMTxWithGigaExecutor) used to drop the receipt and
log only — leaving eth_getTransactionByHash and eth_getTransactionReceipt
returning null forever for included-but-failed txs.

Write a status=0 receipt with gasUsed=gasLimit instead.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
@github-actions
Copy link
Copy Markdown

github-actions Bot commented May 4, 2026

The latest Buf updates on your PR. Results from workflow Buf / buf (pull_request).

BuildFormatLintBreakingUpdated (UTC)
✅ passed✅ passed✅ passed✅ passedMay 6, 2026, 10:26 PM

@wen-coding wen-coding changed the title evm: write status=0 receipt for state-transition errors evm: write status=0 receipt for state-transition errors (CON-256) May 4, 2026
@codecov
Copy link
Copy Markdown

codecov Bot commented May 4, 2026

Codecov Report

❌ Patch coverage is 66.66667% with 5 lines in your changes missing coverage. Please review.
✅ Project coverage is 59.06%. Comparing base (ab54587) to head (a6ad8e1).

Files with missing lines Patch % Lines
app/app.go 66.66% 4 Missing and 1 partial ⚠️
Additional details and impacted files

Impacted file tree graph

@@            Coverage Diff             @@
##             main    #3383      +/-   ##
==========================================
+ Coverage   59.04%   59.06%   +0.01%     
==========================================
  Files        2105     2099       -6     
  Lines      173288   172779     -509     
==========================================
- Hits       102318   102049     -269     
+ Misses      62091    61873     -218     
+ Partials     8879     8857      -22     
Flag Coverage Δ
sei-chain-pr 58.99% <66.66%> (?)
sei-db 70.41% <ø> (ø)

Flags with carried forward coverage won't be shown. Click here to find out more.

Files with missing lines Coverage Δ
app/app.go 70.30% <66.66%> (+0.61%) ⬆️

... and 66 files with indirect coverage changes

🚀 New features to boost your workflow:
  • ❄️ Test Analytics: Detect flaky tests, report on failures, and find test suite problems.
  • 📦 JS Bundle Analysis: Save yourself from yourself by tracking and limiting bundle sizes in JS merges.

Adds TestGiga_FailedExecution_ProducesReceipt — the Giga-path
counterpart to TestEVMTransactionStateTransitionErrorProducesReceipt.

Triggers the EIP-7623 floor-data-gas check inside go-ethereum's
Execute() (intrinsic gas passes EvmStatelessChecks but floor data gas
fails inside Execute()), then asserts the transient receipt store
contains a status=0 receipt with gasUsed=gasLimit and a populated
VmError. Without the app.go fix, the receipt is dropped and the
assertion fails with "receipt not found"; with the fix it passes.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
wen-coding and others added 6 commits May 4, 2026 17:16
Mirror the success-branch evmMsg construction (PR #3384): use
effectiveGasPrice (computed at line 1866) for receipt's
EffectiveGasPrice field instead of ethTx.GasPrice() which returns
GasFeeCap for dynamic-fee txs.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
LegacyTx had GasPrice == GasFeeCap == GasTipCap, so the
EffectiveGasPrice assertion would pass equally whether the receipt
stored maxFee or the EIP-1559 effective price. Switch to a DynamicFeeTx
where tip < cap so the assertion discriminates: if anyone reverts the
err-branch evmMsg to hand-roll with ethTx.GasPrice() (returns GasFeeCap
for dynamic-fee txs), the assertion catches it.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
V2's existing EndBlock synthetic-receipt path (x/evm/keeper/abci.go:100)
already writes a receipt for state-transition errors: the
GetAllEVMTxDeferredInfo fallback synthesizes a DeferredInfo from
txRes.Log when none was appended, and EndBlock's loop gates the
synthetic-receipt write on GetNonceBumped. BasicDecorator's
WithDeliverTxCallback bumps the nonce + calls SetNonceBumped on every
DeliverTx (including failures), so the rule "receipt iff the tx bumped
the sender's nonce" already holds for V2. The explicit WriteReceipt
added to msg_server.go was redundant and also bypassed the
GetNonceBumped gate, so it's removed.

The Giga path is still missing the receipt because its
AppendToEvmTxDeferredInfo call in the err-branch doesn't propagate the
Error string, so EndBlock's `if deferredInfo.Error != ""` check fails
and skips the synthetic-receipt write. Giga's explicit WriteReceipt
in app.go is kept and is gated on the explicit nonce bump it already
does at app.go:1887.

Both tests are extended with nonce-before/after assertions to lock in
the invariant. The V2 test is rewritten to drive the full production
sequence (BasicDecorator -> msgServer -> deliverTxCallback ->
SetMsgs/SetTxResults -> EndBlock) so it exercises the synthetic path
end-to-end for an EIP-7623 floor-data-gas-underflow tx.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
@wen-coding wen-coding changed the title evm: write status=0 receipt for state-transition errors (CON-256) fix(giga): write receipt for state-transition errors that bump the nonce (CON-256) May 6, 2026
V2's msg_server.go is unchanged on this branch (the EndBlock synthetic
path already handles state-transition errors), so the V2 test added
earlier — useful as the experiment that proved the V2 path works for
floor-data-gas-underflow — is out of scope for a Giga-only PR. The
existing tests in x/evm/keeper/abci_test.go (TestEndBlock_NoReceipt
ForNonceMismatch, TestEndBlock_ReceiptCreatedWhenNonceBumped) already
cover the EndBlock synthetic-receipt mechanism and its GetNonceBumped
gate.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Comment thread giga/tests/giga_test.go Outdated
// Without the fix in app.go's executeEVMTxWithGigaExecutor, the receipt was dropped
// for these "should not happen" cases, so eth_getTransactionReceipt returned null
// forever for an included tx, hanging any client that polls. This is the Giga-path
// counterpart to TestEVMTransactionStateTransitionErrorProducesReceipt in
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

nit: where is TestEVMTransactionStateTransitionErrorProducesReceipt? i don't see it in the file mentioned.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Good point, changed.

wen-coding and others added 2 commits May 6, 2026 15:16
PR review feedback (arajasek): the doc comment on
TestGiga_FailedExecution_ProducesReceipt still pointed at
TestEVMTransactionStateTransitionErrorProducesReceipt as the V2-side
counterpart, but the prior commit (a889125) deleted that test along
with the redundant V2 WriteReceipt.

Replace the stale reference with the actual reason V2 doesn't need a
test: EndBlock's synthetic-receipt path (GetAllEVMTxDeferredInfo +
GetNonceBumped) already covers V2 state-transition errors. Giga doesn't
because its execErr branch passes an empty Error string to
AppendToEvmTxDeferredInfo, so the EndBlock fallback never fires for it
— hence the explicit WriteReceipt this test guards.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Comment thread app/app.go
// 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 {
Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I don't think we want to create a receipt under this scenario, although it is practically impossible (or at least not expected) to have an error here.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants