ADR-0020: Sequencer hand-off — a copy left behind must not sign, and a batch must be claimed before it is signed
ADR-0004 makes the sequencer's identity a key, and ADR-0012 makes two receipts from that key for one (batch, position) with different contents a slashable equivocation — demonstrated with a real slash. That is…
Status: accepted (accepted in part). Dated 2026-09-14.
Status: accepted in part, 2026-09-14. The first half is built; the second is the owner's decision and is stated with its cost.
The hazard
ADR-0004 makes the sequencer's identity a key, and ADR-0012 makes two receipts from that key for one (batch, position) with different contents a slashable equivocation — demonstrated with a real slash. That is the right rule against a dishonest sequencer, and it does not care why the two receipts exist. Two honest nodes running under one key produce exactly the same evidence:
- A copy left behind. A standby is provisioned from a datadir copy; the primary keeps sealing. If the standby is ever started from that copy, its counter says "next batch N" while L1 has moved on to N+k, and its first block re-issues batch N's numbers under the same key. Every receipt it hands out is a slashing proof against itself, paired with a receipt the primary already gave someone else.
- Two at once. Primary and standby both up, both at N. Each seals batch N locally and signs its receipts; the first to reach L1 wins the open_batch, the second's settlement fails — but the receipts were signed before either reached L1, so the equivocation exists whichever one loses.
DEPLOYMENT.md §9 item 8 asked for "a standby that can take over the receipt identity without equivocating (signed hand-offs)". A signed hand-off record does not by itself prevent either case: a record says what the primary did last, and the hazard is what the standby does next on stale or concurrent knowledge.
Built: the copy left behind cannot sign
Two changes in the node, 2026-09-14:
- Before signing anything, the node compares its own next batch with L1's. stale_copy_check(local_next, feed_next_seq): the DA feed's next_seq is the only batch number L1 will open next, so it is also how far any node has carried this identity. Level or ahead is a node resuming its own work — the unsettled blocks re-queue as before. Behind is a copy left behind, refused by name with the cure: bring the datadir up to date, never run two nodes under one key. No override flag, because there is no honest reason to sign from behind.
- A datadir without its identity file resumes its batch counter from the log, not from zero. Batch k carries block k+1, so the next batch is the number of blocks logged. Before this, a datadir rebuilt from the published bytes — the runbook's own recovery path — would have opened batch 0 again under a key that had already sealed it, which is the first hazard reached by a different road.
What this closes: the stale-standby case, in the node, before any receipt is signed. What it does not close: two nodes at the same height at the same time.
Proposed: claim the batch on L1 before the first receipt in it
The structural fix for "two at once" is to make the batch number a claim on L1 before it is signed for, not after. The DA program already has the claim: open_batch(k) succeeds once. Today the node signs receipts for batch k as transactions arrive and opens k on L1 only when it settles, so there is a window — the whole life of an open batch — in which a second node can sign the same numbers. If the node instead sent open_batch(k) when it opened k locally, and accepted no transaction into k until that landed, a second node's open_batch(k) would fail and it would accept nothing: no receipt for k could exist from two nodes, because only one of them could ever open k.
What it costs. One more L1 transaction per block in the combined mode ADR-0017 introduced to save exactly that: the combined settle packs open, chunk and seal into one transaction, and an early open splits it into open now and chunk-plus-seal later — about 5,000 lamports per block at the base fee, against the ~4 M lamports a block costs today with rent still sunk, and against the 5,000 flat fee a one-transaction block earns. It also puts one L1 round trip in front of the first admission of every block, which is latency the receipt's soft confirmation currently does not pay.
What it buys. The one property a standby needs and no record can give it: two honest nodes under one key cannot both sign. With it, a hand-off is an operational procedure with no protocol content — stop the primary, start the standby, and the worst a mistake can do is fail to open a batch.
What it changes for the receipts. Nothing in their bytes. A receipt still names (chain, batch, position, slot); the batch it names is now one the signer holds on L1 at the moment of signing, which is a stronger statement than today's, not a different one.
This is the owner's call because it moves cost and latency, and it belongs with ADR-0017 §6.2's block-interval decision: a longer block interval amortises the extra transaction the same way it amortises everything else.
Until then: the operating rule
One key, one running node, always. Provision a standby from the datadir and without the identity file (sequencer.json) — a standby that cannot sign cannot equivocate. To fail over: stop the primary, confirm it is stopped, copy the identity file over, start the standby; the stale guard above refuses the start if the copy fell behind, and the log resume above makes a rebuilt datadir start at the right batch. What the rule does not survive is a primary that is not actually stopped, which is what the claim on L1 is for.
Not decided here
Whether the claim, once built, should be the DA program's open_batch or a separate lease PDA with an expiry, so a dead primary's open batch does not block a standby until someone abandons it. abandon_batch exists and is permissionless; whether that is enough is a question for the drill that DEPLOYMENT.md lists and nobody has run.