A standalone CoinJoin coordinator and client for Bitcoin signet. Uses RSA blind signatures (RFC 9474) so the coordinator cryptographically cannot link transaction inputs to outputs. Coordinators are discoverable via PKARR DHT and all production traffic flows over Tor hidden services.
MIT licensed. No fees. No company. No terms of service.
- Coordinator announces a fixed-denomination CoinJoin round (default: 0.01 BTC) and the input script types it accepts (P2WPKH, P2TR, P2SH-P2WPKH)
- Participants discover the coordinator via PKARR DHT and reject mismatched coordinators before opening a Tor circuit
- Participants register inputs with BIP-322 ownership proofs (one of the three supported script types) and receive blind-signed tokens
- Participants register outputs using unblinded tokens (on a fresh Tor circuit)
- Coordinator builds the transaction, participants verify and sign
- Coordinator broadcasts the final CoinJoin transaction
- Non-signers are detected, banned, and the round restarts with remaining participants
The blind signature scheme (RFC 9474) makes it cryptographically impossible for the coordinator to determine which input produced which output. Each round uses an ephemeral RSA key whose lifetime is bounded by a Rust type signature: RoundStateInner.rsa_signer: Option<RsaBlindSigner> is set to None at the SOLE FSM chokepoint RoundState::transition_to(Phase::Idle) (see docs/AUDIT-CHARTER.md §5), which triggers the transitive rsa::RsaPrivateKey ZeroizeOnDrop chain. The remainder of the round state — registered inputs, partial signatures, blinded tokens — is zeroized from memory by the same Drop sequence.
As of v1.4, the coordinator accepts mixed-script-type rounds (any combination of P2WPKH + P2TR + P2SH-P2WPKH inputs) under an operator-configurable allowlist. The script type of every input is derived from on-chain script_pubkey and cross-checked against the client's declaration — a malicious client cannot bypass the per-script-type sighash verification by lying about which type its input is.
- Security policy — how to report a vulnerability (
johnturner@gmail.com), audit-readiness status, and the v1.6 supply-chain posture: cosign keyless signing + SLSA provenance + SPDX SBOM on every image and release tarball, base-image digest pinning. - Changelog — release notes per milestone, Keep-a-Changelog format.
- FAQ — common questions about what blindjoin is, what it protects against, and when to use it.
- Protocol specification (draft) — BIP-style normative spec of the coordinator–client wire protocol. Work-in-progress; review and issue feedback welcome.
- Technical design — architectural background and design rationale.
- External audit charter — in-scope modules with file:symbol refs, threat models per module, 9 cross-shape rejection properties, v=2 PSBT handling boundary, RSA SecretKey zeroization window, out-of-scope dependencies, residual risks accepted with rationale, and glossary mapping project terms to plain audit language. Read this first if you're auditing the codebase.
- Contributing — local prerequisites + how to run the integration test suite (where output lands, how to interpret pass/fail/skip/ignored).
The fastest way to run blindjoin is with Docker Compose. This starts bitcoind (signet), the coordinator, and a liquidity bot that auto-joins rounds.
# Clone and configure
git clone https://github.com/johnzilla/blindjoin.git
cd blindjoin
cp .env.example .env
# Edit .env with your signet UTXO details (see below)
# Start the stack
docker compose -f docker/docker-compose.yml upThe coordinator will start, publish its address to the PKARR DHT, and wait for participants. The liquidity bot joins rounds automatically to fill the anonymity set.
Back up the coordinator-keys volume (or coordinator_pkarr.key if you're not using Docker). Losing it creates a new DHT identity; participants holding your old pk:... will no longer discover you. See the volume note in docker/docker-compose.yml.
To get a signet UTXO for the bot, use the signet faucet.
Running just the coordinator (your own bitcoind / remote RPC, no bot): one docker run against the signed :latest image. This runs a clearnet listener bound to loopback, intended to sit behind your own Tor hidden service or reverse proxy (the COORDINATOR_PUBLIC_ADDR is what clients discover). BLINDJOIN_ALLOW_CLEARNET=1 acknowledges the WR-04 guardrail — release builds refuse clearnet otherwise. For a built-in arti hidden service with no external Tor to manage, set BLINDJOIN__COORDINATOR__TOR_MODE=true instead and drop the -p, LISTEN_ADDR, and ALLOW_CLEARNET lines (arti creates the .onion and publishes it to PKARR automatically).
docker run -d \
-e BLINDJOIN__NETWORK__BITCOIN_RPC_URL=http://YOUR_BITCOIND_HOST:38332 \
-e BLINDJOIN__NETWORK__BITCOIN_RPC_USER=YOUR_RPC_USER \
-e BLINDJOIN__NETWORK__BITCOIN_RPC_PASS=YOUR_RPC_PASS \
-e BLINDJOIN__NETWORK__BITCOIN_NETWORK=signet \
-e BLINDJOIN__DISCOVERY__COORDINATOR_PUBLIC_ADDR=YOUR_ONION_OR_HOST:8080 \
-e BLINDJOIN__COORDINATOR__LISTEN_ADDR=0.0.0.0:8080 \
-e BLINDJOIN__DISCOVERY__PKARR_KEY_FILE=/app/keys/coordinator_pkarr.key \
-e BLINDJOIN__COORDINATOR__BAN_FILE_PATH=/app/data/ban_list.jsonl \
-e BLINDJOIN_ALLOW_CLEARNET=1 \
-v coordinator-keys:/app/keys \
-v coordinator-data:/app/data \
-p 127.0.0.1:8080:8080 \
--restart unless-stopped \
ghcr.io/johnzilla/blindjoin-coordinator:latestPin :latest → :1.7.0 (or whatever shipped) if you want reproducible deploys. Back up the coordinator-keys volume.
blindjoin accepts mixed input script types (P2WPKH, P2TR, P2SH-P2WPKH) in a
single round. This maximizes the anonymity set across address types but
creates a chain-analysis signal: a CoinJoin transaction with a wildly
heterogeneous input set is visually distinguishable from a uniform-script
CoinJoin. Privacy-sensitive users who require uniform-script rounds can run
a dedicated coordinator with a single allow_* flag enabled.
The bundled liquidity bot rotates the script type it submits across rounds.
This prevents the bot's UTXOs from forming a uniform-script-type fingerprint
(which would otherwise identify the bot's participation by cross-round
correlation). Rotation is round-robin across the operator-configured
BLINDJOIN_BOT_SCRIPT_TYPES; each run is single-shot and uses a fresh
wallet, so output addresses do not cluster across rounds.
Requires Rust 1.89+ and cargo (the floor is set by arti-client 0.41).
cargo build --workspace
cargo test --workspace --all-targets # unit + integration testsIntegration tests that require a live bitcoind graceful-skip locally when one isn't on BITCOIND_EXE / PATH. Under BLINDJOIN_REQUIRE_BITCOIND=1 (which CI sets) they panic-on-miss instead — see CONTRIBUTING.md for the canonical local invocation.
Requires a running Bitcoin Core node (signet, testnet, or regtest).
# Copy and edit config
cp blindjoin.toml.example blindjoin.toml
# Start coordinator (clearnet, for development)
cargo run -p coordinator
# Start coordinator (Tor hidden service, for production)
BLINDJOIN_COORDINATOR_TOR_MODE=true cargo run -p coordinator
# Release builds refuse to start in clearnet mode unless explicitly acknowledged
BLINDJOIN_ALLOW_CLEARNET=1 ./target/release/coordinator # only if you know what you're doingWhen tor_mode is enabled, the coordinator runs as a Tor v3 hidden service via arti-client. No clearnet listener is created. The .onion address is published to the PKARR DHT automatically.
When tor_mode is disabled (default), the coordinator listens on 0.0.0.0:8080 for development and testing. Release builds (cargo build --release) refuse to start in clearnet mode unless BLINDJOIN_ALLOW_CLEARNET=1 is set — this is a deliberate guardrail against accidentally exposing a production-built coordinator on the open internet without Tor. Debug builds (cargo run, cargo test) emit a warning and continue.
See blindjoin.toml.example for all options. All settings can be overridden with BLINDJOIN_* environment variables.
| Setting | Default | Description | Startup-validated |
|---|---|---|---|
network.bitcoin_network |
signet | signet, testnet4, regtest, mainnet | |
network.bitcoin_rpc_url |
127.0.0.1:38332 | Bitcoin Core RPC endpoint | |
coordinator.denomination_sats |
1,000,000 | Fixed output amount (0.01 BTC) | |
coordinator.min_participants |
3 | Minimum to start a round | |
coordinator.max_participants |
20 | Maximum per round | |
coordinator.listen_addr |
0.0.0.0:8080 | HTTP listen address (clearnet mode) | |
coordinator.tor_mode |
false | Run as Tor hidden service | |
coordinator.blame_ban_duration_secs |
3600 | Ban duration for misbehaving UTXOs | |
coordinator.rate_limit_info_per_min |
60 | Per-route limit for read endpoints (/info, /round/tx); 1..=60_000 |
✓ |
coordinator.rate_limit_writes_per_min |
30 | Per-route limit for write endpoints (/round/input, /round/output, /round/sign); 1..=60_000 |
✓ |
coordinator.request_timeout_secs |
30 | Uniform per-request handler deadline; clients see HTTP 408 on stall | ✓ |
coordinator.max_concurrent_connections |
256 | Cap on simultaneous Tor hidden-service streams; excess connections park | ✓ |
discovery.pkarr_key_file |
coordinator_pkarr.key | Ed25519 keypair for DHT identity | |
discovery.heartbeat_interval_secs |
300 | PKARR re-publish interval | |
bip.allow_p2wpkh |
true | Accept BIP-84 P2WPKH inputs (env: BLINDJOIN__BIP__ALLOW_P2WPKH) |
✓ |
bip.allow_p2tr |
true | Accept BIP-86 P2TR inputs (env: BLINDJOIN__BIP__ALLOW_P2TR) |
✓ |
bip.allow_p2sh_p2wpkh |
true | Accept BIP-49 P2SH-P2WPKH inputs (env: BLINDJOIN__BIP__ALLOW_P2SH_P2WPKH) |
✓ |
bip.output_script_type |
p2wpkh | Script type of round outputs; one of p2wpkh / p2tr / p2sh-p2wpkh. MUST match an enabled allow_* flag (env: BLINDJOIN__BIP__OUTPUT_SCRIPT_TYPE) |
✓ |
✓ Startup-validated entries are checked by CoordinatorConfig::validate() at boot — out-of-range values (e.g. request_timeout_secs: 0, or max_concurrent_connections above the OS file-descriptor cap) cause the coordinator to refuse to start with an actionable error, rather than panicking later under load. The unmarked entries get type-level validation only (TOML/serde parses an integer as an integer, but no semantic bounds are enforced). The four coordinator.* marked entries are the v1.2 Phase 8 DoS-hardening knobs; the four bip.* entries are the v1.4 multi-script allowlist (an all-false bip.* combination, or an output_script_type whose matching allow_* flag is false, refuses to start).
| Method | Path | Purpose |
|---|---|---|
| GET | /info |
Coordinator status, round state, RSA public key, supported_script_types (v1.4+), output_script_type (v1.4+) |
| POST | /round/input |
Register a UTXO input + receive blind signature (accepts BIP-322 ownership proofs for all enabled script types) |
| POST | /round/output |
Register an output using unblinded token |
| GET | /round/tx |
Retrieve unsigned PSBT for verification |
| POST | /round/sign |
Submit partial signature |
Script-type advertisement. /info includes supported_script_types (a JSON array such as ["p2sh-p2wpkh","p2tr","p2wpkh"]) and output_script_type (a single kebab-case string) so clients can fail-fast on a mismatch before opening a Tor circuit. The same data is published in compact form (sst/ost fields) in the coordinator's PKARR record (v0.2.0) so even DHT-discovery callers can skip mismatched coordinators without ever connecting. Both supported_script_types and output_script_type are #[serde(default)] on the wire — pre-v1.4 coordinators with no advertised set are interpreted as ["p2wpkh"] and v1.4 clients fall back to the legacy witness-only OwnershipProof envelope when they detect one.
Errors return a structured JSON envelope:
{
"error": {
"code": "UTXO_SPENT",
"message": "Referenced UTXO is already spent on-chain",
"round_id": "abc123..."
}
}Rate limiting and timeouts. All endpoints are rate-limited per route — reads default to 60 req/min, writes to 30 req/min. When a route is flooded, the coordinator returns HTTP 429 with a Retry-After header and the same JSON envelope, with code: "RATE_LIMITED":
{
"error": {
"code": "RATE_LIMITED",
"message": "Rate limit exceeded for this endpoint",
"round_id": "..."
}
}Handlers that stall past request_timeout_secs return HTTP 408. Clients SHOULD back off according to Retry-After and retry on a fresh Tor circuit. Per-peer throttling is intentionally impossible on Tor (GlobalKeyExtractor — all Tor connections look identical to the coordinator); sybil resistance comes from BIP-322 ownership proofs and the per-round denomination, not from per-IP rate limits.
# Generate a wallet of a given script type (writes descriptors.txt, mode 0600)
cargo run -p client -- --generate-wallet --type p2tr # BIP-86 Taproot
cargo run -p client -- --generate-wallet --type p2sh-p2wpkh # BIP-49 wrapped-segwit
cargo run -p client -- --generate-wallet --type p2wpkh # BIP-84 native segwit (default)
# Direct connection (development) — uses a WIF P2WPKH key (legacy v1.3 path)
cargo run -p client -- --coordinator-url http://127.0.0.1:8080 \
--wif <your-private-key-wif> \
--output-address <destination-address>
# Direct connection with a descriptor wallet (works for all 3 script types)
cargo run -p client -- --coordinator-url http://127.0.0.1:8080 \
--descriptor descriptors.txt \
--type p2tr \
--output-address <destination-address>
# Via Tor (production) — coordinator discovery via [PKARR](https://github.com/pubky/pkarr) DHT
cargo run -p client -- --pkarr-pubkey <coordinator-public-key> \
--descriptor descriptors.txt \
--type p2tr \
--output-address <destination-address> \
--torThe --type flag (also settable via the BLINDJOIN_SCRIPT_TYPE env var) accepts p2wpkh, p2tr, or p2sh-p2wpkh and selects the BIP descriptor template at wallet generation (BIP-84 / BIP-86 / BIP-49 respectively). It also tells the discovery layer which script type to require — pointing a --type p2tr client at a coordinator that has bip.allow_p2tr = false fails fast with DiscoveryError::UnsupportedScriptType before any Tor circuit opens, naming both the coordinator and the missing script type.
When --tor is enabled, the client uses per-phase Tor circuit isolation: input registration flows through one circuit (alice) and output registration flows through a different circuit (bob). This prevents the coordinator from correlating phases by Tor circuit.
Backwards compatibility. A v1.4 client pointed at a pre-0.2.0 (v1.3) coordinator detects the absence of supported_script_types on /info and falls back to the legacy witness-only OwnershipProof wire format. A v1.3 client unaware of v1.4 advertisement happily registers a P2WPKH UTXO against a v1.4 coordinator — the v1.4 coordinator's OwnershipProof decoder is a two-phase try-parse that accepts v1.3 array-of-hex shape as version = 1.
Download pre-built binaries from GitHub Releases (Linux x86_64). Other platforms: build from source with cargo build --release.
Docker images are also published to ghcr.io/johnzilla/blindjoin-coordinator, ghcr.io/johnzilla/blindjoin-client, and ghcr.io/johnzilla/blindjoin-bot. Every tagged image push (v1.6+) carries a cosign keyless signature, a SLSA v1.0 provenance attestation, and an SPDX SBOM attestation. Release tarballs ship the cosign .bundle + SLSA .sigstore companion assets. Verify recipes: SECURITY.md § Supply-chain status.
Every pull request — and every push to main — runs four independent CI jobs:
| Job | Command | Blocks merge? |
|---|---|---|
cargo test |
cargo test --workspace --all-targets |
Yes |
cargo clippy |
cargo clippy --workspace --all-targets -- -D warnings |
Yes |
cargo audit |
cargo audit |
Yes |
coordinator binary builds |
cargo build --release --bin coordinator |
Yes |
The cargo test step runs --all-targets, so integration tests under tests/integration/ execute alongside unit tests. As of v1.3 Phase 9, CI provisions a pinned Bitcoin Core v30.2 binary (cached via actions/cache, integrity-verified against achow101's release signature pulled from a SHA-pinned guix.sigs commit, then sha256-checked against the signed SHA256SUMS). The version pin lives in .bitcoind-version — a single-line bump is all it takes to roll forward. With bitcoind available, CI sets BLINDJOIN_REQUIRE_BITCOIND=1 so any missing bitcoind in CI fails fast rather than silently graceful-skipping. The coordinator binary builds smoke job validates that the production binary links cleanly — it does not start the coordinator (that requires bitcoind).
The cargo audit step uses .cargo/audit.toml to declare accepted residual risks. Each ignored advisory carries a written rationale in that file; an ignore without a rationale is a code-review-blocking change.
Release and Docker workflows also run test+clippy as a prerequisite before building. All GitHub Actions are pinned to immutable commit SHAs. Release archives include SHA-256 checksums.
Base-image digest pinning: docker/Dockerfile pins both base images (debian:bookworm-slim and lukemathwalker/cargo-chef:latest-rust-1) by sha256 digest directly in the FROM lines. To check for upstream drift and bump via a one-line PR, see CONTRIBUTING.md §Bumping base-image digests.
For branch protection setup, see docs/branch-protection.md.
blindjoin/
coordinator/ # CoinJoin coordinator binary
src/
api/ # HTTP handlers (axum)
bitcoin/ # RPC client, UTXO validation, BIP-322, PSBT builder, fee estimation
blind/ # RSA blind signature engine
round/ # State machine, input/output reg, signing, blame
discovery/ # [PKARR](https://github.com/pubky/pkarr) DHT publisher
network/ # Tor hidden service (arti-client)
config.rs # TOML + env var configuration
main.rs # Startup, health checks, server
client/ # CLI participant client
src/
round/ # Input registration, output registration, signing
wallet.rs # bdk_wallet key management, BIP-322 proofs, PSBT signing
http.rs # Coordinator HTTP client (clearnet + Tor)
tor.rs # Per-phase Tor circuit isolation (alice/bob)
discover.rs # [PKARR](https://github.com/pubky/pkarr) DHT coordinator discovery
config.rs # CLI argument parsing (clap)
shared/ # Protocol types shared between coordinator and client
src/
protocol.rs # Wire message structs (serde, forward-compatible)
token.rs # Blind token message computation (domain-separated SHA-256)
bip322/ # BIP-322 Simple dispatcher + per-script-type sign/verify (v1.4)
mod.rs # ScriptType enum, dispatcher, 26-LOC bip322-crate adapter
p2wpkh.rs # BIP-143 ECDSA sign/verify
p2tr.rs # BIP-341 Schnorr keypath sign/verify
p2sh_p2wpkh.rs # BIP-49-wrapped P2WPKH sign/verify
errors.rs # Structured error codes
types.rs # Common types (RoundId, Denomination)
tests/
bip322_cross_shape.rs # 9 cross-shape rejection tests (V1.4-CRIT-01 mitigation)
per_script_vectors.rs # Vendored official BIP-322 vectors per script type
ownership_proof_roundtrip.rs # v1.3 array-of-hex ↔ v1.4 flat-struct compat
liquidity-bot/ # Auto-joins rounds for testing and cold-start
src/
main.rs # Polling loop, signet safety guard
strategy.rs # Join strategy and round participation
docker/ # Docker Compose stack
docker-compose.yml
Dockerfile # Multi-target Dockerfile (coordinator, client, liquidity-bot)
digests.txt # Canonical base-image digest manifest (v1.6) — bumped only via CODEOWNERS-gated PR
bitcoind/bitcoin.conf
docs/
PROTOCOL.md # BIP-style wire-protocol spec (draft; Milestone 1)
branch-protection.md # GitHub Rulesets setup + CODEOWNERS gate
tests/
integration/ # End-to-end CoinJoin round tests
mod.rs # Shared: require_bitcoind!() macro, BitcoindGuard RAII, bootstrap_regtest_bitcoind()
.cargo/
audit.toml # Declared residual risks (each ignore documented inline)
.github/
actions/
install-bitcoind/ # Composite action: pinned bitcoind install with PGP verification
workflows/
ci.yml # PR-triggered test, clippy --all-targets, audit gates + pinned-bitcoind install
release.yml # Cross-compiled binary releases (gated on test+clippy)
docker.yml # Multi-arch Docker image publishing (gated on test+clippy)
.bitcoind-version # Pinned Bitcoin Core version used by CI's integration test job
CONTRIBUTING.md # Local prerequisites + how to run integration tests
TODO.md # Open and recently-resolved tech-debt items
Reporting a vulnerability: email johnturner@gmail.com with subject [blindjoin security]. Full policy: SECURITY.md. The section below describes the protocol-level guarantees and operator-facing hardening.
The coordinator cannot:
- Link inputs to outputs (RSA blind signatures, RFC 9474)
- Steal funds (participants sign their own inputs)
- Reconstruct round data after completion (round-end zeroization is structurally bounded — see docs/AUDIT-CHARTER.md §5)
- Correlate input and output registration by Tor circuit (client uses isolated circuits)
The coordinator can:
- Refuse to complete rounds (participants detect and switch coordinators via DHT)
- Register sybil inputs (fixed minimum participant count dilutes impact)
- See which UTXOs registered (observable on-chain anyway)
Session tokens use HMAC with constant-time comparison. BIP-322 ownership proofs verified for all inputs. Banned UTXOs persisted to disk (SHA-256 hashed, append-only JSONL). No PII logging.
Availability hardening (v1.1): Async RPC calls execute before the write lock so slow bitcoind cannot serialize participants. RSA keys are parsed once per round (not per request). Blinded tokens are size-bounded to the RSA modulus. Addresses are validated at registration time (not at PSBT build). Duplicate partial signatures are rejected.
Public-endpoint hardening (v1.2 Phase 8): Per-route rate limits via tower_governor (reads 60/min, writes 30/min by default) return HTTP 429 with Retry-After and a RATE_LIMITED JSON envelope. A uniform request timeout (default 30s) caps handler runtime — slow clients see HTTP 408 rather than tying up worker slots. Concurrent Tor hidden-service streams are bounded by a tokio::sync::Semaphore (default 256) wrapping the accept loop. All four knobs are operator-tunable in coordinator.toml and validated at startup so a misconfigured value fails fast rather than panicking under load. Per-peer throttling is impossible on Tor by design (all streams share an effective IP), so the coordinator deliberately uses GlobalKeyExtractor; sybil resistance lives in BIP-322 proofs and the per-round denomination, not the rate limiter.
Supply-chain hygiene: TLS is pure-Rust rustls across the entire dependency tree; the openssl crate chain is not pulled in. cargo audit blocks merge on any advisory not declared in .cargo/audit.toml, where each accepted residual risk carries a written rationale. cargo clippy --all-targets blocks merge on any lint, including in integration-test code. CI's bitcoind install (v1.3+) verifies the Bitcoin Core tarball against achow101's PGP signature (key fingerprint 152812300785C96444D3334D17565732E08E5E41, from a SHA-pinned guix.sigs commit). v1.6 adds: base-image digest pinning (sha256 digests inlined into docker/Dockerfile's FROM lines); cosign keyless OIDC signing + SLSA v1.0 provenance + SPDX SBOM on every ghcr.io image push and on every release tarball; pinned cosign-installer at v3.10.1 (cosign 2.6.3); a sigstore-pin-check CI gate that fails the build on a floating sigstore-action tag. Verify recipes: SECURITY.md § Supply-chain status.
External audit charter (v1.5): docs/AUDIT-CHARTER.md enumerates in-scope modules with file:symbol refs, threat models per module, the 9 cross-shape rejection properties locked at v1.4, the v=2 OwnershipProof PSBT handling boundary, the RSA SecretKey zeroization window (RoundSecretKey + bounded lifetime per AUDIT-03), out-of-scope dependencies, residual risks accepted with rationale, and a glossary mapping project terms to plain audit language.
Test infrastructure (v1.3 Phase 9): Integration tests under tests/integration/ no longer silently graceful-skip in CI — under BLINDJOIN_REQUIRE_BITCOIND=1 (workflow-level env), tests that can't find bitcoind PANIC, surfacing the misconfiguration immediately. The historical Box::leak(node) pattern that blocked cargo's stdout pipe behind orphan bitcoind processes is replaced with a BitcoindGuard RAII type whose Drop::drop runs node.stop() via tokio::spawn_blocking. All 8 full_round::* end-to-end tests run by default (six former #[ignore = "TODO(Phase-10)..."] carve-outs were closed once the wire-format mismatch in client/server witness encoding was repaired). See CONTRIBUTING.md for local invocation.
Multi-script script-type integrity (v1.4): The coordinator's validate_utxo derives the ScriptType of every input from the on-chain script_pubkey (not from the client-declared field on the wire) and cross-checks against the declaration; mismatch returns Bip322Error::ScriptTypeMismatch before the per-script verifier ever runs. The shared::bip322 dispatcher is the only public verifier surface — per-script verify/sign functions are pub(crate)-only, so a caller cannot reach p2wpkh::verify from outside the crate to bypass dispatch. A bip322-pin-check CI job enforces the bip322 = "=0.0.10" exact pin (the crate is pre-1.0 and any minor release can break the adapter at shared/src/bip322/mod.rs). 9 cross-shape rejection tests in shared/tests/bip322_cross_shape.rs lock the V1.4-CRIT-01 spoofing-vector closure at the shared/ crate boundary.
| Crate | Purpose |
|---|---|
blind-rsa-signatures |
RFC 9474 RSA blind signatures (jedisct1) |
bitcoin (rust-bitcoin) |
Bitcoin primitives, PSBT, scripts |
bip322 |
BIP-322 Simple verifier (rust-bitcoin org). Exact-pinned to =0.0.10 and enforced by a bip322-pin-check CI gate — the crate is pre-1.0 and any minor release can break the wire format. Wrapped behind a 26-LOC zero-lossy adapter so a future swap is mechanical. |
bdk_wallet |
Client wallet: key management, UTXO selection, PSBT signing |
arti-client |
Tor hidden service (coordinator) and circuit isolation (client). Configured default-features = false with the rustls feature so the TLS backend is pure-Rust and the openssl chain is not in the dep tree. |
pkarr |
Coordinator discovery via Mainline DHT |
axum |
HTTP framework for coordinator API |
tower_governor, tower-http |
Per-route rate limiting (GovernorLayer) and uniform request timeouts (TimeoutLayer) on the coordinator API |
tokio |
Async runtime |
zeroize |
Memory zeroing for sensitive round state |
reqwest |
HTTP client with SOCKS5 proxy support for Tor |