Skip to content

feat(scheduler): opt-in deterministic skip of tx-level-invalid txns#110

Open
Richard1048576 wants to merge 1 commit into
Galxe:mainfrom
Richard1048576:feat/skip-invalid-txn-optin
Open

feat(scheduler): opt-in deterministic skip of tx-level-invalid txns#110
Richard1048576 wants to merge 1 commit into
Galxe:mainfrom
Richard1048576:feat/skip-invalid-txn-optin

Conversation

@Richard1048576

@Richard1048576 Richard1048576 commented Jul 11, 2026

Copy link
Copy Markdown
Collaborator

What

Adds Scheduler::with_skip_invalid_txn(bool) (off by default). When enabled, a transaction
that fails revm's tx-level validation (EVMError::Transaction, e.g. NonceTooLow,
RejectCallerWithCode) is deterministically skipped — no state change, and a phantom
0-gas Revert result keeps results aligned 1:1 with the block's txs — instead of aborting
the whole block. Non-tx-level errors (Header/Database) stay fatal.

Non-breaking: the flag defaults to false, so existing callers are unchanged (the abort
path and GrevmError are preserved verbatim).

Why

grevm is the executor for the Gravity order-then-execute L1: consensus commits an ordered
block whose per-tx validity cannot be fully pre-screened, because some invalidity is
execution-induced and unmodelable without executing — e.g. a CREATE/CREATE2 run by an
EIP-7702-delegated authority bumps that account's nonce a second time, so a same-block
follow-up tx becomes NonceTooLow only at execution. Today any such tx makes execute()
return Err, which the caller turns into panic!whole-network halt (gravity-audit#838).
This lets the executor skip the offending tx and keep producing blocks — the durable close-out
of that halt class (gravity-audit#823).

Determinism (why this is safe for consensus)

The skip runs in fallback_sequential, i.e. sequential execution against the final
pre-state
, so:

  • the decision is not a speculative false-positive (a tx that only looked invalid against a
    stale optimistic read is re-checked against the committed pre-state); and
  • because the parallel committed prefix is sequential-equivalent (Block-STM) and the sequential
    tail completes the remainder, the final results / state root are identical on every node
    regardless of where parallel execution aborted.

Verification (single-node + 4-validator devnet, Prague)

Built into gravity_node and driven with the EIP-7702 authority-nonce halt pair (a
SetCode(n) whose authorization double-bumps the sender, plus a stale plain(n+1) co-located
in the same block; the pre-#385 filter admits the stale tx so it reaches the executor):

mode result
flag ON (caller opts in) ERROR ... skipping tx-level-invalid tx ... txid=1 err=NonceTooLow{tx:1,state:2}; chain keeps producing (head 174→331), stale tx gets a phantom receipt
flag OFF (default) node aborts on the same block — original halt behaviour preserved
4 validators (flag ON) all four nodes skip and produce a byte-identical block (same hash) and keep advancing → deterministic, no fork

Caller opt-in

The caller enables it explicitly, e.g. in the reth ParallelExecutor:

let executor = Scheduler::new(cfg, env, txs, state, false, precompiles)
    .with_skip_invalid_txn(true);

That reth-side change is a separate PR and should be gated with the coordinated
state-transition-function upgrade (a bad tx goes from halt to skip; mixed versions cause a
liveness stall, not a state fork — only the skip result exists).

Receipt semantics — resolved (gravity-audit#823)

The phantom 0-gas Revert (keeps results 1:1, commits no state) is the intended shape for
this path. gravity-audit#823 settled it in favor of a deterministic status-0 revert receipt
— every committed tx yields exactly one receipt (tooling totality) — with no state effect:
for a tx-level-invalid tx (e.g. NonceTooLow) the executor charges no gas and does not
bump the nonce. That is deliberately different from a normal execution revert (which does
charge gas / bump nonce): for a NonceTooLow tx the account nonce is already ahead (nothing to
advance), and charging gas for a tx that only reaches execution via a faulty proposer would be a
replay-griefing vector. Full rationale + the Monad comparison it's grounded in are in
gravity-audit#823.

So the remaining action is on the receipt builder (greth side, not grevm): ensure the receipt
derived from the phantom Revert (zero gas, empty logs, empty output — revm 40:
ExecutionResult::Revert { gas: ResultGas::new(0,0,0), logs: vec![], output: Bytes::new() })
is well-formed — status = 0, empty
logs/logsBloom, null contractAddress, cumulativeGasUsed carried monotonically (this tx
adds 0). output: [] (no return data) is correct.

Update — parallel-path skip (was bypassed)

A review found the original skip only engaged on the sequential path. On the parallel path
(block_size >= MIN_PARALLEL_TXS, default 64), workers run with disable_nonce_check=true, so a
NonceTooLow is detected by the committer, and parallel_execute's commit_result()
short-circuit returned Err before post_execute()'s skip — so the caller still panic!ed
→ halt. My earlier devnet test only exercised small (sequential-path) blocks and missed this.

Fixed by routing a commit-detected tx-level EVMError::Transaction to fallback_sequential too
(truncating results to the failing txid so it re-executes and skips it). Verified on both revm
versions with a 102-tx block: skip ON → Ok (skip engaged); skip OFF → Err(NonceTooLow)
(non-breaking)
.

Add `Scheduler::with_skip_invalid_txn` (off by default; existing callers unaffected).
When enabled, a transaction that fails revm tx-level validation (`EVMError::Transaction`,
e.g. NonceTooLow) is deterministically SKIPPED — no state change, a phantom 0-gas revert
keeps `results` aligned 1:1 with the block's txs — instead of aborting the whole block.

Motivation: order-then-execute chains (Gravity) hand grevm a consensus-ordered block whose
per-tx validity cannot be fully pre-screened (execution-induced nonce changes, e.g. a CREATE
by a 7702-delegated authority, are not modelable without executing). Today any such tx makes
`execute()` return `Err`, which the caller turns into a panic -> whole-network halt. This lets
the executor skip the offending tx and keep producing blocks.

Determinism: the skip runs in `fallback_sequential` (sequential execution against the final
pre-state), so the decision is not a speculative false-positive; and because the parallel
committed prefix is sequential-equivalent (Block-STM), the final results/state root are
identical on every node regardless of where parallel execution aborted. Non-tx-level errors
(Header/Database) stay fatal. gravity-audit#823.
@Richard1048576 Richard1048576 force-pushed the feat/skip-invalid-txn-optin branch from 0f91a66 to c7e92be Compare July 12, 2026 06:46
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