Bitcoin signet · protocol walkthrough

Anatomy of a blindjoin round

One CoinJoin round, start to finish — the message sequence, the coordinator's phase machine, and the one moment that makes the coordinator unable to link your input to your output.

RSA blind signatures · RFC 9474 two Tor circuits per client fixed denomination

↗ View the source on GitHub · README · releases

The cast

Who is on the wire

You · Alice circuit
Your wallet on one Tor circuit — registers the input.
You · Bob circuit
Same wallet, a fresh Tor circuit — registers the output.
Coordinator
Assembles the round. Blindly signs; never links in → out.
Bitcoin Core
Confirms UTXOs, and broadcasts the finished CoinJoin.
PKARR DHT
Decentralized discovery — where you find the coordinator.

Sequence

One round, message by message

Read top to bottom. The shaded bands are the coordinator's phases. Note how the input arrives on the Alice circuit and the output on the Bob circuit — the coordinator sees both, but not that they belong to the same person.

fig 1 — round sequence
%%{init: {'theme':'base','themeVariables':{'fontFamily':'ui-monospace, Menlo, monospace','fontSize':'13px','actorBkg':'#eef0fc','actorBorder':'#4b57c8','actorTextColor':'#191c26','actorLineColor':'#c2c7d8','signalColor':'#3a3f4d','signalTextColor':'#3a3f4d','labelBoxBkgColor':'#eef0fc','labelBoxBorderColor':'#4b57c8','labelTextColor':'#191c26','loopTextColor':'#3a3f4d','noteBkgColor':'#fdf1c4','noteTextColor':'#4a3c05','noteBorderColor':'#e4d38a','sequenceNumberColor':'#ffffff','activationBkgColor':'#e3e6f5','activationBorderColor':'#4b57c8','primaryColor':'#eef0fc','primaryBorderColor':'#4b57c8','primaryTextColor':'#191c26','lineColor':'#8a90a6'}}}%%
sequenceDiagram
    participant P as PKARR DHT
    box transparent You · one wallet, two Tor circuits
    participant A as Alice circuit
    participant B as Bob circuit
    end
    participant C as Coordinator
    participant N as Bitcoin Core

    Note over A,N: 1 · Discovery
    A->>P: resolve coordinator (denomination, onion, params)
    P-->>A: onion address + round parameters

    rect rgb(230, 245, 242)
    Note over A,N: 2 · Input registration — Alice circuit
    Note over A: pick a UTXO ≥ denom + fee, then BLIND a fresh token
    A->>C: UTXO + BIP-322 ownership proof + blinded token
    C->>N: gettxout — exists, unspent, value ok?
    N-->>C: confirmed
    C->>C: blind-sign the token (cannot see its contents)
    C-->>A: blind signature
    Note over A: UNBLIND → a valid coordinator signature on a token it has never seen
    end

    Note over A,C: ✦ unlinkability wall — blinded token in ≠ unblinded token out

    rect rgb(240, 235, 251)
    Note over B,C: 3 · Output registration — Bob circuit (fresh Tor circuit)
    B->>C: unblinded token + signature + output address
    C->>C: verify signature (RFC 9474), token binds to address + amount
    C-->>B: accepted
    end

    rect rgb(251, 244, 231)
    Note over A,C: 4 · Signing
    Note over C: assemble ONE canonical CoinJoin PSBT — N inputs, N equal outputs + change
    A->>C: GET round tx
    C-->>A: the assembled PSBT
    Note over A: check MY outcome — my output exact, fee ≤ cap, ≥ anonymity floor
    A->>C: partial signature (signs only my input)
    end

    rect rgb(230, 246, 237)
    Note over C,N: 5 · Broadcast
    Note over C: all partial signatures collected
    C->>N: testmempoolaccept, then sendrawtransaction
    N-->>C: txid
    Note over C: round state zeroed — a fresh round begins immediately
    end
      

Why the coordinator can't link you

The whole design turns on one trick. At input registration you don't hand the coordinator your token — you hand it a blinded version it cannot read, and it signs that blindly. You then unblind the result, leaving you with a genuine coordinator signature on a token the coordinator has never seen in the clear.

in (Alice): blinded token → unblind → out (Bob): unblinded token

When you come back on a different Tor circuit to register your output, you present the unblinded token. It verifies — but the coordinator has no way to tie it back to any specific blinded token from input registration. Inputs and outputs sit on opposite sides of a wall it cannot see through. Every hop runs over Tor, so there's no network-level shortcut either.

State machine

What the coordinator is doing at any moment

The round is a small state machine. The happy path is Idle → InputReg → OutputReg → Signing → Broadcast → Idle; anything that goes wrong routes through Blame, which bans the misbehaving UTXO and resets.

fig 2 — coordinator phases
%%{init: {'theme':'base','themeVariables':{'fontFamily':'ui-monospace, Menlo, monospace','fontSize':'13px','primaryColor':'#eef0fc','primaryBorderColor':'#4b57c8','primaryTextColor':'#191c26','mainBkg':'#eef0fc','nodeBorder':'#4b57c8','lineColor':'#8a90a6','textColor':'#191c26','labelColor':'#191c26','edgeLabelBackground':'#fcfdff','background':'#fcfdff','defaultLinkColor':'#8a90a6'}}}%%
stateDiagram-v2
    direction LR
    classDef phase fill:#eef0fc,stroke:#4b57c8,stroke-width:1px,color:#191c26
    [*] --> Idle
    Idle --> InputReg: start round · fresh RSA key
    InputReg --> OutputReg: quorum reached
    InputReg --> Idle: quorum not met
    OutputReg --> Signing: all outputs in
    OutputReg --> Blame: outputs missing
    Signing --> Broadcast: all partial sigs in
    Signing --> Blame: timeout · ban non-signers
    Broadcast --> Idle: tx accepted on-chain
    Broadcast --> Blame: broadcast failed · attribute + ban
    Blame --> Idle: reset
    class Idle,InputReg,OutputReg,Signing,Broadcast,Blame phase
      

The coordinator never rests at Idle — it immediately re-arms a fresh round with a brand-new per-round RSA key, so a new signature can never be correlated against an old one. Once a round broadcasts, its sensitive state is zeroed.