fix(dbtc): fence every door that mints a publicly-derivable Bitcoin output - #753
Merged
Conversation
…utput
The shipped tap derives its Bitcoin spend authority from public data: the
manifold seed is BLAKE3 over a compile-time policy commit ("pure math ... no
per-device randomness"), and the deposit nonce is published in the vault
advertisement. Anyone who can read an unauthenticated storage node can
reconstruct the claim private key and sweep the tap.
dbtc_public_data_attack.rs reproduces it end to end, deriving a valid
secp256k1 key and proving the funded script commits to it. Test A is the
standing mutation control for the redesign.
Three doors manufacture such an output and all three now refuse
independently. The third was found by a broadcast-site sweep and is not in
the original write-up: fractional exit's pour_partial() re-locks the
remainder into a successor P2WSH. Its fresh OsRng deposit nonce fixes
nothing, because the advertisement publishes it and rejects an empty one.
Exit-direction routes stay open so already-funded taps are not stranded, and
the fenced partial exit names bitcoin.full.sweep in its refusal. Disposition
of funded taps remains an owner decision.
Each fence was removed individually: every mutation turns only its own door's
test red, by reaching real work — tap creation, the vault lookup, and in the
fractional case the network.
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
PR A of the dBTC tap redesign: fence the doors that manufacture a Bitcoin
output behind a publicly-derivable key, and pin the defect with tests that go
red if it is ever restored. No migration path, no compatibility path, no
override.
The defect, reproduced
dsm_sdk/tests/dbtc_public_data_attack.rsderives a valid secp256k1 privatekey controlling a tap's HTLC claim branch from public data alone — and proves
it is the key the funded script actually commits to:
BitcoinTapSdk::derive_preimage_from_deposit_nonceispub, takes only thosetwo public values, and its own doc advertises it as the path for nonces read
from storage-node advertisements. Test B pins the consequence: the depositor
and a stranger with no wallet, device or relationship derive the identical
spend authority.
This is not an implementation slip — it follows from the old formal
definitions. A future bearer redeems, the depositor is absent, the witness is
never transferred or stored, and it is reconstructed from vault/policy bytes.
The only object satisfying all of those at once is a witness made of public
data.
Three doors, not two
The plan named two. A reachability sweep of every on-chain broadcast site found
a third, and it is a genuine new-exposure door:
The successor draws a fresh
deposit_noncefromOsRng, which looks like itfixes the problem and does not: the advertisement publishes that nonce and
bitcoin_tap_sdk.rs:3462refuses an empty one, so the successor claim key is afunction of public data again. A partial exit is therefore also a manufacturing
door — it re-locks value behind the same defect.
All three refuse independently. A fence on tap creation is not a fence on
funding, and neither is a fence on successor rotation.
What stays open, deliberately
Exit-direction routes move value OUT of a vulnerable tap and are left alone, so
nothing is stranded:
bitcoin.full.sweep(verified to create no successor),bitcoin.sweep.recover,bitcoin.claim.auto,bitcoin.deposit.refund. Thefenced partial exit names
bitcoin.full.sweepin its refusal so the exit isdiscoverable.
exit_direction_routes_are_not_fencedasserts each of those fourdoes NOT emit the fence token.
Disposition of already-funded taps stays an owner decision — this cut does not
make it, it only keeps it possible.
bitcoin.tx.broadcastis a generic raw-transaction relay and is NOT fenced:sending BTC to an address is something any wallet can do, and the property at
issue is DSM manufacturing and advertising the vulnerable output.
Mutation controls — three, each red by performing the forbidden action
Each fence removed individually. Every mutation turns exactly its own door's
test red while the other two stay green, which is what proves independence:
the_fence_precedes_argument_validationpins the ordering from the other side:with empty args an unfenced route reports a decode failure, so the fence must
win instead. Restored ⇒ 5/5 green.
Test A is the standing mutation control for the whole redesign: restore any
construction where the tap key is a function of public data and it goes red by
producing a spendable key.
Verification (exact gate commands, pinned 1.98.0)