Skill: adversarial-check
Given two git refs (commit hashes and/or tags), derive the diff between them and adversarially search for execution paths that could produce different consensus outputs — state root, block hash, receipts root, gas used, or accept/reject decisions. The divergence to hunt is of two kinds, and both matter: between the two refs (REF_OLD vs REF_NEW, the classic upgrade fork) and within REF_NEW itself — two code paths in the candidate binary that must agree but might not (sequencer build vs importer/validator, default builder vs Flashblock build, producer vs verifier, local build vs P2P/cached replay). The goal is to find the edge case that forks the chain before it ships.
TRIGGER when: user runs /adversarial-check <ref1> <ref2>, or asks to "check consensus safety between two commits/tags/versions", "will this diff fork the chain", "adversarial check".
DO NOT TRIGGER when: user asks for a general code review (use pr-review) or a security audit.
Hard constraints: static analysis only — NEVER build or execute
This skill is a pure static-reasoning exercise over git history. Building this workspace takes enormous CPU/time and adds nothing the diff can't tell you.
- NEVER run
cargo build, cargo check, cargo clippy, cargo test, cargo nextest, cargo install, cargo run, just build*/just check/just test, docker build, or any command that compiles code or executes the node/tests. This applies to every sub-agent spawned by this skill — repeat the prohibition verbatim in their prompts.
- NEVER modify the working tree (no checkout, no submodule update, no cargo metadata/tree, which may touch the lockfile or network). Read code exclusively via
git log, git diff REF_OLD REF_NEW -- <path>, git show <ref>:<path>, and git -C <submodule> … equivalents. One write is sanctioned: the final report file (Section 7) — a single untracked HTML report file; never write anything else into the repo.
- EXCEPTION — remote reconciliation is required, not optional.
git ls-remote <origin> (read-only, no objects fetched, no working-tree change) MUST be run against the source-of-truth remote to resolve REF_OLD/REF_NEW — see Inputs. A stale local tag pointing at the wrong commit is the single most damaging failure this skill can make (the whole report is then about the wrong diff), so trusting local git rev-parse alone is forbidden. Fetching the authoritative objects for the two validated refs by SHA (git fetch <origin> <sha>) is likewise permitted — objects only, never a checkout/pull/reset/local-ref update. This is the ONE sanctioned network step besides the bounded submodule-object fetch in Section 1.
- Stay within the two validated refs' histories — never browse other branches. All analysis is confined to commits reachable from the current branch's
HEAD, REF_OLD, or REF_NEW (the refs validated in Inputs — tags are permitted even when they lie off the current branch, since tags are immutable release pointers). Never enumerate, resolve, or read commits from any branch other than the current one: no git log <other-branch>, no git show <other-branch>:<path>, no git diff against a branch head outside the current branch's history, no git branch -a/git for-each-ref sweeps to discover other branches, and no fetching other branches. If a trail of evidence appears to lead to a commit outside the validated refs' histories, record it as an open question — do not follow it. (This does not conflict with remote reconciliation: ls-remote-ing the exact REF_OLD/REF_NEW names the user supplied, to confirm the local objects match the server, is resolving the inputs — not discovering or reading other branches.) Repeat this prohibition verbatim in every sub-agent prompt, alongside the build/execute prohibition.
- The only permitted evidence is: the diff hunks, file contents at the two refs, commit messages, and manifest/lockfile contents at the two refs — all reachable from the validated refs or the current branch's
HEAD.
- Where only a build could settle a question (does it compile, which rev does cargo actually resolve, does a test pass), record it as an open question or a verification suggestion in the report — for humans/CI to run later — never execute it yourself.
Inputs
Two git refs are required: REF_OLD and REF_NEW. Tags are always acceptable (even when they lie on another line of history — releases are tagged across release branches); commit hashes and branch names are acceptable only when they lie on the current branch (see validation below).
- If invoked as
/adversarial-check <ref1> <ref2>, use them as REF_OLD and REF_NEW (older/currently-deployed first, newer/candidate second).
- If fewer than two refs are given, ask the user for the missing ref(s). Do not guess.
- Reconcile every ref against the upstream source (
origin) BEFORE anything else — never trust a local tag. A local tag or branch can be stale, or renamed/moved on the remote, and silently point at a different commit than the same-named ref on the server. Resolving with git rev-parse alone will happily return the stale local value and the entire analysis then runs against the wrong commit. So for each ref, the authoritative SHA is the remote's, and you must confirm the local object matches it:# 0. Identify the source-of-truth remote. Default to `origin` (the upstream the repo
# was cloned from — e.g. the GitLab server). If there is no `origin`, run
# `git remote -v` and pick the non-personal-fork upstream; if still ambiguous, ask.
SRC=origin
# 1. Ask the SERVER what this ref name points to — as a tag AND as a branch.
# ls-remote is read-only network; it does not fetch objects or touch the working tree.
git ls-remote "$SRC" "refs/tags/<ref>" "refs/heads/<ref>" "<ref>"
# 2. Compare against the local resolution.
git rev-parse --verify --quiet "<ref>^{commit}"
Then apply these rules:
- The remote SHA wins. If the local ref resolves to a different commit than the remote's
refs/tags/<ref> (or refs/heads/<ref>), the local ref is stale — do NOT proceed on it. Fetch the authoritative object by SHA and pin the analysis to the remote value: git fetch "$SRC" <remote-sha> (objects only — still never checkout, pull, submodule update, or anything that moves the working tree or a local ref). Report the mismatch to the user (local <sha> vs origin <sha>) and use the remote SHA for all subsequent steps.
- Tag/branch name collision. If the server returns BOTH a
refs/tags/<ref> and a refs/heads/<ref> and they differ, say so and default to the tag (releases are what get compared); note the branch SHA in the report. If only one exists, use it.
- Ref unknown on the remote. If
ls-remote returns nothing for the name, it is not a real upstream ref — try git fetch "$SRC" --tags once in case of a brand-new tag, re-run ls-remote, and if still empty, abort and report failure to the user. Do not fall back to a local-only ref.
- Bare commit SHA. If the user passed a 40/7-hex commit hash rather than a name, there is nothing to reconcile — verify it resolves (
git rev-parse --verify), and if the object is absent locally, git fetch "$SRC" <sha> (objects only).
- Record the resolved
REF_OLD/REF_NEW SHAs (the remote-authoritative ones) in the report's Scope line, so the reader can confirm which commits were actually compared.
- Each ref must be either on the current branch or a tag. After the remote-authoritative SHA is fixed, accept a ref if it passes at least one of:
# (a) ancestor of (or equal to) the current branch's HEAD
git merge-base --is-ancestor <ref>^{commit} HEAD
# (b) a tag — immutable release pointer, permitted even off the current branch
git rev-parse --verify --quiet "refs/tags/<ref>"
If a ref passes neither check, it is a branch head or loose commit from another line of history — abort and report which ref was rejected; do not fetch or analyze other branches, and do not fall back to a nearest merge-base. Bare commit hashes are accepted only via check (a); other branches' heads fail both checks by construction.
- Divergent-history tag pairs: when
REF_OLD is not an ancestor of REF_NEW (common with release/hotfix tags carrying cherry-picks), state this explicitly in the report along with the merge-base (git merge-base REF_OLD REF_NEW). The analysis then has two consensus-relevant directions: git log REF_OLD..REF_NEW (changes added by the upgrade) and git log REF_NEW..REF_OLD (changes present in the deployed version that the upgrade removes — e.g. hotfix cherry-picks not yet forward-ported). Both directions feed the same checklist; a hotfix that exists only in REF_OLD and vanishes in REF_NEW is a prime fork candidate and must be called out under its own heading. The two-point diff git diff REF_OLD REF_NEW already covers the net file changes of both directions.
Instructions
1. Establish the diff scope
# Commit-level context (what landed between the refs)
git log --oneline --no-merges REF_OLD..REF_NEW
# For divergent-history tag pairs only: what the upgrade REMOVES
# (commits reachable from REF_OLD but not REF_NEW, e.g. hotfix cherry-picks)
git log --oneline --no-merges REF_NEW..REF_OLD
# Full change surface
git diff --stat REF_OLD REF_NEW
# File list for classification
git diff --name-status REF_OLD REF_NEW
Also capture dependency drift — for an execution client, dependency bumps are a primary consensus-fork source:
git diff REF_OLD REF_NEW -- Cargo.toml Cargo.lock '**/Cargo.toml'
git diff REF_OLD REF_NEW -- .gitmodules deps/
Flag any version change to revm, reth/reth-*, alloy-*, op-alloy-*, op-revm, or the optimism submodule as in-scope even though the Rust diff may look empty — the consensus change lives in the dependency. When a consensus-critical dependency is bumped, list its old→new versions in the report and recommend running this same checklist against the dependency's own diff.
Locally-diffable dependency drift — diff it, don't defer it. Before declaring a bumped dependency unverifiable, check whether its two revisions are readable from here:
- Submodule gitlink moved (
deps/optimism etc.): the objects usually already exist in the submodule's local git dir. Read them with git -C deps/<sub> log --oneline OLD_SHA..NEW_SHA (commit messages alone often name the behavior change) and git -C deps/<sub> diff OLD_SHA NEW_SHA -- <consensus paths>, applying this same checklist to the consensus-relevant hunks. If an object is missing, one bounded git -C deps/<sub> fetch origin <sha> attempt is permitted — objects only; still never submodule update, checkout, or anything that touches any working tree.
- Git-rev pin moved in Cargo.toml/Cargo.lock (e.g.
okx/reth rev A → rev B): check for a local clone of that repo (workspace sibling directories, cargo git checkouts) holding both revs and diff there the same way.
- crates.io version bump: genuinely not diffable here — report old→new and recommend the dependency-side check.
Only after these fail may a dependency bump be filed as an open question; say in the report which of the two revisions was unreadable and why.
2. Classify changed files onto the consensus surface
Sort every changed file into one or more checklist dimensions (Section 3). Files that touch none of them (docs, CI, metrics naming, log messages, RPC read-only formatting) go to a "declared non-consensus" list — still shown in the report so the classification itself can be challenged.
Treat as consensus-relevant by default:
- EVM/execution:
revm, op-revm, evm, op-evm, alloy-op-evm, reth, op-reth, evm config, precompiles, execute/executor, receipt builder
- Chainspec/hardforks:
crates/chainspec/, fork activation timestamps/heights, genesis
- Payload building & validation: payload builder, block assembly, tx pool → block ordering
- Engine/consensus: newPayload/FCU handling, block validation, header checks
- Gas/fees: L1 data fee, base fee, operator/vault fee logic, gas refunds
- Derivation-adjacent state: anything altering what op-node sees via Engine API
- DB/trie: state root computation, trie caching (incremental/cached trie updates reused across blocks or flashblocks), storage formats, pruning that affects historical execution
- Sync paths: live sync vs backfill vs replay executing the same block differently
3. Adversarial checklist (base — extend as the diff demands)
For each dimension, ask: "construct an input (tx, block, timing, history) for which REF_OLD and REF_NEW give different answers in the 'typical assertion' column." Every hit is a finding.
Overall guidelines, applying to every dimension:
- For every affected execution path, enumerate and review every reachable Rust
enum variant and every conditional branch (if/else, match arms, guards, early returns, and ? error propagation). Map each branch to a concrete triggering input and expected observable result; do not validate only the nominal success path or assume an untested arm is semantically equivalent.
- Do not limit the review to comparing REF_OLD with REF_NEW. Also examine whether the execution paths introduced or modified for the new feature can alter the consensus semantics of existing features. In most cases, these interactions can be identified by tracing every affected execution path in REF_NEW.
| # |
Boundary |
Key content to probe |
Typical assertion (must be identical across refs) |
| 1 |
Txpool admission & candidate selection |
Validators, subpools, ordering/replacement rules, gasless admission, candidate iterators |
Accept/reject, rejection class, subpool, replacement result, candidate tx set |
| 2 |
Consensus pre-execution validation |
Header/payload checks, tx env validation, balance/nonce/gas guards, error → Engine API mapping |
Tx/block acceptance decision; no state or counter commits on invalid input |
| 3 |
EVM execution semantics & control flow |
Opcodes, precompiles, env fields, frames, error propagation, fork gating |
Execution status, return/revert data, logs, observable opcode values, halt category |
| 4 |
State transition, commit & rollback |
Journal checkpoints, commit timing, exclusion paths, block finalization |
Per-tx state diffs, post-state, state root; empty delta on failure/exclusion |
| 5 |
Gas & fee accounting |
Gas counters, refunds, fee formulas, vaults, L1 data / operator fees |
gasUsed, cumulative gas, refunds, effective price, all balance deltas |
| 6 |
Receipts, header fields & block commitments |
Receipt construction, header assembly, encoding, roots/hashes |
Receipts, bloom, header fields, tx/receipt/state roots, block hash |
| 7 |
Payload build/import & Flashblock path equivalence |
Default builder, no_tx_pool, Flashblock build/replay, engine import, backfill |
Same inputs ⇒ identical included set, results, post-state, roots, block hash on every path |
Detailed probes per dimension (non-exhaustive — invent more from the actual diff):
Dimension 1: Txpool admission and candidate selection
Key entry points include:
deps/optimism/rust/op-reth/crates/txpool/src/validator.rs
deps/optimism/rust/op-reth/crates/txpool/src/xlayer_gasless.rs
deps/optimism/rust/op-reth/crates/txpool/src/pool.rs
best_transactions / execute_best_transactions in deps/optimism/rust/op-reth/crates/payload/src/builder.rs
When the diff changes a txpool validator, subpool, ordering, replacement rule, or iterator error path, check:
- Does the same transaction change from accepted to rejected, or move among
pending, basefee, queued, or other subpools? Cover nonce < state_nonce, ==, and >, plus consecutive nonces and nonce gaps for one sender.
- Did a balance boundary change? Cover balance sufficient only for
value, only for the maximum L2 fee, for the L2 fee plus L1 data fee, exactly sufficient, and short by 1 wei. Confirm that gasless, normal, and deposit transactions do not incorrectly share one balance rule.
- Did a fee-cap boundary change? Cover
max_fee_per_gas = 0, basefee - 1, basefee, and basefee + 1, plus priority-fee, blob-fee, operator-fee, and L1-fee checks where applicable.
- Do gasless admission and the block executor use the same contract address, state/header, and
(to, input, gas_limit)? Cover whitelist allow and deny, allowance gas exactly at the cap and off by one, contract-call errors, and an unavailable latest header.
- After a new head, base-fee change, or reorg, are existing transactions reclassified or revalidated? Can a previously accepted gasless transaction execute as non-gasless, or vice versa?
- When one transaction from a sender fails, does the iterator skip only that transaction, skip all later nonces from that sender, or terminate the whole candidate iteration? Did a change among
mark_invalid, skip, continue, and break alter the candidate set?
- Did replacement rules or ordering keys change? Cover the same sender and nonce, equal tips, gasless mock tips, and percentile boundaries. Determine whether this is an intended producer-policy change or an allegedly equivalent implementation that changed the candidate set.
- Does a
NoTxPool payload still ignore the txpool completely? Ensure forced or payload-provided transactions cannot be affected accidentally by txpool filters, priority, or gasless mock prices.
Typical assertions: identical txpool accept/reject decisions, permanent/temporary rejection class, subpool, replacement result, and candidate transaction set. Unless ordering is an explicit protocol rule, do not require blocks built under different txpool policies to have the same block hash.
Dimension 2: Consensus pre-execution validation
Key entry points include:
validate_block_gas and execute_transaction_without_commit in deps/optimism/rust/alloy-op-evm/src/block/mod.rs
validate_env and validate_against_state_and_deduct_caller in deps/optimism/rust/op-revm/src/handler.rs
- The reth handler trait pipeline that drives the above — review the trait impls, not only the free functions, since an override or a reordered stage changes what runs:
self.validate(evm) → validate_env + validate_initial_tx_gas, then self.pre_execution(evm, ..) → validate_against_state_and_deduct_caller
- Payload/header validation and the mapping of errors to Engine API
newPayload results
When the diff changes a pre-execution guard, transaction error, ?, or early return, check:
- Do normal, gasless, deposit, system-deposit, and post-execution transactions enter the correct branches? Are deposit/system-transaction rules accidentally relaxed or tightened before or after a fork?
- Are baseline checks for
enveloped_tx, transaction type, chain ID, signature, sender code, nonce, init-code size, and deployed-code size skipped, duplicated, or reordered?
- Which parts must the sender's balance cover:
value, maximum execution fee, L1 data fee, and operator fee? If gasless waives fees, does it still require balance >= value?
- Does block-gas validation use the declared gas limit, actual EVM gas, canonical gas, or gas after refund? Cover remaining block gas exactly equal to and one below the required value, the Regolith deposit exception, and several consecutive transactions.
- Do producer and verifier use the same counter definition and comparison boundary for DA footprint, blob gas, post-execution payload index/refund, and other block-level limits?
- When the gasless allowance system call returns allow/deny, exceeds its gas cap, reverts, halts, or encounters a DB error, how is the transaction classified? Are its journal, warm accesses, logs, and temporary context fully discarded before the real transaction executes?
- Does a new error variant actually change block validity, or only diagnostics? Follow it to the final Engine API result:
VALID, INVALID, SYNCING, or ACCEPTED.
- After every validation early return, do nonce, balance, journal, warm set, receipt count, cumulative gas, DA counters, and canonical head remain unchanged?
Typical assertions: identical transaction/block acceptance decisions; invalid inputs commit no state or counters; when callers branch on error type, compare a stable error category and final payload status rather than the error string.
Dimension 3: EVM execution semantics and control flow
Key entry points include:
OpEvm::transact_raw in deps/optimism/rust/alloy-op-evm/src/lib.rs
- The handler lifecycle in
deps/optimism/rust/op-revm/src/handler.rs, including the reth handler trait stage self.execution(evm, ..) (the frame loop) — review the trait impl, not only the free functions
- Opcode implementations in
revm/crates/interpreter/src/instructions.rs (revm is a workspace dependency crate — diff the pinned revm rev, not just in-repo code)
- Diffs to revm opcode tables, precompiles, frames, and the interpreter
When the diff changes BlockEnv, TxEnv, CfgEnv, an opcode, a precompile, or error propagation, check:
- Which opcodes can observe each modified environment field? At minimum map
block.basefee -> BASEFEE and effective gas-price/transaction fee fields to GASPRICE; also check whether adjacent fields such as NUMBER, TIMESTAMP, COINBASE, PREVRANDAO, GASLIMIT, BLOBBASEFEE, and CHAINID are overwritten or restored together.
- Is a temporary context change used only to bypass validation, or is it visible during contract execution? For example, temporarily setting
block.basefee to zero changes fee validation, BASEFEE, and potentially fee calculations that depend on base fee. Treat any newly introduced execution path the same way — a new if/guard, a new branch only some txs or some configs take: enumerate which inputs enter each arm and the observable output each produces, because a new conditional is itself a fork switch when its predicate differs across refs or across the fleet.
- Is the same behavior preserved for top-level calls, nested
CALL, STATICCALL, DELEGATECALL, CREATE, CREATE2, and precompile paths?
- Does a new or removed
return, ?, map_err, or guard bypass cleanup, context restoration, refund handling, beneficiary rewards, or execution-result normalization? Test revert, halt, OOG, invalid transaction, DB error, and inspector/hook errors, not only success.
- Are temporary fields restored on every exit? Within one block, execute
special tx -> normal tx and special tx revert/OOG -> normal tx; make the later transaction read the affected opcode and persist the value to storage.
- Did propagation of revert data, return data, logs, or halt reasons change? Can an inner-frame revert be converted incorrectly into success/halt, or leave logs/state that should have rolled back?
- Is fork/spec selection off by one? Exercise the same opcode or precompile immediately before activation, in the first active block, and after activation.
- Can changes to Rust integer conversions,
checked_*, saturating_*, defaults, or unwrap_or_default convert an exception into truncation/zero, or vice versa? Exercise zero, maximum, and off-by-one values.
Typical assertions: identical execution status, return/revert data, logs, observable opcode values, halt category, and complete EVM context after the transaction.
Dimension 4: State transition, commit, and rollback
Focus on journal checkpoints, ResultAndState, commit_transaction, block finalization, system/predeploy calls, and paths that execute a candidate but exclude it from the block.
When the diff changes account mutation, commit timing, or rollback, check:
- When does the caller nonce increment for calls and creates, success/revert/halt, and deposit/gasless/normal transactions? A validation failure or excluded candidate must not leave a nonce change.
- Did the order of value transfer, deposit minting, fee deduction/reimbursement, and beneficiary/vault crediting change? After an intermediate failure, only protocol-specified persistent effects may remain.
- Did creation/deletion semantics change for code, storage, transient storage, self-destructed accounts, created accounts, or touched-empty accounts? Cover create/self-destruct/re-create in the same transaction and the same block.
- Can a gasless whitelist/system call, simulation, or execute-then-exclude candidate leave state, logs, access warming, or cached reads? Can the next transaction observe phantom state or warming?
- Is any state/trie/overlay cache reused across blocks (or across flashblocks) without being invalidated when the underlying state moves? A pruned or stale cache entry — one that reflects a different block's state than the one being computed — feeding a state-root or storage-root computation produces a wrong root on the node that holds it. The XLayer flashblock sequencer always uses such a cache, so it is the highest-risk site (and a validator that caches can diverge too). Exercise: build block N with the cache warm, advance past persistence / prune, then build block N+1 touching a range the stale entry covers; the sequencer and an independent validator must compute the same root. (Note: before concluding a cache "drops" an update, apply the end-to-end reasoning gate below — confirm the input is a local delta and not cumulative state that is regenerated downstream.)
- If post-processing fails after
execute_transaction_without_commit succeeds, has any state patch already been applied? Can it later be committed during a retry, the next transaction, or block finalization?
- Did the ordering of receipt/counter updates and DB commit change? What happens if a receipt is pushed but state commit fails, or state commits before receipt construction fails?
- Can failure in block finalization, a post-block balance increment, or a system-contract update leave half of the block state committed?
- Execute
tx1 -> tx2, tx1 revert -> tx2, and tx1 excluded -> tx2; compare the nonce, balance, code, storage, and warm/cold state observed by tx2.
Typical assertions: identical per-transaction account/storage diffs, final post-state, and stateRoot; an empty state delta for failure or exclusion paths; identical pre-state observed by the next transaction in the sequence.
Dimension 5: Gas and fee accounting
When the diff changes a gas counter, refund, fee formula, vault, or any +/-, checked_*, or saturating_* operation, first enumerate every gas quantity present in the code: declared gas limit, intrinsic gas, EVM gas used, canonical gas used, refunded gas, cumulative block gas, DA/blob gas, and state/reservoir gas. Then check:
- Which gas quantity feeds each limit and receipt field, and is it the same for producer and verifier? In particular, ensure canonical gas after refund is not used accidentally to limit actual computation.
- Which counters are incremented, decremented, or cleared on success, revert, halt/OOG, failed create, deposits before/after Regolith, and gasless execution? Does a new early return skip any update?
- Is refund non-negative, capped by the relevant gas used, applied exactly once, and limited by the correct fork's refund cap? Cover
0, exactly at the cap, cap ± 1, and refund greater than gas used.
- Did the inputs to effective gas price, base fee, or priority fee change? Exercise the boundary
max_fee_per_gas == base_fee (the point where the effective priority tip is exactly zero), max_fee_per_gas == base_fee ± 1, zero priority fee, and the priority-fee limit.
- Are sender precharge, unused-gas reimbursement, beneficiary reward, base-fee vault, L1-fee vault, and operator-fee vault balance changes conserved without charging or refunding the same gas twice?
- Does gasless waive only the fees specified by the protocol while preserving gas consumption, refund, and cumulative-gas semantics? It must not set
gasUsed to zero merely because fees are waived, nor reward a beneficiary/vault with unpaid fees.
- Did the ordering of deposit mint/value and fee deduction change? Did loading behavior for L1-fee metadata change when the metadata is absent?
- Are the encoded transaction, compressed size, gas basis, and rounding used for L1-data/operator fees unchanged? Check large multiplication, division, rounding direction, and overflow paths.
- Does a post-execution/SDM refund update both receipt gas and sender/beneficiary/vault balances? Updating only receipt gas directly produces a state-root divergence.
Typical assertions: exact equality of per-transaction gasUsed, cumulative gas, block gas used, refund, effective gas price, and balance deltas for the sender, beneficiary, and every fee vault. Also assert fee-conservation relations instead of comparing only final balances.
Dimension 6: Receipts, header fields, and block commitments
This dimension covers consensus outputs after transaction order has been fixed. Do not review txpool priority or ordering policy here.
When the diff changes receipt construction, header assembly, encoding, a root, or a hash, check:
- Did receipt type, status/post-state, cumulative gas, logs or log order, or logs bloom change? Are OP/X Layer extension fields such as deposit nonce, deposit receipt version, and operator fee present only for the correct fork and transaction type?
- Are receipts constructed correctly for success, revert/halt, deposit, gasless, and post-execution transactions? An excluded or validation-failed candidate must not produce a receipt.
- Are
None, Some(0), an empty-list root, and an absent field kept distinct? At fork boundaries, do withdrawals root, requests hash, blob/DA fields, extraData, and similar fields use the correct presence and encoding?
- Does header
gasUsed use canonical gas or raw EVM gas as required? Do base fee, gas limit, timestamp, and L1-origin-derived fields come from the same payload attributes?
- Is the transaction root computed from the exact ordered transaction bytes finally included in the block? Is the receipt root computed using the correct typed-receipt encoding? Does the state root correspond to that same committed state?
- Is the block hash computed from the newly derived header fields rather than echoing a hash/root supplied in the input block?
- Can a post-execution error make the producer drop a transaction while the importer rejects the block, or make the two paths choose different fallback values for a receipt/header field?
Typical assertions: identical final ordered transaction bytes, complete receipts, logs bloom, consensus header fields, transaction root, receipt root, state root, and block hash. Report the earliest differing field before reporting its derived root/hash.
Dimension 7: Payload build, import, and Flashblock path equivalence
For xlayer-reth, compare the following paths; op-rbuilder is out of scope:
- Optimism default payload builder:
deps/optimism/rust/op-reth/crates/payload/src/builder.rs
- The
no_tx_pool payload-attributes path
- X Layer local Flashblock build:
crates/builder/src/flashblocks/builder.rs
- External/cached Flashblock execution and replay:
crates/builder/src/flashblocks/handler.rs and related cache/replay paths
- Engine API import and canonical block execution of the final payload
- The validator / RPC (named) node path: independent re-execution of the imported payload and any state/proof serving it performs — it must recompute identical commitments to the sequencer
- Backfill, historical replay, or restart/resume paths where relevant
Fix the same parent state, payload attributes, and ordered transaction list to remove txpool-selection noise, then check:
- Are pre-execution system/deposit transactions, payload-provided transactions, builder transactions, and normal transactions inserted at the same positions? Can
no_tx_pool, replay, or first/last-Flashblock conditions omit, duplicate, or reorder them?
- Do the default builder, NoTxPool, and Flashblock paths use the same block-executor configuration, including chain spec, gasless contract, post-execution mode, DA configuration, and receipt builder?
- When one transaction fails execution, does each path exclude only that transaction, skip later transactions from the same sender, continue to the next transaction, stop the current Flashblock, or invalidate the whole payload? Can these choices produce a different included set or pre-state?
- Does segmented Flashblock execution produce the same final state, receipts, and counters as executing the same ordered list at once? Segment boundaries must not reset cumulative gas, DA footprint, warm state, post-execution entries, or builder-transaction state.
- If partial replay of an external cached Flashblock fails, does the path resume at the failed item, fall back to a fresh build, or retain the successful prefix? If an error is logged and ignored, can old and new versions retain different prefix state?
- Do P2P-received Flashblock execution, local Flashblock building, and final canonical import apply the same gasless, fee, and validation semantics to a transaction?
- Cancellation, timeout, and fallback may select a different valid payload, but can they leave state/cache from a cancelled build that contaminates the next build?
- Under a fork supported by both versions, is a candidate-built payload accepted by the old importer, and an old-built payload accepted by the candidate importer, with identical recomputed commitments?
Typical assertions: given the same parent, attributes, and ordered transaction list, all paths produce the same included transaction set, per-transaction results, receipts, gas/fee deltas, post-state, roots, and block hash. If the input candidate sets differ, attribute that first to dimension 1 rather than misclassifying it as an execution-path inconsistency.
Cross-version pairing matters: the fleet upgrades gradually. Always evaluate the mixed topology — REF_NEW sequencer + REF_OLD replicas, and the reverse. A change that is self-consistent within one binary still forks the network if build (new) and import (old) disagree.
4. Build an abstract state/logic tree from the diff — read hunks, not just names
For every file classified in Section 2, read the actual diff (git diff REF_OLD REF_NEW -- <path>) and, where the hunk is ambiguous, read surrounding context at both refs (git show REF_OLD:<path>, git show REF_NEW:<path>). Never compile or execute anything — all reasoning is done on this abstraction:
For each consensus-relevant hunk, model the changed function as an abstract tree and compare the two versions node by node:
input domain (tx fields, block/header fields, timestamps, config flags, prior state)
└─ guard/branch conditions (in evaluation order)
└─ state transitions taken on each branch (writes: nonce/balance/storage/code;
accumulators: gas, fees, logs; early returns / errors)
└─ consensus outputs produced (state root input set, receipts, gas used,
accept/reject + error variant, header fields)
Derive findings by structural comparison of the REF_OLD tree vs the REF_NEW tree:
- Branch set changed: a guard added/removed/reordered → find the input region that lands in different branches across refs.
- Same branch, different transition: identical condition but the write/accumulation differs (order, width, rounding, saturation) → find the value range where results differ.
- Input domain changed: a field/flag/default now feeds the decision → the fleet-wide value of that input becomes a fork switch.
- Output mapping changed: same post-state but different externalization (receipt fields, error variant, header default) → check which consensus assertion consumes it.
Walk each checklist dimension (Section 3) against the merged tree and ask the standard question: is there an input for which the two trees emit different consensus outputs? Every such input region is a candidate finding for Section 5; the tree path IS the trigger derivation.
Opcode observability analysis — for every hunk that touches the EVM environment, execution context, or gas accounting, enumerate which EVM opcodes could observe the change and derive triggers as "a tx executing opcode X reads a different value across refs" (each is a candidate state-root/receipts finding, since observed values flow into storage, logs, and control flow):
- Block env mutation (
BlockEnv/CfgEnv fields set, zeroed, or restored around a tx): BASEFEE (0x48), COINBASE (0x41), TIMESTAMP (0x42), NUMBER (0x43), PREVRANDAO (0x44), GASLIMIT (0x45), CHAINID (0x46), BLOBBASEFEE (0x4A), BLOCKHASH (0x40)
- Tx env / fee handling changes (fee skips, price overrides, sponsor/gasless paths):
GASPRICE (0x3A), ORIGIN (0x32), CALLER (0x33)
- Balance-crediting order or fee-vault changes:
BALANCE (0x31), SELFBALANCE (0x47) — a contract reading its own or a vault's balance mid-tx sees the difference
- Gas accounting, refund, warming/access-list changes:
GAS (0x5A) — any change to when/how much gas is charged is observable by a contract that branches on remaining gas; also dynamic-cost opcodes SLOAD/SSTORE/*CALL/EXTCODE* under warming rule changes
- Code/state introspection after write-ordering changes:
EXTCODESIZE/EXTCODEHASH/EXTCODECOPY (0x3B/0x3F/0x3C), SELFDESTRUCT (0xFF) re-create semantics
- New/changed precompiles or opcode gas tables behind a fork gate: list the affected opcodes/precompile addresses and mark findings fork-conditional on that activation
A mitigation that "only" tweaks the environment a tx runs under (e.g. zeroing base fee to bypass a fee check) is a consensus change if ANY opcode can read it — say which opcode, and the trigger is a tx using it.
Scoped-toggle restoration analysis — for every hunk that mutates shared execution state on a per-tx or per-call scope (a CfgEnv/BlockEnv/TxEnv field toggled around one tx, a validation flag like disable_base_fee, a precompile set or gas table swapped in, a cache primed or bypassed), verify the set→restore pairing structurally, then hunt the leak:
- Every exit path restores: success, revert, invalid-tx skip, and the error/
? early-return paths — a toggle restored only on the happy path leaks on the first failing tx. Trace each return/? between set and restore in both trees.
- Same-block leakage: if the toggle is NOT restored before the next tx executes, the next tx in the block runs under the relaxed/mutated rule — e.g. a base-fee validation bypass leaking lets an underpriced non-exempt tx into the block. The trigger is a two-tx sequence in one block: one tx that engages the toggle (or errors inside it), followed by one that is only valid/invalid depending on the leaked state. Derive it explicitly.
- Cross-ref scope mismatch: one ref scoping the mutation per-tx and the other per-block (or not mutating at all) diverges block content and validation for the follow-up txs, even when the toggled tx itself executes identically — check the txs after the trigger, not just the trigger.
- Observability double-check: a correctly-restored toggle can still be observable during the tx (see the opcode observability analysis above); a validation-only flag that no opcode reads and that restores on every path is the only clean outcome. State which of the two you verified.
Each leak is a finding whose trigger is a same-block tx sequence — cheap for an adversary or even normal traffic to produce, so default likelihood to critical unless the toggle provably cannot be engaged by user txs.
Unusual-opcode trigger sweep — when constructing trigger inputs, do not stop at mainstream opcodes; adversarial payloads live in the rarely-executed
…(truncated)
1---2name: adversarial-check3description: Adversarially compare two commits/tags to derive code execution paths that could consensus-fork (state root mismatch, block hash divergence, gas divergence). Takes two git refs as input, diffs them, and hunts edge cases across a consensus-surface checklist.4---56# Skill: adversarial-check78Given two git refs (commit hashes and/or tags), derive the diff between them and adversarially search for execution paths that could produce **different consensus outputs** — state root, block hash, receipts root, gas used, or accept/reject decisions. The divergence to hunt is of two kinds, and both matter: **between the two refs** (REF_OLD vs REF_NEW, the classic upgrade fork) *and* **within REF_NEW itself** — two code paths in the candidate binary that must agree but might not (sequencer build vs importer/validator, default builder vs Flashblock build, producer vs verifier, local build vs P2P/cached replay). The goal is to find the edge case that forks the chain *before* it ships.910TRIGGER when: user runs `/adversarial-check <ref1> <ref2>`, or asks to "check consensus safety between two commits/tags/versions", "will this diff fork the chain", "adversarial check".11DO NOT TRIGGER when: user asks for a general code review (use `pr-review`) or a security audit.1213## Hard constraints: static analysis only — NEVER build or execute1415This skill is a pure static-reasoning exercise over git history. Building this workspace takes enormous CPU/time and adds nothing the diff can't tell you.1617- **NEVER run** `cargo build`, `cargo check`, `cargo clippy`, `cargo test`, `cargo nextest`, `cargo install`, `cargo run`, `just build*`/`just check`/`just test`, `docker build`, or any command that compiles code or executes the node/tests. This applies to every sub-agent spawned by this skill — repeat the prohibition verbatim in their prompts.18- **NEVER modify the working tree** (no checkout, no submodule update, no cargo metadata/tree, which may touch the lockfile or network). Read code exclusively via `git log`, `git diff REF_OLD REF_NEW -- <path>`, `git show <ref>:<path>`, and `git -C <submodule> …` equivalents. **One write is sanctioned**: the final report file (Section 7) — a single untracked HTML report file; never write anything else into the repo.19- **EXCEPTION — remote reconciliation is required, not optional.** `git ls-remote <origin>` (read-only, no objects fetched, no working-tree change) MUST be run against the source-of-truth remote to resolve `REF_OLD`/`REF_NEW` — see Inputs. A stale local tag pointing at the wrong commit is the single most damaging failure this skill can make (the whole report is then about the wrong diff), so trusting local `git rev-parse` alone is forbidden. Fetching the authoritative **objects** for the two validated refs by SHA (`git fetch <origin> <sha>`) is likewise permitted — objects only, never a `checkout`/`pull`/`reset`/local-ref update. This is the ONE sanctioned network step besides the bounded submodule-object fetch in Section 1.20- **Stay within the two validated refs' histories — never browse other branches.** All analysis is confined to commits reachable from the current branch's `HEAD`, `REF_OLD`, or `REF_NEW` (the refs validated in Inputs — tags are permitted even when they lie off the current branch, since tags are immutable release pointers). Never enumerate, resolve, or read commits from any *branch* other than the current one: no `git log <other-branch>`, no `git show <other-branch>:<path>`, no `git diff` against a branch head outside the current branch's history, no `git branch -a`/`git for-each-ref` sweeps to discover other branches, and no fetching other branches. If a trail of evidence appears to lead to a commit outside the validated refs' histories, record it as an open question — do not follow it. (This does not conflict with remote reconciliation: `ls-remote`-ing the exact `REF_OLD`/`REF_NEW` names the user supplied, to confirm the local objects match the server, is resolving the inputs — not discovering or reading other branches.) Repeat this prohibition verbatim in every sub-agent prompt, alongside the build/execute prohibition.21- The only permitted evidence is: the diff hunks, file contents at the two refs, commit messages, and manifest/lockfile contents at the two refs — all reachable from the validated refs or the current branch's `HEAD`.22- Where only a build could settle a question (does it compile, which rev does cargo actually resolve, does a test pass), record it as an **open question** or a **verification suggestion** in the report — for humans/CI to run later — never execute it yourself.2324## Inputs2526Two git refs are **required**: `REF_OLD` and `REF_NEW`. Tags are always acceptable (even when they lie on another line of history — releases are tagged across release branches); commit hashes and branch names are acceptable only when they lie on the current branch (see validation below).2728- If invoked as `/adversarial-check <ref1> <ref2>`, use them as `REF_OLD` and `REF_NEW` (older/currently-deployed first, newer/candidate second).29- If fewer than two refs are given, ask the user for the missing ref(s). Do not guess.30- **Reconcile every ref against the upstream source (`origin`) BEFORE anything else — never trust a local tag.** A local tag or branch can be stale, or renamed/moved on the remote, and silently point at a different commit than the same-named ref on the server. Resolving with `git rev-parse` alone will happily return the stale local value and the entire analysis then runs against the wrong commit. So for each ref, the authoritative SHA is the **remote's**, and you must confirm the local object matches it:31 ```bash32 # 0. Identify the source-of-truth remote. Default to `origin` (the upstream the repo33 # was cloned from — e.g. the GitLab server). If there is no `origin`, run34 # `git remote -v` and pick the non-personal-fork upstream; if still ambiguous, ask.35 SRC=origin3637 # 1. Ask the SERVER what this ref name points to — as a tag AND as a branch.38 # ls-remote is read-only network; it does not fetch objects or touch the working tree.39 git ls-remote "$SRC" "refs/tags/<ref>" "refs/heads/<ref>" "<ref>"4041 # 2. Compare against the local resolution.42 git rev-parse --verify --quiet "<ref>^{commit}"43 ```44 Then apply these rules:45 - **The remote SHA wins.** If the local ref resolves to a different commit than the remote's `refs/tags/<ref>` (or `refs/heads/<ref>`), the local ref is **stale** — do NOT proceed on it. Fetch the authoritative object by SHA and pin the analysis to the remote value: `git fetch "$SRC" <remote-sha>` (objects only — still never `checkout`, `pull`, `submodule update`, or anything that moves the working tree or a local ref). Report the mismatch to the user (`local <sha> vs origin <sha>`) and use the remote SHA for all subsequent steps.46 - **Tag/branch name collision.** If the server returns BOTH a `refs/tags/<ref>` and a `refs/heads/<ref>` and they differ, say so and default to the **tag** (releases are what get compared); note the branch SHA in the report. If only one exists, use it.47 - **Ref unknown on the remote.** If `ls-remote` returns nothing for the name, it is not a real upstream ref — try `git fetch "$SRC" --tags` once in case of a brand-new tag, re-run `ls-remote`, and if still empty, abort and report failure to the user. Do not fall back to a local-only ref.48 - **Bare commit SHA.** If the user passed a 40/7-hex commit hash rather than a name, there is nothing to reconcile — verify it resolves (`git rev-parse --verify`), and if the object is absent locally, `git fetch "$SRC" <sha>` (objects only).49 - Record the resolved `REF_OLD`/`REF_NEW` SHAs (the remote-authoritative ones) in the report's Scope line, so the reader can confirm which commits were actually compared.50- **Each ref must be either on the current branch or a tag.** After the remote-authoritative SHA is fixed, accept a ref if it passes at least one of:51 ```bash52 # (a) ancestor of (or equal to) the current branch's HEAD53 git merge-base --is-ancestor <ref>^{commit} HEAD5455 # (b) a tag — immutable release pointer, permitted even off the current branch56 git rev-parse --verify --quiet "refs/tags/<ref>"57 ```58 If a ref passes neither check, it is a branch head or loose commit from another line of history — **abort and report which ref was rejected**; do not fetch or analyze other branches, and do not fall back to a nearest merge-base. Bare commit hashes are accepted only via check (a); other branches' heads fail both checks by construction.59- **Divergent-history tag pairs**: when `REF_OLD` is not an ancestor of `REF_NEW` (common with release/hotfix tags carrying cherry-picks), state this explicitly in the report along with the merge-base (`git merge-base REF_OLD REF_NEW`). The analysis then has two consensus-relevant directions: `git log REF_OLD..REF_NEW` (changes added by the upgrade) **and** `git log REF_NEW..REF_OLD` (changes present in the deployed version that the upgrade *removes* — e.g. hotfix cherry-picks not yet forward-ported). Both directions feed the same checklist; a hotfix that exists only in `REF_OLD` and vanishes in `REF_NEW` is a prime fork candidate and must be called out under its own heading. The two-point diff `git diff REF_OLD REF_NEW` already covers the net file changes of both directions.6061## Instructions6263### 1. Establish the diff scope6465```bash66# Commit-level context (what landed between the refs)67git log --oneline --no-merges REF_OLD..REF_NEW6869# For divergent-history tag pairs only: what the upgrade REMOVES70# (commits reachable from REF_OLD but not REF_NEW, e.g. hotfix cherry-picks)71git log --oneline --no-merges REF_NEW..REF_OLD7273# Full change surface74git diff --stat REF_OLD REF_NEW7576# File list for classification77git diff --name-status REF_OLD REF_NEW78```7980Also capture **dependency drift** — for an execution client, dependency bumps are a primary consensus-fork source:8182```bash83git diff REF_OLD REF_NEW -- Cargo.toml Cargo.lock '**/Cargo.toml'84git diff REF_OLD REF_NEW -- .gitmodules deps/85```8687Flag any version change to `revm`, `reth`/`reth-*`, `alloy-*`, `op-alloy-*`, `op-revm`, or the `optimism` submodule as **in-scope even though the Rust diff may look empty** — the consensus change lives in the dependency. When a consensus-critical dependency is bumped, list its old→new versions in the report and recommend running this same checklist against the dependency's own diff.8889**Locally-diffable dependency drift — diff it, don't defer it.** Before declaring a bumped dependency unverifiable, check whether its two revisions are readable from here:9091- **Submodule gitlink moved** (`deps/optimism` etc.): the objects usually already exist in the submodule's local git dir. Read them with `git -C deps/<sub> log --oneline OLD_SHA..NEW_SHA` (commit messages alone often name the behavior change) and `git -C deps/<sub> diff OLD_SHA NEW_SHA -- <consensus paths>`, applying this same checklist to the consensus-relevant hunks. If an object is missing, one bounded `git -C deps/<sub> fetch origin <sha>` attempt is permitted — objects only; still never `submodule update`, `checkout`, or anything that touches any working tree.92- **Git-rev pin moved in Cargo.toml/Cargo.lock** (e.g. `okx/reth` rev A → rev B): check for a local clone of that repo (workspace sibling directories, cargo git checkouts) holding both revs and diff there the same way.93- **crates.io version bump**: genuinely not diffable here — report old→new and recommend the dependency-side check.9495Only after these fail may a dependency bump be filed as an open question; say in the report which of the two revisions was unreadable and why.9697### 2. Classify changed files onto the consensus surface9899Sort every changed file into one or more checklist dimensions (Section 3). Files that touch none of them (docs, CI, metrics naming, log messages, RPC read-only formatting) go to a **"declared non-consensus"** list — still shown in the report so the classification itself can be challenged.100101Treat as consensus-relevant by default:102- EVM/execution: `revm`, `op-revm`, `evm`, `op-evm`, `alloy-op-evm`, `reth`, `op-reth`, evm config, precompiles, `execute`/`executor`, receipt builder103- Chainspec/hardforks: `crates/chainspec/`, fork activation timestamps/heights, genesis104- Payload building & validation: payload builder, block assembly, tx pool → block ordering105- Engine/consensus: newPayload/FCU handling, block validation, header checks106- Gas/fees: L1 data fee, base fee, operator/vault fee logic, gas refunds107- Derivation-adjacent state: anything altering what op-node sees via Engine API108- DB/trie: state root computation, trie caching (incremental/cached trie updates reused across blocks or flashblocks), storage formats, pruning that affects historical execution109- Sync paths: live sync vs backfill vs replay executing the same block differently110111### 3. Adversarial checklist (base — extend as the diff demands)112113For each dimension, ask: **"construct an input (tx, block, timing, history) for which REF_OLD and REF_NEW give different answers in the 'typical assertion' column."** Every hit is a finding.114115Overall guidelines, applying to every dimension:116117- For every affected execution path, enumerate and review every reachable Rust `enum` variant and every conditional branch (`if`/`else`, `match` arms, guards, early returns, and `?` error propagation). Map each branch to a concrete triggering input and expected observable result; do not validate only the nominal success path or assume an untested arm is semantically equivalent.118- Do not limit the review to comparing REF_OLD with REF_NEW. Also examine whether the execution paths introduced or modified for the new feature can alter the consensus semantics of existing features. In most cases, these interactions can be identified by tracing every affected execution path in REF_NEW.119120| # | Boundary | Key content to probe | Typical assertion (must be identical across refs) |121|---|---|---|---|122| 1 | **Txpool admission & candidate selection** | Validators, subpools, ordering/replacement rules, gasless admission, candidate iterators | Accept/reject, rejection class, subpool, replacement result, candidate tx set |123| 2 | **Consensus pre-execution validation** | Header/payload checks, tx env validation, balance/nonce/gas guards, error → Engine API mapping | Tx/block acceptance decision; no state or counter commits on invalid input |124| 3 | **EVM execution semantics & control flow** | Opcodes, precompiles, env fields, frames, error propagation, fork gating | Execution status, return/revert data, logs, observable opcode values, halt category |125| 4 | **State transition, commit & rollback** | Journal checkpoints, commit timing, exclusion paths, block finalization | Per-tx state diffs, post-state, **state root**; empty delta on failure/exclusion |126| 5 | **Gas & fee accounting** | Gas counters, refunds, fee formulas, vaults, L1 data / operator fees | `gasUsed`, cumulative gas, refunds, effective price, all balance deltas |127| 6 | **Receipts, header fields & block commitments** | Receipt construction, header assembly, encoding, roots/hashes | Receipts, bloom, header fields, tx/receipt/state roots, **block hash** |128| 7 | **Payload build/import & Flashblock path equivalence** | Default builder, `no_tx_pool`, Flashblock build/replay, engine import, backfill | Same inputs ⇒ identical included set, results, post-state, roots, block hash on every path |129130Detailed probes per dimension (non-exhaustive — invent more from the actual diff):131132#### Dimension 1: Txpool admission and candidate selection133134Key entry points include:135136- `deps/optimism/rust/op-reth/crates/txpool/src/validator.rs`137- `deps/optimism/rust/op-reth/crates/txpool/src/xlayer_gasless.rs`138- `deps/optimism/rust/op-reth/crates/txpool/src/pool.rs`139- `best_transactions` / `execute_best_transactions` in `deps/optimism/rust/op-reth/crates/payload/src/builder.rs`140141When the diff changes a txpool validator, subpool, ordering, replacement rule, or iterator error path, check:142143- Does the same transaction change from accepted to rejected, or move among `pending`, `basefee`, `queued`, or other subpools? Cover `nonce < state_nonce`, `==`, and `>`, plus consecutive nonces and nonce gaps for one sender.144- Did a balance boundary change? Cover balance sufficient only for `value`, only for the maximum L2 fee, for the L2 fee plus L1 data fee, exactly sufficient, and short by 1 wei. Confirm that gasless, normal, and deposit transactions do not incorrectly share one balance rule.145- Did a fee-cap boundary change? Cover `max_fee_per_gas = 0`, `basefee - 1`, `basefee`, and `basefee + 1`, plus priority-fee, blob-fee, operator-fee, and L1-fee checks where applicable.146- Do gasless admission and the block executor use the same contract address, state/header, and `(to, input, gas_limit)`? Cover whitelist allow and deny, allowance gas exactly at the cap and off by one, contract-call errors, and an unavailable latest header.147- After a new head, base-fee change, or reorg, are existing transactions reclassified or revalidated? Can a previously accepted gasless transaction execute as non-gasless, or vice versa?148- When one transaction from a sender fails, does the iterator skip only that transaction, skip all later nonces from that sender, or terminate the whole candidate iteration? Did a change among `mark_invalid`, `skip`, `continue`, and `break` alter the candidate set?149- Did replacement rules or ordering keys change? Cover the same sender and nonce, equal tips, gasless mock tips, and percentile boundaries. Determine whether this is an intended producer-policy change or an allegedly equivalent implementation that changed the candidate set.150- Does a `NoTxPool` payload still ignore the txpool completely? Ensure forced or payload-provided transactions cannot be affected accidentally by txpool filters, priority, or gasless mock prices.151152Typical assertions: identical txpool accept/reject decisions, permanent/temporary rejection class, subpool, replacement result, and candidate transaction set. Unless ordering is an explicit protocol rule, do not require blocks built under different txpool policies to have the same block hash.153154#### Dimension 2: Consensus pre-execution validation155156Key entry points include:157158- `validate_block_gas` and `execute_transaction_without_commit` in `deps/optimism/rust/alloy-op-evm/src/block/mod.rs`159- `validate_env` and `validate_against_state_and_deduct_caller` in `deps/optimism/rust/op-revm/src/handler.rs`160- The reth handler **trait pipeline** that drives the above — review the trait impls, not only the free functions, since an override or a reordered stage changes what runs: `self.validate(evm)` → `validate_env` + `validate_initial_tx_gas`, then `self.pre_execution(evm, ..)` → `validate_against_state_and_deduct_caller`161- Payload/header validation and the mapping of errors to Engine API `newPayload` results162163When the diff changes a pre-execution guard, transaction error, `?`, or early return, check:164165- Do normal, gasless, deposit, system-deposit, and post-execution transactions enter the correct branches? Are deposit/system-transaction rules accidentally relaxed or tightened before or after a fork?166- Are baseline checks for `enveloped_tx`, transaction type, chain ID, signature, sender code, nonce, init-code size, and deployed-code size skipped, duplicated, or reordered?167- Which parts must the sender's balance cover: `value`, maximum execution fee, L1 data fee, and operator fee? If gasless waives fees, does it still require `balance >= value`?168- Does block-gas validation use the declared gas limit, actual EVM gas, canonical gas, or gas after refund? Cover remaining block gas exactly equal to and one below the required value, the Regolith deposit exception, and several consecutive transactions.169- Do producer and verifier use the same counter definition and comparison boundary for DA footprint, blob gas, post-execution payload index/refund, and other block-level limits?170- When the gasless allowance system call returns allow/deny, exceeds its gas cap, reverts, halts, or encounters a DB error, how is the transaction classified? Are its journal, warm accesses, logs, and temporary context fully discarded before the real transaction executes?171- Does a new error variant actually change block validity, or only diagnostics? Follow it to the final Engine API result: `VALID`, `INVALID`, `SYNCING`, or `ACCEPTED`.172- After every validation early return, do nonce, balance, journal, warm set, receipt count, cumulative gas, DA counters, and canonical head remain unchanged?173174Typical assertions: identical transaction/block acceptance decisions; invalid inputs commit no state or counters; when callers branch on error type, compare a stable error category and final payload status rather than the error string.175176#### Dimension 3: EVM execution semantics and control flow177178Key entry points include:179180- `OpEvm::transact_raw` in `deps/optimism/rust/alloy-op-evm/src/lib.rs`181- The handler lifecycle in `deps/optimism/rust/op-revm/src/handler.rs`, including the reth handler **trait stage** `self.execution(evm, ..)` (the frame loop) — review the trait impl, not only the free functions182- Opcode implementations in `revm/crates/interpreter/src/instructions.rs` (revm is a workspace **dependency crate** — diff the pinned revm rev, not just in-repo code)183- Diffs to revm opcode tables, precompiles, frames, and the interpreter184185When the diff changes `BlockEnv`, `TxEnv`, `CfgEnv`, an opcode, a precompile, or error propagation, check:186187- Which opcodes can observe each modified environment field? At minimum map `block.basefee -> BASEFEE` and effective gas-price/transaction fee fields to `GASPRICE`; also check whether adjacent fields such as `NUMBER`, `TIMESTAMP`, `COINBASE`, `PREVRANDAO`, `GASLIMIT`, `BLOBBASEFEE`, and `CHAINID` are overwritten or restored together.188- Is a temporary context change used only to bypass validation, or is it visible during contract execution? For example, temporarily setting `block.basefee` to zero changes fee validation, `BASEFEE`, and potentially fee calculations that depend on base fee. Treat any **newly introduced execution path** the same way — a new `if`/guard, a new branch only some txs or some configs take: enumerate which inputs enter each arm and the observable output each produces, because a new conditional is itself a fork switch when its predicate differs across refs or across the fleet.189- Is the same behavior preserved for top-level calls, nested `CALL`, `STATICCALL`, `DELEGATECALL`, `CREATE`, `CREATE2`, and precompile paths?190- Does a new or removed `return`, `?`, `map_err`, or guard bypass cleanup, context restoration, refund handling, beneficiary rewards, or execution-result normalization? Test revert, halt, OOG, invalid transaction, DB error, and inspector/hook errors, not only success.191- Are temporary fields restored on every exit? Within one block, execute `special tx -> normal tx` and `special tx revert/OOG -> normal tx`; make the later transaction read the affected opcode and persist the value to storage.192- Did propagation of revert data, return data, logs, or halt reasons change? Can an inner-frame revert be converted incorrectly into success/halt, or leave logs/state that should have rolled back?193- Is fork/spec selection off by one? Exercise the same opcode or precompile immediately before activation, in the first active block, and after activation.194- Can changes to Rust integer conversions, `checked_*`, `saturating_*`, defaults, or `unwrap_or_default` convert an exception into truncation/zero, or vice versa? Exercise zero, maximum, and off-by-one values.195196Typical assertions: identical execution status, return/revert data, logs, observable opcode values, halt category, and complete EVM context after the transaction.197198#### Dimension 4: State transition, commit, and rollback199200Focus on journal checkpoints, `ResultAndState`, `commit_transaction`, block finalization, system/predeploy calls, and paths that execute a candidate but exclude it from the block.201202When the diff changes account mutation, commit timing, or rollback, check:203204- When does the caller nonce increment for calls and creates, success/revert/halt, and deposit/gasless/normal transactions? A validation failure or excluded candidate must not leave a nonce change.205- Did the order of value transfer, deposit minting, fee deduction/reimbursement, and beneficiary/vault crediting change? After an intermediate failure, only protocol-specified persistent effects may remain.206- Did creation/deletion semantics change for code, storage, transient storage, self-destructed accounts, created accounts, or touched-empty accounts? Cover create/self-destruct/re-create in the same transaction and the same block.207- Can a gasless whitelist/system call, simulation, or execute-then-exclude candidate leave state, logs, access warming, or cached reads? Can the next transaction observe phantom state or warming?208- Is any state/trie/overlay **cache reused across blocks (or across flashblocks)** without being invalidated when the underlying state moves? A pruned or stale cache entry — one that reflects a *different* block's state than the one being computed — feeding a state-root or storage-root computation produces a wrong root on the node that holds it. The XLayer flashblock **sequencer always uses such a cache**, so it is the highest-risk site (and a validator that caches can diverge too). Exercise: build block N with the cache warm, advance past persistence / prune, then build block N+1 touching a range the stale entry covers; the sequencer and an independent validator must compute the same root. (Note: before concluding a cache "drops" an update, apply the end-to-end reasoning gate below — confirm the input is a local delta and not cumulative state that is regenerated downstream.)209- If post-processing fails after `execute_transaction_without_commit` succeeds, has any state patch already been applied? Can it later be committed during a retry, the next transaction, or block finalization?210- Did the ordering of receipt/counter updates and DB commit change? What happens if a receipt is pushed but state commit fails, or state commits before receipt construction fails?211- Can failure in block finalization, a post-block balance increment, or a system-contract update leave half of the block state committed?212- Execute `tx1 -> tx2`, `tx1 revert -> tx2`, and `tx1 excluded -> tx2`; compare the nonce, balance, code, storage, and warm/cold state observed by `tx2`.213214Typical assertions: identical per-transaction account/storage diffs, final post-state, and `stateRoot`; an empty state delta for failure or exclusion paths; identical pre-state observed by the next transaction in the sequence.215216#### Dimension 5: Gas and fee accounting217218When the diff changes a gas counter, refund, fee formula, vault, or any `+/-`, `checked_*`, or `saturating_*` operation, first enumerate every gas quantity present in the code: declared gas limit, intrinsic gas, EVM gas used, canonical gas used, refunded gas, cumulative block gas, DA/blob gas, and state/reservoir gas. Then check:219220- Which gas quantity feeds each limit and receipt field, and is it the same for producer and verifier? In particular, ensure canonical gas after refund is not used accidentally to limit actual computation.221- Which counters are incremented, decremented, or cleared on success, revert, halt/OOG, failed create, deposits before/after Regolith, and gasless execution? Does a new early return skip any update?222- Is refund non-negative, capped by the relevant gas used, applied exactly once, and limited by the correct fork's refund cap? Cover `0`, exactly at the cap, cap ± 1, and refund greater than gas used.223- Did the inputs to effective gas price, base fee, or priority fee change? Exercise the boundary `max_fee_per_gas == base_fee` (the point where the effective priority tip is exactly zero), `max_fee_per_gas == base_fee ± 1`, zero priority fee, and the priority-fee limit.224- Are sender precharge, unused-gas reimbursement, beneficiary reward, base-fee vault, L1-fee vault, and operator-fee vault balance changes conserved without charging or refunding the same gas twice?225- Does gasless waive only the fees specified by the protocol while preserving gas consumption, refund, and cumulative-gas semantics? It must not set `gasUsed` to zero merely because fees are waived, nor reward a beneficiary/vault with unpaid fees.226- Did the ordering of deposit mint/value and fee deduction change? Did loading behavior for L1-fee metadata change when the metadata is absent?227- Are the encoded transaction, compressed size, gas basis, and rounding used for L1-data/operator fees unchanged? Check large multiplication, division, rounding direction, and overflow paths.228- Does a post-execution/SDM refund update both receipt gas and sender/beneficiary/vault balances? Updating only receipt gas directly produces a state-root divergence.229230Typical assertions: exact equality of per-transaction `gasUsed`, cumulative gas, block gas used, refund, effective gas price, and balance deltas for the sender, beneficiary, and every fee vault. Also assert fee-conservation relations instead of comparing only final balances.231232#### Dimension 6: Receipts, header fields, and block commitments233234This dimension covers consensus outputs after transaction order has been fixed. Do not review txpool priority or ordering policy here.235236When the diff changes receipt construction, header assembly, encoding, a root, or a hash, check:237238- Did receipt type, status/post-state, cumulative gas, logs or log order, or logs bloom change? Are OP/X Layer extension fields such as deposit nonce, deposit receipt version, and operator fee present only for the correct fork and transaction type?239- Are receipts constructed correctly for success, revert/halt, deposit, gasless, and post-execution transactions? An excluded or validation-failed candidate must not produce a receipt.240- Are `None`, `Some(0)`, an empty-list root, and an absent field kept distinct? At fork boundaries, do withdrawals root, requests hash, blob/DA fields, `extraData`, and similar fields use the correct presence and encoding?241- Does header `gasUsed` use canonical gas or raw EVM gas as required? Do base fee, gas limit, timestamp, and L1-origin-derived fields come from the same payload attributes?242- Is the transaction root computed from the exact ordered transaction bytes finally included in the block? Is the receipt root computed using the correct typed-receipt encoding? Does the state root correspond to that same committed state?243- Is the block hash computed from the newly derived header fields rather than echoing a hash/root supplied in the input block?244- Can a post-execution error make the producer drop a transaction while the importer rejects the block, or make the two paths choose different fallback values for a receipt/header field?245246Typical assertions: identical final ordered transaction bytes, complete receipts, logs bloom, consensus header fields, transaction root, receipt root, state root, and block hash. Report the earliest differing field before reporting its derived root/hash.247248#### Dimension 7: Payload build, import, and Flashblock path equivalence249250For xlayer-reth, compare the following paths; op-rbuilder is out of scope:251252- Optimism default payload builder: `deps/optimism/rust/op-reth/crates/payload/src/builder.rs`253- The `no_tx_pool` payload-attributes path254- X Layer local Flashblock build: `crates/builder/src/flashblocks/builder.rs`255- External/cached Flashblock execution and replay: `crates/builder/src/flashblocks/handler.rs` and related cache/replay paths256- Engine API import and canonical block execution of the final payload257- The **validator / RPC (named) node** path: independent re-execution of the imported payload and any state/proof serving it performs — it must recompute identical commitments to the sequencer258- Backfill, historical replay, or restart/resume paths where relevant259260Fix the same parent state, payload attributes, and ordered transaction list to remove txpool-selection noise, then check:261262- Are pre-execution system/deposit transactions, payload-provided transactions, builder transactions, and normal transactions inserted at the same positions? Can `no_tx_pool`, replay, or first/last-Flashblock conditions omit, duplicate, or reorder them?263- Do the default builder, NoTxPool, and Flashblock paths use the same block-executor configuration, including chain spec, gasless contract, post-execution mode, DA configuration, and receipt builder?264- When one transaction fails execution, does each path exclude only that transaction, skip later transactions from the same sender, continue to the next transaction, stop the current Flashblock, or invalidate the whole payload? Can these choices produce a different included set or pre-state?265- Does segmented Flashblock execution produce the same final state, receipts, and counters as executing the same ordered list at once? Segment boundaries must not reset cumulative gas, DA footprint, warm state, post-execution entries, or builder-transaction state.266- If partial replay of an external cached Flashblock fails, does the path resume at the failed item, fall back to a fresh build, or retain the successful prefix? If an error is logged and ignored, can old and new versions retain different prefix state?267- Do P2P-received Flashblock execution, local Flashblock building, and final canonical import apply the same gasless, fee, and validation semantics to a transaction?268- Cancellation, timeout, and fallback may select a different valid payload, but can they leave state/cache from a cancelled build that contaminates the next build?269- Under a fork supported by both versions, is a candidate-built payload accepted by the old importer, and an old-built payload accepted by the candidate importer, with identical recomputed commitments?270271Typical assertions: given the same parent, attributes, and ordered transaction list, all paths produce the same included transaction set, per-transaction results, receipts, gas/fee deltas, post-state, roots, and block hash. If the input candidate sets differ, attribute that first to dimension 1 rather than misclassifying it as an execution-path inconsistency.272273**Cross-version pairing matters**: the fleet upgrades gradually. Always evaluate the mixed topology — REF_NEW sequencer + REF_OLD replicas, and the reverse. A change that is self-consistent within one binary still forks the network if build (new) and import (old) disagree.274275### 4. Build an abstract state/logic tree from the diff — read hunks, not just names276277For every file classified in Section 2, read the actual diff (`git diff REF_OLD REF_NEW -- <path>`) and, where the hunk is ambiguous, read surrounding context at both refs (`git show REF_OLD:<path>`, `git show REF_NEW:<path>`). **Never compile or execute anything** — all reasoning is done on this abstraction:278279For each consensus-relevant hunk, model the changed function as an abstract tree and compare the two versions node by node:280281```282input domain (tx fields, block/header fields, timestamps, config flags, prior state)283 └─ guard/branch conditions (in evaluation order)284 └─ state transitions taken on each branch (writes: nonce/balance/storage/code;285 accumulators: gas, fees, logs; early returns / errors)286 └─ consensus outputs produced (state root input set, receipts, gas used,287 accept/reject + error variant, header fields)288```289290Derive findings by structural comparison of the REF_OLD tree vs the REF_NEW tree:291292- **Branch set changed**: a guard added/removed/reordered → find the input region that lands in different branches across refs.293- **Same branch, different transition**: identical condition but the write/accumulation differs (order, width, rounding, saturation) → find the value range where results differ.294- **Input domain changed**: a field/flag/default now feeds the decision → the fleet-wide value of that input becomes a fork switch.295- **Output mapping changed**: same post-state but different externalization (receipt fields, error variant, header default) → check which consensus assertion consumes it.296297Walk each checklist dimension (Section 3) against the merged tree and ask the standard question: *is there an input for which the two trees emit different consensus outputs?* Every such input region is a candidate finding for Section 5; the tree path IS the trigger derivation.298299**Opcode observability analysis** — for every hunk that touches the EVM environment, execution context, or gas accounting, enumerate which EVM opcodes could *observe* the change and derive triggers as "a tx executing opcode X reads a different value across refs" (each is a candidate state-root/receipts finding, since observed values flow into storage, logs, and control flow):300301- Block env mutation (`BlockEnv`/`CfgEnv` fields set, zeroed, or restored around a tx): `BASEFEE` (0x48), `COINBASE` (0x41), `TIMESTAMP` (0x42), `NUMBER` (0x43), `PREVRANDAO` (0x44), `GASLIMIT` (0x45), `CHAINID` (0x46), `BLOBBASEFEE` (0x4A), `BLOCKHASH` (0x40)302- Tx env / fee handling changes (fee skips, price overrides, sponsor/gasless paths): `GASPRICE` (0x3A), `ORIGIN` (0x32), `CALLER` (0x33)303- Balance-crediting order or fee-vault changes: `BALANCE` (0x31), `SELFBALANCE` (0x47) — a contract reading its own or a vault's balance mid-tx sees the difference304- Gas accounting, refund, warming/access-list changes: `GAS` (0x5A) — any change to when/how much gas is charged is observable by a contract that branches on remaining gas; also dynamic-cost opcodes `SLOAD`/`SSTORE`/`*CALL`/`EXTCODE*` under warming rule changes305- Code/state introspection after write-ordering changes: `EXTCODESIZE`/`EXTCODEHASH`/`EXTCODECOPY` (0x3B/0x3F/0x3C), `SELFDESTRUCT` (0xFF) re-create semantics306- New/changed precompiles or opcode gas tables behind a fork gate: list the affected opcodes/precompile addresses and mark findings fork-conditional on that activation307308A mitigation that "only" tweaks the environment a tx runs under (e.g. zeroing base fee to bypass a fee check) is a consensus change if ANY opcode can read it — say which opcode, and the trigger is a tx using it.309310**Scoped-toggle restoration analysis** — for every hunk that mutates shared execution state on a per-tx or per-call scope (a `CfgEnv`/`BlockEnv`/`TxEnv` field toggled around one tx, a validation flag like `disable_base_fee`, a precompile set or gas table swapped in, a cache primed or bypassed), verify the set→restore pairing structurally, then hunt the leak:311312- **Every exit path restores**: success, revert, invalid-tx skip, and the error/`?` early-return paths — a toggle restored only on the happy path leaks on the first failing tx. Trace each `return`/`?` between set and restore in both trees.313- **Same-block leakage**: if the toggle is NOT restored before the next tx executes, the next tx in the block runs under the relaxed/mutated rule — e.g. a base-fee validation bypass leaking lets an underpriced non-exempt tx into the block. The trigger is a two-tx sequence in one block: one tx that engages the toggle (or errors inside it), followed by one that is only valid/invalid depending on the leaked state. Derive it explicitly.314- **Cross-ref scope mismatch**: one ref scoping the mutation per-tx and the other per-block (or not mutating at all) diverges block content and validation for the *follow-up* txs, even when the toggled tx itself executes identically — check the txs after the trigger, not just the trigger.315- **Observability double-check**: a correctly-restored toggle can still be observable *during* the tx (see the opcode observability analysis above); a validation-only flag that no opcode reads and that restores on every path is the only clean outcome. State which of the two you verified.316317Each leak is a finding whose trigger is a same-block tx sequence — cheap for an adversary or even normal traffic to produce, so default likelihood to critical unless the toggle provably cannot be engaged by user txs.318319**Unusual-opcode trigger sweep** — when constructing trigger inputs, do not stop at mainstream opcodes; adversarial payloads live in the rarely-executed320321…(truncated)