Skip to content

fix: eth_estimateGas executes the call and reports reverts (stop Curio broadcasting doomed txs) - #195

Merged
Reiers merged 5 commits into
mainfrom
fix/estimategas-execute
Oct 31, 2026
Merged

Reiers merged 5 commits into
mainfrom
fix/estimategas-execute

Conversation

@Reiers

@Reiers Reiers commented Oct 31, 2026

Copy link
Copy Markdown
Owner

Problem

eth_estimateGas returned a fixed heuristic gas limit (0x55b13d93, about 1.44B gas) without executing the call. Lotus executes the message and fails with JSON-RPC code 3 when it would revert; stock Curio relies on that failure to not broadcast a doomed transaction. With Lantern as its only chain node, Curio broadcast transactions that then reverted on chain (PDPVerifier provePossession "Incorrect fee amount", nextProvingPeriod DataSetPaymentBeyondEndEpoch, FilecoinPay settleRail exit 7). An external operator saw about 30% of roughly 230 txs/hour revert, each burning gas at the 1.44B limit.

Same request on calibration, before this PR: Glif returns Only the storage provider can prove possession, Lantern returned 0x55b13d93.

Fix

EthEstimateGas (moved to rpc/handlers/eth_estimategas.go) executes the call with the local EVM against verified state at the live head, with from, to, value, data:

Outcome Response
reverts Lotus' api.ErrExecutionReverted: code 3, message execution failed (exit=[33], revert reason=[...], vm error=[message failed with backtrace: ...]), data = revert bytes hex. Reason decoding ports Lotus' ParseEthRevert (Error(string), Solidity panics, raw hex for custom errors, none for empty). Backtrace: actor id, FVM method (3844450837, or 6 for DELEGATECALL frames), pc of each REVERT, exit code.
sender can't cover value Lotus' exit=[SysErrInsufficientFunds(6)] error, data 0x
succeeds bridge's exact estimate when --vm-bridge-rpc is set (an upstream code-3 revert is passed through as code 3), else the existing heuristic
not executable locally bridge when configured, else the heuristic plus a rate-limited WARN eth_estimateGas: call was NOT executed locally

The error type implements jsonrpc.RPCErrorCodec, so it goes out on the wire as {code: 3, message, data: "0x..."} exactly like Lotus; go-ethereum's ethclient surfaces it as a revert (rpc.DataError). The same codec is added to the existing local eth_call revert error, so eth_call reverts are now code 3 + data too (previously code 1, data only in the message text).

Call value (payable calls, #178 follow-up)

vm/evm gains CallWithValue: the sender balance check (else InsufficientFundsError, nothing runs), CALLVALUE, and balance moves journaled on the ephemeral overlay (BALANCE / SELFBALANCE see them; a reverted sub-call rolls them back). CALL{value} from a contract is modeled into EVM contracts and account-type actors (EOAs, placeholders, burnt funds actor f099, new f4 addresses), so the usual "burn the fee, refund the excess to msg.sender" pattern executes. Nothing is transferred on chain.

Coverage and limits (explicit)

Covered:

  • Reverts anywhere in EVM code (incl. through ERC1967 proxies / DELEGATECALL), with Lotus-identical message, data and backtrace.
  • Payable calls: fee checks on msg.value, value forwarded to EVM contracts / EOAs / burnt funds actor.
  • Plain FIL sends to accounts and new f4 addresses (balance-checked).

Not covered (falls back to the bridge, or heuristic + WARN when bridge-off; never a guessed success):

  • Target is a native actor (miner, multisig, ...), or contract creation (no to).
  • CALL{value} into a native actor, a precompile (including the FEVM call_actor "pay" path 0xfe..03), an unknown ID address, or more value than the contract holds; unimplemented precompiles / opcodes (existing ErrUnsupported faults). If such an operation is reached after the point where the revert would be decided, the call is still treated as not executable rather than guessed.
  • State: execution uses head.ParentState() (what eth_call "latest" runs on). Lotus estimates on the post-head state plus the sender's pending mpool messages; Lantern does not execute tipsets, so it is one epoch older and ignores pending messages. A tx whose outcome flipped in the last epoch can be mis-judged once; Curio retries.
  • Gas number: Lantern still does not meter FEVM gas, so a successful call returns the heuristic (about 1.4B) bridge-off. That is safe (never too low) but overestimation is burned on chain; a metered estimate is a follow-up.
  • Bridge-relayed reverts keep code 3 and the message, but the bridge flattens upstream errors to text, so their data is 0x.

Tests

  • rpc/handlers/testdata/estimategas_calib.json: every code / storage slot / balance the local EVM read for 7 cases, recorded from a public calibration Lotus at a pinned block together with Lotus' own eth_estimateGas answer (re-record with LANTERN_RECORD_ESTGAS_FIXTURE=<rpc> go test ./rpc/handlers -run TestRecordEstimateGasFixture). TestEstimateGas_Fixture requires the local error message and data to equal Lotus' byte for byte (backtrace PCs and actor ids included) and success where Lotus estimates: provePossession from a non-provider, addPieces with value=1 ("no fee on add to existing dataset", payable fee check) and value=0, USDFC decimals() and transfer(0), FIL send, FIL send beyond balance.
  • Handler flow: a revert is never a success (with or without bridge, bridge not consulted), success returns an estimate (heuristic / bridge / bridge-revert / bridge-down), not-executable falls back with a rate-limited WARN, wire shape through go-jsonrpc (code 3 + data string).
  • vm/evm/value_test.go: payable fee check with insufficient fee / no value / enough (burn + refund), insufficient sender funds, BALANCE / SELFBALANCE visibility, rollback of value on a reverted sub-call, unmodeled targets fault, STATICCALL stays read-only, proxy revert backtrace.

Live check (temp calibration home, bridge-off daemon on a loopback port, vs Glif)

provePossession(44538, ...) from 0x...dEaD to PDPVerifier:

LANTERN: {"error":{"code":3,"message":"message execution failed (exit=[33], revert reason=[Error(Only the storage provider can prove possession)], vm error=[message failed with backtrace:
00: f0176630 (method 3844450837) -- contract reverted at 75 (33)
01: f0176630 (method 6) -- contract reverted at 2590 (33)
 (RetCode=33)])","data":"0x08c379a0...2e4f6e6c79207468652073746f726167652070726f76696465722063616e2070726f766520706f7373657373696f6e0000..."}}
GLIF:    {"error":{"code":3,"message":"message execution failed (exit=[33], revert reason=[Error(Only the storage provider can prove possession)], vm error=[message failed with backtrace:
00: f0176630 (method 3844450837) -- contract reverted at 75 (33)
01: f0176630 (method 6) -- contract reverted at 2590 (33)
 (RetCode=33)])","data":"0x08c379a0...(identical)"}}

addPieces(44538, ...) value=0x1 (payable): both revert reason=[Error(no fee on add to existing dataset)], same backtrace (pc 14774) and data. value=0: both Error(Only the storage provider can add pieces) (pc 14695). FIL send beyond balance: identical SysErrInsufficientFunds(6) message. Successes (USDFC decimals(), transfer(0), FIL send): Lantern returns an estimate (heuristic, no bridge configured), Glif returns its metered estimate.

go build, go vet, go test ./... green with CGO_ENABLED=0. Merges cleanly with perf/curio-pdp-burst (trial merge builds and passes rpc/handlers + vm/evm); no changes to evmexec.go / chain_api.go.

CHANGELOG entry and a RUN-PDP troubleshooting row added.

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

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant