When White Hats Take the Reserve

A cache key that omitted context let forged L-BTC exist, and one authorized peg-out moved nearly the whole bitcoin reserve out.
When White Hats Take the Reserve

On September 6 the Liquid Network accepted a transaction whose enormous confidential L-BTC output rested on a rangeproof that had been checked only in a different context. Two peg-out requests followed, for 2.65138358 and 3,996.01834922 BTC, and the functionary watchmen paid 3,998.66973280 BTC out of the Bitcoin reserve. The defect was a cache key that recorded less than the verifier had checked, and a six-line fix for it had been public in the Elements repository since September 1. The receiving address then wrote messages onto Bitcoin calling its owners whitehats.

Three controls failed and only one of them was mathematical: the cache key let invalid value enter the sidechain, the peg-out path let one authorized withdrawal remove almost all of the bitcoin standing behind circulating L-BTC, and the exploit holder took custody of that reserve before anyone else could contain the defect.

Who holds the keys

The Liquid Federation is a membership body of exchanges, brokers, trading desks, and wallet companies, and membership brings governance participation, not signing keys. The set of parties that can move the reserve is smaller and is described in Liquid’s technical overview: fifteen functionary operators each run a hardened machine with a hardware security module, carrying two roles. As blocksigners they sign Liquid blocks on a rotating schedule. As watchmen they participate in the multisignature controlling the bitcoin held against circulating L-BTC. Both roles require eleven of the fifteen, and the published functionary code uses these terms. Blockstream writes most of the software and coordinates incident response, while the eleven signatures stay spread across independent operators, so stopping the chain, choosing a history, and restoring the peg are joint decisions. Bitcoin’s own rules held throughout: the watchmen signed a payment Bitcoin was correct to accept against a validly authorized request.

A two-part key for a four-part check

Liquid hides amounts using Pedersen commitments, which are additively homomorphic, so a verifier can add the input commitments, subtract the output commitments and fees, and require the result to be the commitment to zero. That is not enough on its own, because values live in a finite field, so a large enough amount wraps around and behaves like a negative number, letting a transaction balance while holding one absurdly positive output and one negative counterpart. The rangeproof closes that gap by proving in zero knowledge that a committed amount sits inside a permitted nonnegative range. Confidential Assets adds a second dimension, since each asset type has its own generator point, so verification takes four inputs: the proof bytes, the value commitment, the asset generator, and extra committed data including the output’s scriptPubKey. A proof valid for one asset under one script establishes nothing about a different asset under a different script.

Rangeproof verification is expensive, so Elements caches successful checks. In the current master lineage, commit f81122f made confidential validation context-aware, passing the proof, value commitment, asset commitment, and scriptPubKey into VerifyRangeProof, and 0b506614 introduced and activated the cache in March 2019 while keying entries on only the proof and value commitment, so a hit returned success before the verifier examined the omitted asset and script context. For seven years the code filed the answer to a four-part question under a two-part label.

Exploitation follows from that mismatch. Prime a node so that it verifies a proof and value commitment in a context where both are genuinely valid, and it stores success under a key built from those two fields alone. Present an output reusing the same bytes under a different asset and script, and the primed node reports that the proof holds, while a node that never saw the priming transaction, or that has restarted since, performs the full check and rejects. Validity stopped being a function of the transaction bytes, because cache entries are process-local and salted, so two honest operators running identical binaries could disagree about the same block, since a performance optimization had become consensus code while being reviewed only as an optimization.

The transaction that used this is f24a4b17…183f in Liquid block 4,050,336. A public reconstruction describes one enormous positive confidential L-BTC output with a sound proof, paired with an unspendable output carrying a compensating negative commitment whose rangeproof is invalid in the context where it was recorded. The balance equation was satisfied, and the bound that should have rejected the negative leg was answered from cache. Because the defect sat in shared confidential-asset validation, it applied in principle to any confidential asset, while the publicly observed exploit inflated L-BTC only and no public evidence shows that another issued asset was touched. The cryptography came through intact, and the fix is short because it repairs the identity of the cached statement, binding the key to the proof, the value commitment, the asset commitment, and the script.

Public patch, no tagged release

The fix carries a git author date of August 3 and became public on September 1, when pull request #1592 merged as part of a bundle titled “blind/blindpsbt fixes”, presented as small issues surfaced by automated code scanning. It was cherry-picked to elements-23.x and elements-23.3.x before the exploit, and no tagged release contained it, so the latest stable software on the day was Elements 23.3.3. A consensus validation fix buried as one line item in a cleanup bundle, and backported without an advisory, belongs in its own commit, under embargo or shipped immediately as a tagged emergency release with confirmation that the signing set has upgraded.

Git history proves the public source chronology and nothing about which binaries individual functionaries ran. A circulating claim that the September commit introduced the bug, and that this proves the functionaries ran bleeding-edge code, is contradicted by the diff and the history: c26d719 adds the two missing fields to the key, and its parent is vulnerable back to the March 2019 commit. Liquid blocks record which functionaries signed and carry no trace of a binary or branch, so the exploit proves only that enough signing infrastructure followed the primed, vulnerable path. Vulnerable stable software with a warm cache is indistinguishable from a build carrying unreleased changes, because the deciding state sat in process memory.

