Liquid posted on Sunday that around 4,000 BTC had left its federation wallet. That is about 95% of what was sitting in there, somewhere near $320 million depending on which hour you price it at, and it went out in a bit over twenty minutes.
Whoever did it wrote we are whitehats. contact us on chain into an OP_RETURN and then waited for someone at Blockstream to notice.
Blockstream's own phrasing was "purported white-hat hackers", which is the most carefully constructed noun phrase anyone has published this month.
Most of the money is back. I'll get to the "most". But the recovery isn't the interesting part — the interesting part is that nobody stole anything you would normally call a credential.
Fifteen keys, all of them honest
Liquid's peg is an 11-of-15 federation. Fifteen functionaries, largely exchanges and trading desks, and moving real Bitcoin out of the reserve needs eleven of them to sign.
That design has a threat model, and it's a perfectly good one. No single custodian can walk off with the reserve. To force a fraudulent withdrawal you'd have to compromise eleven independent organisations across eleven different jurisdictions, which is properly hard.
Nobody compromised eleven organisations. Nobody compromised one. Every functionary signed, every signature verified, and the peg-out they signed was — as far as every piece of software in the room was concerned — an ordinary, fully-backed withdrawal against L-BTC that existed.
The L-BTC didn't exist. The software said it did.
This is the bit I would happily tattoo across a lot of architecture diagrams. A threshold signature scheme answers did enough of the right parties agree? It does not answer was the thing they agreed to true? Those are two different questions, and the second one gets answered somewhere else entirely, by code that has no keys in front of it and gets a fraction of the security attention.
Everyone points at the 11-of-15 when they describe how Liquid is secured. The 11-of-15 worked flawlessly and it did not matter even slightly.
The cache
Liquid runs Confidential Transactions, so amounts are hidden. A hidden amount is only safe if you can prove it's a sensible number, so every output carries a range proof: cryptographic evidence that the amount you can't see is non-negative and doesn't wrap around. Take away the range proof and there is nothing at all stopping you conjuring value out of nowhere. It is, quite literally, the thing standing between an attacker and unbacked coins.
Range proofs are also slow to verify. So Elements caches the results — the same trick as Bitcoin Core's signature cache. Hash the inputs that identify the proof, remember that this one checked out, and skip the arithmetic the second time you see it, because you'll see it at least twice: once in the mempool, once in the block.
The reconstructions going around say the cache key was a salted hash over a raw concatenation, roughly:
key = H(salt || proof || commitment || asset_generator || scriptPubKey)
No length prefixes. No separators. The proof and the script are both variable-length and they sit at opposite ends of the stream, so the boundary between the fields can be slid: shave a byte off the end of one, glue it to the front of the next, and you have a genuinely different tuple that serialises to a byte-identical string. Collide with something that legitimately verified earlier and your invalid range proof gets waved through on somebody else's cached yes.
Worth flagging: Blockstream hasn't put out a full post-mortem, so treat the byte-level shape as the best available reconstruction rather than gospel. The category isn't in dispute — everyone agrees it's a rangeproof verification cache bug — but the exact concatenation is third-party analysis.
And if that shape is right, then this is not exotic cryptography failing. It's the ambiguous-encoding bug from page one of every protocol design document, the one where "alice" + "bobcarol" and "aliceb" + "obcarol" are the same bytes. If you hash a concatenation of variable-length fields without lengths, you have not hashed the fields. You've hashed one string that several different field splits can produce.
I find it hard not to like this bug, in the specific way you can like a bug that cost somebody $47 million. There's no clever lattice attack. It's string concatenation.
The irony is load-bearing
Follow the chain backwards. Confidential Transactions is Liquid's headline feature over plain Bitcoin. Confidential amounts require range proofs. Range proofs are expensive. Expensive verification justifies a cache. The cache is where verification stopped happening.
The privacy feature paid for the shortcut that skipped the check the privacy feature needed.
Five days
Here's the part that has stayed with me longer than the money.
The fix for the rangeproof cache bug was committed publicly to the Elements repository on 1 September, with a commit title that says what it fixes. No tagged release contained it. No production node had it. The drain ran on the 6th.
Bitcoin Core's convention for consensus-critical bugs is embargo: fix it privately, coordinate a release, disclose once nodes have actually upgraded. There is a reason that convention exists, and it isn't ceremony. A public commit to consensus code is a disclosure with a map stapled to it. You have told every reader of the repository where the bug lives, roughly what shape it is, and — crucially — that nothing in production is protected from it yet.
I'll stay off the high horse, because I don't know what the internal timeline looked like, and there's no shortage of reasons a release slips five days. But the practice exists precisely for the scenario where somebody reads your repo faster than your users upgrade, and "public fix on the 1st, drained on the 6th" is not a sequence I can look at and call a coincidence.
The generalisable version, for those of us who will never run a sidechain: your commit is part of your disclosure timeline whether you meant it to be or not. The window between merging a security fix and shipping it belongs to whoever is watching the repository. If that window is measured in days and your fix is described in the subject line, you've run an embargo with the embargo removed.
About that word
The bridge nodes were patched around 01:09 UTC on the 7th. Once that was confirmed, about 3,400 BTC came back.
They kept 598.5 BTC. Call it $47 million, roughly 15%, described as a bounty. Their own bounty, set by them, after the fact, priced from inside the vault, with the counterparty's alternative being losing everything.
A white hat reports the bug. If we're being generous and allowing for the dramatic version, a white hat takes the funds, returns all of them, and then negotiates a reward from the position of somebody who has already returned all of them. Choosing your own fee while still holding the money is a different activity, and the honest name for it isn't a bounty.
Credit where it's genuinely due, though: they moved fast, they returned 85%, and they waited for the patch before handing anything back rather than dumping funds into a still-broken bridge. Compared to the realistic worst case, this ended extremely well for Liquid.
It's just not white-hat, and the industry going along with the framing teaches an unhelpful lesson to the next person who finds a rangeproof bug — because the lesson currently sitting on the table is $47 million, no charges, and the press calls you one of the good guys.
What I'd actually take from it
If your system has a multi-party approval control anywhere in it, go and find the code that decides what those parties are approving. Not the signing code. The validating code. That's where the interesting attacks live, and it's usually the half nobody put on the diagram.
Then go and grep for places you hash a concatenation of variable-length things — session tokens, webhook signatures, cache keys, anything with || and a sha256 near each other. Length-prefix the fields. It's a five-minute change and it is exactly the bug above.
And if you're sitting on a security fix that hasn't shipped: the repository is public, the commit message is a headline, and the clock started when you pushed.
Comments