Skip to content

evm: write status=0 receipt for state-transition errors (CON-256)#3383

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

evm: write status=0 receipt for state-transition errors (CON-256)#3383
wen-coding wants to merge 7 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

Both V2 (x/evm/keeper/msg_server.go) and Giga (app/app.go executeEVMTxWithGigaExecutor) had an err != nil branch — for state-transition errors that aren't VM errors — that logged the failure and returned without writing a receipt. The receipt store stays empty for that tx hash, so eth_getTransactionByHash and eth_getTransactionReceipt return null forever for an included tx. Clients that poll for the receipt (e.g. ethers, hardhat-ethers) hang.

This patch writes a status=0 receipt with gasUsed=gasLimit in both branches before returning.

Why this branch existed

Original V2 msg_server (#1083, Oct 2023) treated this branch as defensively unreachable: the antehandler validated everything that could fail before execution (signature, nonce, balance, intrinsic gas), so by the time msg_server ran Execute() could only return VM errors (which WriteReceipt already handles via vmError). Dropping the receipt in this branch was harmless then because nothing reached it.

Pectra/EIP-7623 added a floor-data-gas check inside go-ethereum's Execute(). The Sei antehandler doesn't replicate it (and shouldn't — the chain follows go-ethereum semantics). So a tx with intrinsic ≤ gasLimit < floor now passes the antehandler and fails inside Execute(), surfacing this previously-unreachable branch in normal operation.

The Giga path (#2654, Jan 2026; #2889, Feb 2026) was deliberately ported to match V2's behavior here and inherited the same gap.

Why this surfaced first under Autobahn

Under V2+CometBFT the Cosmos tx indexer records every tx in the block by hash, and evmrpc falls back to it when the EVM receipt store has no entry. eth_getTransactionByHash resolves quickly via the fallback even with no EVM receipt, so the missing-receipt bug stayed latent. Autobahn intentionally has no Cosmos tx indexer (it's a deliberate design choice — Cosmos-specific indexing isn't being pulled forward), so the fallback isn't there. Same bug, different visibility.

The general principle

Beyond the specific EIP-7623 case: any tx included in a block should produce a receipt. Silently dropping receipts for "this shouldn't happen" cases is a fragile contract — the next protocol upgrade or antehandler change can move a check across that boundary and make the case observable. Always producing a receipt (with status=0 + a meaningful vmError) gives clients deterministic feedback and removes a class of "tx vanished" bugs by construction.

Things done

  • V2 path (x/evm/keeper/msg_server.go): write receipt in err != nil branch
  • Giga path (app/app.go): write receipt in execErr != nil branch, using effectiveGasPrice for EIP-1559 correctness
  • Unit test in x/evm/keeper/msg_server_test.go — uses a dynamic-fee tx where tip < cap so the EffectiveGasPrice assertion discriminates between effective price and maxFee
  • 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 5, 2026, 1:35 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 58.82353% with 7 lines in your changes missing coverage. Please review.
✅ Project coverage is 59.08%. Comparing base (7004ef1) to head (e038c7a).

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

Impacted file tree graph

@@           Coverage Diff           @@
##             main    #3383   +/-   ##
=======================================
  Coverage   59.08%   59.08%           
=======================================
  Files        2100     2099    -1     
  Lines      173007   172989   -18     
=======================================
- Hits       102218   102209    -9     
+ Misses      61926    61915   -11     
- Partials     8863     8865    +2     
Flag Coverage Δ
sei-chain-pr 64.09% <58.82%> (?)
sei-db 70.41% <ø> (ø)

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

Files with missing lines Coverage Δ
x/evm/keeper/msg_server.go 77.77% <0.00%> (-0.65%) ⬇️
app/app.go 70.46% <66.66%> (+0.62%) ⬆️

... and 28 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 5 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>
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.

1 participant