From invalid L-BTC to real bitcoin

Inventing L-BTC is a sidechain problem, and turning it into bitcoin required the watchmen to sign, which they did. Liquid’s peg-out path uses pegout authorization keys: a whitelisted participant authorizes a withdrawal to a specific Bitcoin destination, and the watchmen check that authorization before paying, so PAK establishes that a permitted party asked and whether the burned L-BTC was ever backed sits outside its scope. SideSwap stated publicly that a customer submitted 4,000 L-BTC through its normal peg-out service, that the authorization was valid, and that its own keys and systems were not compromised, and no on-chain artifact contradicts that account. The Liquid peg-out request looks like a large ordinary withdrawal because that is what it was to the watchmen.

Only one relevant guard sits in the watchman software: its accounting module treats a peg-out exceeding the total amount currently pegged in as a fatal ExcessPegout condition. The published code would permit a 3,996 BTC request whenever the tracked total is larger, and the observed payout of 83 inputs totaling 4,019.44426085 BTC is consistent with that coarse boundary. One validly authorized request took almost all the backing, with no effective per-request ceiling, velocity limit, reserve-fraction threshold, or human confirmation in its way. The missing control is a ceiling set as a fraction of total backing, paired with a timelock that a fresh quorum of functionary operators must confirm before the payment goes out, and it needs no change to the eleven-of-fifteen threshold.

After the payout

After the payout, an exchange took place on Bitcoin. Sjors Provoost published a reproducible audit separating messages authenticated by a spend from the holding address, or by Blockstream’s PGP key, from the unsigned OP_RETURN noise around them. In the authenticated subset the holder writes that they are whitehats and asks to be contacted on chain, Blockstream asks them to write to its security address, the holder says it is sending most back and asks that the bug be fixed and every node patched first, and Blockstream replies, in a clearsigned message, “Yes, thank you.” All of it happened after the reserve had moved. As of 08:45 UTC on September 7 the holding address still contained 3,998.498796 BTC with no unconfirmed transactions. No authenticated public message states terms, and “most back” implies something kept. Blockstream and the functionary operators have published no recovery plan, so restitution, the backing behind circulating L-BTC, and any final loss figure remain unresolved.

The label the holder chose carries a standard: take custody only when no less harmful effective intervention is available. The 2014 sweep of Blockchain.info wallets met it. Repeated ECDSA nonces exposed private keys, and Jochen Hoenicke swept the vulnerable coins, returned roughly 255 BTC to their owners, and documented the sweep as he went, while those coins sat on Bitcoin, where nobody could pause the chain, freeze the affected outputs, or stop a second party deriving the same keys. Sweeping was the least harmful effective action because it was the only effective action. Liquid offered other options.

What holders can rely on

The chain kept producing blocks after the exploit, and ingress was disabled afterward, leaving coinbase-only blocks: at the 08:45 UTC snapshot the public explorer showed a tip at block 4,051,232 carrying a single transaction. The pause worked and it arrived after the damage. Users’ keys still prove control of their Liquid outputs on whatever history the functionary operators settle on, and that guarantee is bounded, because cryptographic control of a UTXO does not manufacture bitcoin that has left the reserve. Recovery is heavier than installing a patch, since a corrected verifier rejects the exploit block on a cold cache, and it may require choosing a canonical history, reindexing or rolling back, reconciling what followed the fork point, and restoring or replacing the missing backing before peg-outs can be honored on demand.

What disclosure would have looked like

Responsible disclosure had a concrete shape here, available to anyone who read the diff. Send Blockstream’s published security channel enough private technical detail to reproduce and contain the defect: the mismatch between the cache key and the verifier’s inputs, the priming sequence, and, if proof of impact was needed, a demonstration built with a trivial amount. Then allow a bounded emergency response, which on this network means disabling ingress and standing down blocksigners while patched builds go out, a step that needs only five of the fifteen to decline to sign. If the first recipient ignored the report, the defect still had to stay confidential, and the alert had to widen privately to the independent functionary operators or anyone else positioned to stop signing or shut ingress. Publishing exploit details while the network remained vulnerable was never an option, since it would hand a working mint to every party able to run it and could itself cause the loss. If every private path failed and theft by someone else was demonstrably imminent, a tightly bounded protective intervention could become the least harmful remaining option, and it would have to move the smallest amount that removes the threat, preserve evidence, impose no bargaining terms, and return control promptly. Taking nearly the whole reserve because maintainers were slow clears none of those conditions.

Instead, consensus code was exploited in production to mint invalid units on a chain carrying real user balances, after which almost the entire backing moved to an address the actor controls. Assuming the exploit holder made no private report to Blockstream or to Federation functionary operators first, and no reviewed evidence settles that question either way, this was not responsible disclosure. Users were exposed to a loss that a private report to a published security address could have prevented, and the same action that caused the loss gave the actor control over whether the peg is restored and on what terms.


Write a comment