agents/skills/injectable/l1/fork-choice-audit/SKILL.md
L1 trigger - audits fork-choice rule implementation (LMD-GHOST, Tendermint locking, Nakamoto longest-chain) for equivocation handling, slot-vs-block reasoning, duplicate block handling, and chain reorg correctness.
npx skillsauth add plamentsv/plamen fork-choice-auditInstall this skill globally with one command. Works with Claude Code, Cursor, and Windsurf.
3 of 9 scanners reported clean
Some scanners were skipped, did not run, or reported a non-clean status. Review each row below.
L1 trigger:
L1_PATTERN=trueAND (fork_choice/ORghost/ORlmd/ORconsensus/tendermint/ORfork.rsORchoice.rsdetected in recon subsystem map) Inject Into:depth-consensus-invariantLanguage: Go and Rust Finding prefix:[FC-N]Status: v0.1 draft, Round 4 exemplars pending
Recon identifies a fork-choice module. This skill applies whether the protocol uses LMD-GHOST (Ethereum beacon chain), longest-chain (Bitcoin, pre-merge Ethereum), Tendermint locking (Cosmos), or a custom fork-choice rule.
Before auditing, determine which rule is implemented. Extract from spec docs and code:
| Rule | Signature | Key invariants | |---|---|---| | LMD-GHOST | "latest message driven — greedy heaviest observed subtree" | Each validator's latest attestation contributes to a subtree weight; choose heaviest | | Casper FFG + LMD-GHOST (Ethereum) | LMD-GHOST bounded by finalized checkpoints | Never revert past justified/finalized checkpoint | | Tendermint BFT | Round-based, locking on 2/3+ prevotes | Locked validator cannot vote for conflicting proposal in same round | | Nakamoto longest-chain | Heaviest accumulated work | Chain with most work wins; stale blocks discarded | | HotStuff / Aptos / Sui | 3-phase: prepare, precommit, commit | No two conflicting QCs at the same view |
Write the identified rule into the finding header so reviewers know which invariants apply.
Check: Enumerate every call site of find_head() / get_head(). For each, verify the return value is bounded by the latest finalized checkpoint.
Check: Follow the lock_value, lock_round, valid_value, valid_round state variables. Verify every set_lock call is guarded by the 2/3 check.
Equivocation = a validator signing two conflicting attestations. Fork choice must:
Check:
Known exemplar class: Cosmos / Tendermint "Denial of Validators" attacks — multiple historical bugs where equivocation detection could itself be exploited to halt the chain.
Tag: [EQUIVOC:{loc}:{handling}]
When fork choice selects a new head, the node must reorg: revert state from the old head back to the common ancestor, then apply blocks from the common ancestor to the new head.
Check:
Tag: [REORG-GAP:{state-category}]
For every helper that returns blocks to apply during a reorg
(get_fork_blocks, fork_blocks, blocks_to_apply, ancestor_path,
rollback_path):
Tag: [REORG-ORDER:{function}:{expected}->{actual}]
Known exemplar class: Solana Sept 2022 stuck validator bug — fork choice could not revert to heaviest bank when its slot matched the last voted slot.
A subtle class of bugs comes from conflating slot (time unit) with block (proposed content). Rules of thumb:
Check: Ast-grep for "slot" in fork-choice code. For each, ask: does the code distinguish "no block at this slot" (valid) from "block not yet arrived" (network delay)?
Apply the sweep specific to fork choice:
| State | Test | Expected | Observed | |---|---|---|---| | Genesis | fork choice on only genesis block | returns genesis | | | Single child | one block building on head | returns the child | | | Tie | two children with equal weight | deterministic tie-break | | | Deep reorg | new head requires reverting 50+ blocks | completes without state corruption | | | Finality boundary | new head would revert finalized block | rejected | | | Equivocator weight | equivocator's vote equals non-equivocator's vote | equivocator excluded, non-equivocator wins | | | All votes missing | no attestations in latest epoch | returns last-known head | |
Tag: [BOUNDARY:fork-choice:{test} → {result}]
[CONFORMANCE-PASS] (spec test against reference) > [NON-DET-PASS] > [DIFF-PASS] > [LSP-TRACE]Solana duplicate-block fork-choice stuck-validator (September 30, 2022) — a validator's hot-spare malfunctioned and generated duplicate blocks for the same slot. Fork choice had no rule for same_leader + same_slot + different_payload. Consensus stalled until coordinated restart. Helius outage history. Skill catch point: Section 3 — enumerate leader-duplicate scenarios (same slot/different parent, same slot/same parent/different content, same slot/two leaders); assert fork choice resolves each without liveness loss.
Prysm Fusaka outdated-attestation state replay DoS (December 4, 2025) — Prysm v7.0.0 unnecessarily generated old beacon states while processing outdated attestations. Out-of-sync attestations referencing historical block roots triggered thousands of costly state replays, dropping validator participation to 75% and costing 382 ETH in missed rewards. Rated High not Critical because 9 other clients continued validating. Crypto.news analysis; postmortem. Skill catch point: new methodology step — identify every path that triggers historical state replay, assert the trigger is bounded by a recent-slot check. Stale attestations must be rejected before any state replay.
Lighthouse fork-choice timing bug (v2.5.0, fixed in v2.5.1 "Slippy") — two separate bugs introduced in v2.4.0 and v2.5.0 causing intermittent fork-choice errors. Self-resolved within seconds but noise in logs indicated deeper race conditions. Release v2.5.1 "Slippy". Skill catch point: Section 2a — for each tick-driven fork-choice update, verify ordering of: new block arrival, attestation arrival, slot tick, head update. Fuzz all permutations.
Tendermint lite-client bisection safety gap (issue #3244) — bisection binary-searched for blocks where validator set voting power changes by <1/3. With sufficient validator-set flux, an attacker could fool a lite client into accepting an invalid header at zero cost. tendermint #3244; attack spec. Skill catch point: for any skip/jump verification, assert the security argument depends on slashing-accountability of faulty validators between trusted and target heights, NOT on honest-majority assumption at target alone.
Add to Section 2a: The Prysm Fusaka lesson generalizes — any fork-choice handler that replays historical state on attacker-controlled input is a DoS vector. Specific check:
Tag: [FC-STALE-DOS:{handler}:{replay-trigger}]
git logconsensus-safety-invariants, bls-aggregation-audit (for attestation signature check), state-sync-pruning (reorg clears sync state)depth-consensus-invariantdocs/l1-mode/severity-matrix.mddevelopment
Prepare Solidity projects for a security audit — test coverage, test quality, NatSpec docs, code hygiene, dependency health, best-practice enforcement, deployment readiness, and project documentation checks. Generates a scored Audit Readiness Report and optionally runs static analysis. Trigger on: "prepare for audit", "audit readiness", "pre-audit check", "audit prep", "NatSpec check", or any request to review a Solidity codebase before a security review.
development
Launch the Plamen deterministic Web3 security audit pipeline
development
Run the Plamen smart-contract audit wizard in Codex
testing
Launch the Plamen deterministic L1 infrastructure audit pipeline