silent-pay coordinates payroll-style payouts from a MuSig2 Bitcoin treasury to recipients addressed with Silent Payments (BIP-352).
A Silent Payment address (sp1… on mainnet, tsp1… on testnet) lets a sender
derive a unique on-chain output for a recipient without any interaction and
without linking payments on-chain.
The treasury is an N-of-N MuSig2 wallet: every signer must contribute before a transaction can be finalized. The coordinator (this app) builds the PSBT and merges signer contributions; the signers hold the keys. DLEQ proofs and output-script verification are mandatory before a transaction is finalized, so the coordinator can trust that each signer's Silent Payment contribution is correct.
The workspace has two crates:
silent-pay— the library plus thesilent-paybinary (the desktop app).sp-demo— deterministic demo/fixture CLIs for testing and receiver-side validation.
- Rust (stable, edition 2021).
justfor the task recipes.- Git access to the forked dependencies the crates pull from:
spdkandrust-psbt(both on theirmusig2-workingbranches). - Slint's desktop runtime dependencies (platform GUI toolkit).
- A Bitcoin Core node for broadcasting. The RPC URL, cookie file, and user/password are configurable from within the app.
Addresses in the shipped demo are testnet Silent Payment addresses (tsp1…).
Launch with:
just payOn startup the app reads its saved configuration (network, data directory, fee rate, dust limit, and Bitcoin Core RPC settings) and, if a wallet file already exists for the configured network, loads it automatically. Wallet, UTXO, and recipient files are stored per network under the configured data directory.
The screens follow the payout lifecycle:
- Treasury wallet — set the network and define the MuSig2 signers (each is
a fingerprint
xfp, a derivation path, and anxpub) or import a descriptor. Save to persist the wallet file. - Addresses — derive and copy the current receive and change addresses; advance the derivation indices as needed.
- UTXOs — record the treasury UTXOs available to spend, mark spent ones, and select which UTXO funds the payroll.
- Recipients — add, edit, and remove recipients (label, amount, Silent Payment address). Save to persist the recipient file.
- Build — build the payroll PSBT and the signer descriptor from the selected UTXO and recipients, using the configured fee rate and dust limit.
- Finalize — load the PSBT returned by the signers, finalize it into a broadcastable transaction, review the transaction hex, and broadcast it over Bitcoin Core RPC.
Configuration changes (data directory, network, fee/dust, RPC settings) are saved back to the app config so the next launch picks them up.
- Clone the repository.
- Install
just. - For broadcasting, point the app at a reachable Bitcoin Core node (RPC URL and cookie file or user/password) and select the matching network.
The demo recipes hand PSBTs off to a Coldcard running MuSig2 + Silent Payment firmware. See the Coldcard firmware PR for that support: Coldcard/firmware#683.
The demo recipes read two justfile variables you can override:
coldcard_path— wherejust payrollwrites the descriptor and PSBTs (defaults to the Coldcard firmware testing data directory,~/src/coldcard-firmware/testing/data).cc_sp_out— where the signed, merged final PSBT is expected (defaults to/tmp/cc-sp-out).
Override at invocation, e.g.:
just coldcard_path=/path/to/out cc_sp_out=/path/to/signed payrollThe demo drives the full coordinator flow against fixed key material: a MuSig2 3-of-3 treasury and the recipients in demo/recipients.toml. It exists for testing and receiver-side validation, not production payouts.
- Build —
just payrollwrites a round-1 PSBT, a cosigner-contribution PSBT, and a Coldcard descriptor intocoldcard_path. - Sign (hardware handoff) — import the descriptor into the signers. Each
signer returns its round-1 contribution (ECDH share + DLEQ proof) and round-2
contribution (public nonce + partial signature). The merged, fully signed
PSBT is expected at
cc_sp_out/musig2-sp-final.psbt. - Finalize —
just finalizefinalizes the signed PSBT into a broadcastable transaction. - Scan —
just scanscans the final PSBT and reports which recipient Silent Payment outputs were detected. - Verify —
just verifyverifies a single recipient receipt against its Silent Payment address and scan key (the shipped recipe usesrecipient-3fromdemo/recipients.toml).
| Step | Reads | Writes / produces |
|---|---|---|
just payroll |
demo/recipients.toml |
round-1 PSBT, cosigner-contribution PSBT, descriptor (in coldcard_path) |
| signers | descriptor | cc_sp_out/musig2-sp-final.psbt |
just finalize |
cc_sp_out/musig2-sp-final.psbt |
broadcastable transaction |
just scan |
cc_sp_out/musig2-sp-final.psbt, demo/recipients.toml |
detected recipient outputs |
just verify |
cc_sp_out/musig2-sp-final.psbt |
receipt verification for one recipient |
Each recipient is a [[recipients]] record:
| Field | Description |
|---|---|
label |
Human-readable identifier for the recipient. |
amount_sat |
Amount to pay, in satoshis. |
address |
The recipient's Silent Payment address (tsp1… on testnet). |
scan_key_hex |
The recipient's scan private key, hex. Demo/receiver-side only. |
scan_key_hex is present so the demo can scan and verify receipts from the
receiver's perspective. Production sender configs must not carry recipient scan
or spend secrets. The demo key material is generated by the gen_demo_recipients
binary and is for testing only.
- Missing recipients file — pass
--recipients <path>(the demo bins default torecipients.tomlin the working directory). - Unsigned / incomplete PSBT — finalization fails until every signer has
contributed. Confirm the merged PSBT at
cc_sp_out/musig2-sp-final.psbtcontains all round-1 and round-2 contributions.