The Duplicate Check Skipped for Speed — Python Bug Hunt
Modelled on Bitcoin Core CVE-2018-17144 (fixed in 0.16.3, September 2018).
- Language: Python
- Layer: Backend
- Difficulty: Medium
- Concepts: Security, Validation, Money
- Modelled on: Bitcoin Core · CVE-2018-17144
- Visible tests: a plain valid block is accepted and applied; a transaction spending one output twice is rejected
- Reward: 50 XP for a complete fix
Briefing
Modelled on Bitcoin Core CVE-2018-17144 (fixed in 0.16.3, September 2018). A performance optimisation stopped block validation from running the duplicate-input check on each transaction, on the assumption that a later step would catch it. It did not: a block containing a transaction that spent the same output twice could crash nodes, and on some versions could have been accepted — creating coins from nothing.
This project is a reconstruction: block.py validates a block against a UTXO set and calls the locked tx_check.check_transaction with duplicate checking turned off. Its input loop totals each listed input without removing it first, so a repeated input is counted twice.
Fix validate_block so no transaction with a repeated input can be accepted.
Bug report
BUG-CVE-2018-17144 · Priority: Critical (consensus) · Reported by: security@
validate_block(block, utxo) — block = {"txs": [{"id", "inputs": [outpoint…], "outputs": [amount…]}]}; utxo maps outpoint -> amount. Returns {"ok": bool, "reason": str}; reasons, checked per tx in this order:
- tx_check.check_transaction's reason (it reports "bad-txns-vin-empty", "bad-txns-vout-negative" and, when asked, "bad-txns-inputs-duplicate")
- "bad-txns-inputs-missingorspent" — an input not in the (staged) UTXO set
- "bad-txns-in-belowout" — outputs sum to more than the inputs
Transactions are applied in order, so a later tx may spend an earlier tx's outputs ("<txid>:<index>"). On success utxo is updated in place (inputs removed, outputs added) and reason is ""; on ANY failure utxo is left exactly as it was.
Observed: a tx listing the same outpoint twice is accepted and its outputs carry twice the input value.
Logs
[validation] block 000…a7f accepted: tx f00d inputs=[c0ffee:0, c0ffee:0] in=100 out=200
[validation] total supply check: +100 unexpectedThe code as shipped
src/chain/block.py (editable)
tx_check = bug_require("./tx_check.py")
def validate_block(block, utxo):
staged = dict(utxo)
for tx in block["txs"]:
err = tx_check.check_transaction(tx, False)
if err:
return {"ok": False, "reason": err}
total_in = 0
for outpoint in tx["inputs"]:
if outpoint not in staged:
return {"ok": False, "reason": "bad-txns-inputs-missingorspent"}
total_in += staged[outpoint]
if sum(tx["outputs"]) > total_in:
return {"ok": False, "reason": "bad-txns-in-belowout"}
for outpoint in tx["inputs"]:
staged.pop(outpoint, None)
for i, amount in enumerate(tx["outputs"]):
staged[tx["id"] + ":" + str(i)] = amount
utxo.clear()
utxo.update(staged)
return {"ok": True, "reason": ""}
Read-only context: src/chain/tx_check.py.
Open the hunt to edit the files, run the visible tests and submit against the hidden ones. More Python bug hunts.