consensus: banned-attacker rule is output-only, but its stated threat model is a resurrected UTXO #76

Open
opened 2026-08-21 17:20:25 +00:00 by dobbscoin · 0 comments
dobbscoin commented 2026-08-21 17:20:25 +00:00

Summary

Params().BannedAttackerScripts() is enforced in two places, and both check only vout:

  • src/main.cpp:893 — AcceptToMemoryPool, rejects a tx that pays a banned script
  • src/main.cpp:2356 — ConnectBlock, same check over every tx in the block

Good: it is enforced at the block level too, so it is consensus and not merely mempool policy.

The gap is what the rule is for. From chainparams.cpp:

Our chainstate (recovered from Wayback, tip block 966,413, June 2015) PREDATES the attack by ~870K blocks — these addresses hold ZERO in our UTXO set. The ban is a policy invariant against any future chain-rewrite or resurrected pre-attack UTXO from re-emerging value at these scripts.

A resurrected pre-attack UTXO does not arrive by someone paying one of those scripts. It arrives because a reorg restores outputs that already existed on the original chain. Nothing creates a new vout in that path, so neither check fires — and once those coins are in the UTXO set they are freely spendable, because nothing inspects the scriptPubKey of a spent prevout.

So the rule as written prevents new value being sent to the 533,983-OFF wallets, but does not freeze value that appears at them — which is the one scenario the comment names.

Severity: low

The named scenario is already blocked twice over, independently of this rule:

  • the hardcoded checkpoint at h=976,000 buries the relevant history
  • MAX_REORG_DEPTH = 100 in ActivateBestChain rejects any reorg deeper than 100 blocks past the active tip

The attack is ~870K blocks behind the recovered tip, so reaching it would require defeating both. This is a defense-in-depth mismatch between what the comment promises and what the code does — not a live exploitable path.

Suggested fix

An input-side check in ConnectBlock, where the spent coins are already in hand via the CCoinsViewCache: for each non-coinbase input, look up the prevout's scriptPubKey and reject if it is in the banned set. Roughly a dozen lines next to the existing vout loop.

Two caveats worth deciding on before anyone writes it:

  1. It is technically a consensus tightening — it can only make otherwise-valid blocks invalid. It is inert today, in the same sense the existing rule is inert, because the banned scripts hold zero in the UTXO set and cannot be spent from. But it should ship as a versioned rule alongside the other Restoration gates rather than as a quiet patch.
  2. Decide whether the invariant is worth enforcing at all, given the checkpoint and reorg-depth defenses already cover it. Closing this as "documented, mitigated elsewhere" and fixing the comment instead would be a perfectly reasonable outcome — the comment currently claims more than the code delivers, and that is the part that actually misleads.

How this was found

Read during a bug sweep of the OFF-specific consensus surface, 2026-08-21. Also checked and found correct in the same pass: the banlist persistence in #17, the per-peer sync height in #18, LWMA-3 including its bootstrap walk-back, the mid-grind emergency-valve template rebuild in miner.cpp, and the MAX_REORG_DEPTH common-ancestor walk.

### Summary `Params().BannedAttackerScripts()` is enforced in two places, and both check **only `vout`**: - `src/main.cpp:893` — `AcceptToMemoryPool`, rejects a tx that *pays* a banned script - `src/main.cpp:2356` — `ConnectBlock`, same check over every tx in the block Good: it is enforced at the block level too, so it is consensus and not merely mempool policy. The gap is what the rule is *for*. From `chainparams.cpp`: > Our chainstate (recovered from Wayback, tip block 966,413, June 2015) **PREDATES the attack** by ~870K blocks — these addresses hold **ZERO** in our UTXO set. The ban is a policy invariant against **any future chain-rewrite or resurrected pre-attack UTXO** from re-emerging value at these scripts. A resurrected pre-attack UTXO does not arrive by someone *paying* one of those scripts. It arrives because a reorg restores outputs that already existed on the original chain. Nothing creates a new `vout` in that path, so **neither check fires** — and once those coins are in the UTXO set they are freely spendable, because nothing inspects the `scriptPubKey` of a *spent* prevout. So the rule as written prevents new value being sent **to** the 533,983-OFF wallets, but does not freeze value that appears **at** them — which is the one scenario the comment names. ### Severity: low The named scenario is already blocked twice over, independently of this rule: - the hardcoded checkpoint at **h=976,000** buries the relevant history - **`MAX_REORG_DEPTH = 100`** in `ActivateBestChain` rejects any reorg deeper than 100 blocks past the active tip The attack is ~870K blocks behind the recovered tip, so reaching it would require defeating both. This is a defense-in-depth mismatch between what the comment promises and what the code does — not a live exploitable path. ### Suggested fix An input-side check in `ConnectBlock`, where the spent coins are already in hand via the `CCoinsViewCache`: for each non-coinbase input, look up the prevout's `scriptPubKey` and reject if it is in the banned set. Roughly a dozen lines next to the existing `vout` loop. Two caveats worth deciding on before anyone writes it: 1. **It is technically a consensus tightening** — it can only make otherwise-valid blocks invalid. It is *inert* today, in the same sense the existing rule is inert, because the banned scripts hold zero in the UTXO set and cannot be spent from. But it should ship as a versioned rule alongside the other Restoration gates rather than as a quiet patch. 2. **Decide whether the invariant is worth enforcing at all**, given the checkpoint and reorg-depth defenses already cover it. Closing this as "documented, mitigated elsewhere" and fixing the *comment* instead would be a perfectly reasonable outcome — the comment currently claims more than the code delivers, and that is the part that actually misleads. ### How this was found Read during a bug sweep of the OFF-specific consensus surface, 2026-08-21. Also checked and found correct in the same pass: the banlist persistence in #17, the per-peer sync height in #18, LWMA-3 including its bootstrap walk-back, the mid-grind emergency-valve template rebuild in `miner.cpp`, and the `MAX_REORG_DEPTH` common-ancestor walk.
Sign in to join this conversation.
No labels
enhancement
No milestone
No project
No assignees
1 participant
Notifications
Due date
The due date is invalid or out of range. Please use the format "yyyy-mm-dd".

No due date set.

Dependencies

No dependencies set

Reference
SubGeniusFinance/Offerings-to-Cthulhu#76
No description provided.