Bitcoin Bridge
Every other asset in Umbra arrives via shield() — a plain ERC20 transfer
plus a client-computed note commitment. Bitcoin cannot work that way: there is
no ERC20 on the Bitcoin side to transfer. Instead, a Noir circuit
(btc_deposit) proves a real, confirmed Bitcoin signet payment happened,
and a verified proof mints real WrappedBTC — an ordinary ERC20, not a
note — directly to the depositor’s public balance. From there WrappedBTC
is exactly like FXRP or USDT0: shield it into a hidden note, pay,
placeOrder/matchOrders, or withdraw it, no special-casing.
Why a public mint, not a private note
An earlier version of this bridge minted a hidden note directly, with a
manual on-chain claim step. That design had no real EVM-side collateral
locked in ShieldedVault for a BTC-sourced note — if withdraw() had ever
paid one out as if it were genuinely backed, it would have drained other
users’ real collateral. The current design fixes this at the root: the
circuit binds the depositor’s own EVM address (read straight from the
Bitcoin transaction) and mints a real, publicly-backed WrappedBTC balance
first. Only after that does the ordinary note/commitment system take over,
the same way it does for any other asset.
The deposit transaction — a fixed template
The circuit only accepts one exact shape: one input, two outputs.
output 0: OP_RETURN, 0 value, 34-byte script (OP_RETURN + push32 + 20-byte EVM address, zero-padded)
output 1: P2WPKH payment to Umbra vault addressThe OP_RETURN output carries the depositor’s own EVM address in the
clear. Only the real depositor could have produced a Bitcoin transaction
carrying their own address — signed with their own key — so the resulting
mint destination is fixed by the real payment, not by whoever happens to
submit the proof. Anyone can observe a confirmed deposit and construct its
proof, but they can never redirect where it mints.
What the circuit actually proves
Given a bounded header chain anchored at a trusted, on-chain-registered
checkpoint, plus a transaction and its Merkle-inclusion path under one of
those headers: that a real Bitcoin signet payment matching the fixed
template above happened, extracting the recipient address and satoshi
amount directly from it. depositExternal checks this proof, checks the
proof’s checkpoint commitment matches what is currently registered
on-chain, checks the payment nullifier has not been used before, then
mints.
Checkpoint trust: The circuit does not validate Bitcoin full history back to genesis — it trusts one recent header, registered on-chain once as a genesis anchor, the same model BTC Relay and SPV clients use, and proves only that a 6-header chain (K = 6, roughly an hour of signet blocks) correctly extends forward from it to the deposit confirming block. That one genesis value — and every other one-time bridge-config action (trusting the deposit verifier, registering
WrappedBTC) — is held by a real 2-of-3 Safe multisig behind a 48h public timelock, not a single admin key; see the Threat Model . Every checkpoint advance after genesis is a different, much weaker trust story below.
No manual claim, and no manual checkpoint step either
The deposit side needs no clicks from you: the Faucet page BTC card auto-derives your signet address on wallet connect, then auto-builds, signs, and broadcasts the deposit transaction itself the moment your funding confirms. A backend watcher independently scans the vault Bitcoin address and self-registers any deposit whose browser closed before it could report itself.
Advancing the checkpoint past genesis is permissionless, not an admin action: extendCheckpoint accepts a real Noir proof (a second circuit, checkpoint_relay) that a submitted new checkpoint is a valid 6-header extension of the current one, and anyone can call it — no role check, no timelock, since there’s no admin trust to delay. A dedicated worker (btc-checkpoint-relay-worker) does this automatically on a schedule paced to real signet block production, so a fully-confirmed deposit unblocks itself within roughly one signet block rather than waiting on a human to run a script.
Honest current limitation:
extendCheckpointonly accepts strictly linear extension of the current on-chain checkpoint — there’s no cumulative-chainwork comparison across competing forks (the BTC-Relay “most work wins” pattern). On signet specifically, where proof-of-work is intentionally cheap, a determined actor could still attempt to race the honest chain with their own low-effort but individually-valid-proof fork. Seedocs/LIMITATIONS.md#1 for the full disclosure.
Withdrawal funds from a 2-of-3 reserve, not one custodian key
withdraw() for WrappedBTC emits an event instead of moving funds
itself, and a backend relayer fulfills it on real signet — but that
relayer never holds the key that can complete a payout alone. Real signet
deposit inflow lands in a hot wallet, which is periodically swept in full
into a real 2-of-3 P2WSH Bitcoin reserve; every payout is built as an
unsigned PSBT that needs 2 of 3 independently-held signers to sign before
it broadcasts. A leaked hot-wallet key is now bounded to “whatever’s
arrived since the last sweep,” not the bridge’s whole balance, and an
attacker who somehow got 2-of-3 signer keys is still bounded by an
optional configurable payout rate cap.
A public solvency endpoint (custodian + reserve balance vs. outstanding
obligations, plus how many withdrawals are stuck past a configurable age
threshold) makes both the reserve’s real coverage and any stalled
withdrawal independently checkable. What this does not yet do:
automatically reclaim a withdrawal back to the requester if the reserve
signers are simply unavailable — that’s disclosed plainly, along with why,
in docs/LIMITATIONS.md #7.