Bitcoin signet · protocol walkthrough
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.
Who is on the wire
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.
%%{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
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.
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.
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.
%%{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.