Federated accountable-anonymous e-cash on Bitcoin.
ArcMint issues Cryptographic Bearer Notes (CBNs) — private, threshold-signed bearer instruments backed by BTC and redeemable via the Lightning Network. No wrapped token. No custodian. No trusted third party. Target: institutional OTC desk settlement and high-value private payments.
Live on Bitcoin mainnet. Federation running. Notes in production.
- How It Works
- Architecture
- Security Properties
- Quick Start
- Key Management
- TLS and Certificates
- Bitcoin Anchoring
- Monitoring
- Load Testing
- Adversarial Testing
- Environment Variables
- Known Limitations
- License
- Registration — User samples identity scalar
θ_u ∈ Z_q, computes commitmentT_u = g^θ_u, registers with the gateway, and receives an HMAC token bound toθ_u. - Issuance — Cut-and-choose: user submits
kcandidates with Pedersen bit commitments, coordinator challengesk−1to open, threshold FROST signs the unopened candidate. Notes carry a 90-day validity window. - Spending — User presents note + signature; merchant issues a random challenge vector; user produces a selective opening proof; coordinator checks the double-spend registry atomically.
- Double-spend detection — A second spend of the same serial triggers identity recovery: XOR of blinding factors at differing-challenge positions recovers
θ_u, identifying the cheating party without touching honest users. - Anchoring — Merkle roots of issued and spent serial sets are committed to Bitcoin via
OP_RETURNevery 600 seconds.
| Primitive | Choice | Rationale |
|---|---|---|
| Group | Ristretto255 | Prime-order, cofactor-free, DL-hard |
| Threshold signing | FROST (frost-ristretto255) | EUF-CMA secure, no trusted dealer in production |
| Commitments | Pedersen C = g^b · h^r |
Perfectly hiding, computationally binding |
| MAC | HMAC-SHA256 | Gateway token binding |
| Hash | SHA-256 with domain separation | arcmint:identity:v1, arcmint:note:v1, arcmint:theta:v1 |
| Crate | Role |
|---|---|
arcmint-core |
Cryptographic primitives — FROST, Ristretto255, Pedersen commitments |
arcmint-federation |
Coordinator and Signer binaries |
arcmint-gateway |
User registration, HMAC token issuance, IP rate limiting |
arcmint-merchant |
Note verification, spend proof verification, payment acceptance |
arcmint-wallet |
CLI wallet — identity, issuance, spending, atomic file writes |
arcmint-loadtest |
Real HTTP load testing — register → issue → spend → double-spend |
arcmint-adversary |
Protocol attack testing |
arcmint-dkg |
Distributed key generation ceremony tooling |
arcmint-monitor |
Ratatui TUI — Bitcoin node, service health, anchoring status |
| Service | Port | Role |
|---|---|---|
| coordinator | 7000 | Orchestrates signing rounds, double-spend registry, anchoring |
| signer-1 | 7001 | FROST signer with individual key package |
| signer-2 | 7002 | FROST signer with individual key package |
| signer-3 | 7003 | FROST signer with individual key package |
| gateway | 7002 (internal) | User registration, token issuance, identity resolution |
| merchant | 7003 (internal) | Note and spend proof verification |
| bitcoind | 18443 | Bitcoin Core (regtest) or mainnet pruned |
| lnd | 8080, 10009 | LND node 1 |
| lnd-2 | 10009 (internal) | LND node 2 |
| certgen | — | CA + mTLS certificate generation (init container) |
- Signers only trust requests authenticated by the coordinator's secret. They do not interact with end users.
- The coordinator observes only blinded commitments, serial numbers, and spend proofs — never user identities.
- The gateway manages identity registration and rate limiting but never sees note serials or spend proofs.
- The wallet never shares long-term secrets; it communicates exclusively via the public HTTP APIs.
- The merchant is untrusted by the federation; it must prove note validity through the coordinator's spend verification API.
All inter-service communication uses mutual TLS. The certificate authority is generated by the certgen init container; each service has its own cert signed by the federation CA.
| Property | Status | Mechanism |
|---|---|---|
| Unforgeability | Proven | Reduction to DL hardness + FROST EUF-CMA |
| Traceability | Proven | Pedersen binding — double-spender identified with probability 1 − 2^{−ℓ} |
| Blindness | Heuristic | Holds under static corruption; formal proof under adaptive corruption is an open problem |
| Double-spend prevention | Enforced | BEGIN IMMEDIATE transaction with rollback coverage; atomic registry check |
| Secret isolation | Enforced | zeroize on all sensitive types (Scalar, BlindingFactor, UnsignedNote) |
| Timing safety | Enforced | subtle::ConstantTimeEq for all secret comparisons; all OPERATOR_SECRET variants checked without early return |
| Replay protection | Enforced | Spend proofs bound to merchant nonce + timestamp |
| Note expiry | Enforced | 90-day validity window; reissuance protocol included |
| Protocol versioning | Enforced | protocol_version: u8 on all message types |
| Input bounds | Enforced | MAX_CANDIDATES = 512 on issuance |
| Metrics authentication | Enforced | Bearer OPERATOR_SECRET required on all /metrics routes |
- C1 Hardcoded Bitcoin RPC credentials → env vars, no defaults
- C2 Weak default secrets → blanks with comments;
validate_secret()enforced at startup (min 32 chars) - C3 TLS validation disabled in test clients →
danger_accept_invalid_certsremoved; CA cert required - C4 Unauthenticated Prometheus metrics → bearer token required on all
/metricsroutes - H1 Services binding
0.0.0.0→ default127.0.0.1; Docker setsBIND_ADDR=0.0.0.0 - H2
MERCHANT_INSECURE=trueplaintext option → removed; TLS mandatory
- Docker
- Docker Compose v2
# 1. Configure secrets
cp .env.example .env
# Edit .env — all secrets must be ≥ 32 random bytes; no dev- prefixes in production
# 2. Build and start
docker compose up --build
# 3. Run the load test to verify the stack
docker compose --profile loadtest run --rm arcmint-loadtestAll services expose /health for liveness checks wired into Docker health checks.
Default endpoints (host-side ports configurable via .env):
| Service | URL |
|---|---|
| Coordinator | https://localhost:7000 |
| Signer 1 | https://localhost:7001 |
| Signer 2 | https://localhost:7002 |
| Signer 3 | https://localhost:7003 |
| Gateway | https://localhost:7002 |
| Merchant | https://localhost:7003 |
arcmint-wallet register --identity-id alice
arcmint-wallet generate-note --denomination 1000 --k 32
arcmint-wallet list-notes
arcmint-wallet spend --serial <note-serial-hex> --merchant-url https://localhost:7003In development, FROST keys are generated by the keygen binary using a trusted dealer. Keys are written into a shared volume and mounted read-only into signers, coordinator, and merchant. Development keys are not suitable for production. The trusted dealer path is gated behind #[cfg(feature = "dev-keygen")].
Production deployments must establish FROST keys via the distributed key generation ceremony using dkg_coordinator and dkg_participant. INIT_DEV_KEYS must be disabled.
Ceremony procedure:
- Each of the
nfederation operators runs on a separate machine in a separate jurisdiction. The internal CA certificate is distributed to all operators out-of-band. - Operator 1 starts the ceremony with
--create-ceremony, specifying the agreed thresholdtand signer countn. - All operators run
dkg_participantwith their--participant-id,--operator-token, and coordinator URL. - On completion, each operator prints a transcript hash. Operators verify the hash out-of-band (voice call or secure channel). A differing hash indicates a compromised ceremony — abort and restart.
- Each operator's
signer_{id}_key.jsonmust be stored at mode0600and never transmitted off-machine. The sharedpublic_key.jsonis safe to distribute to coordinator, gateway, and merchant.
Key rotation requires a full DKG ceremony. Old keys remain active until the new ceremony is confirmed live across all services.
Operator tokens must be high-entropy (≥ 32 random bytes), never reused across ceremonies, and destroyed after the ceremony completes.
The certgen init container generates an internal CA, signer server certificates, coordinator and gateway server certificates, and mTLS client certificates. All services mount the /certs volume automatically.
Set ACME_DOMAIN to the externally reachable hostname for automatic Let's Encrypt certificate provisioning. The internal mesh (signers and coordinator) continues to use the internal CA.
ACME_DOMAIN=gateway.example.com
ACME_EMAIL=ops@example.com
ACME_CACHE_DIR=/var/lib/arcmint/acme
Rotate internal certificates by re-running the certgen service and performing a rolling restart of signers, coordinator, and gateway. Long-lived clients must refresh their TLS configuration to trust the new CA before old certificates are revoked.
The coordinator anchors federation state to Bitcoin via OP_RETURN every 600 seconds.
Wallet setup:
bitcoin-cli createwallet anchor
bitcoin-cli -rpcwallet=anchor getnewaddress
# Fund the address with ≥ 0.001 BTC
bitcoin-cli -rpcwallet=anchor dumpprivkey <address>Set ANCHOR_WALLET_WIF and ANCHOR_CHANGE_ADDRESS in .env.
Cost estimate: ~500–2000 satoshis per anchor at normal fee rates (1 input, 2 outputs, OP_RETURN).
docker compose --profile monitoring up| UI | URL | Credentials |
|---|---|---|
| Prometheus | http://localhost:9090 | — |
| Grafana | http://localhost:3000 | admin / GRAFANA_PASSWORD from .env |
Auto-provisioned dashboards:
arcmint-federation.json— issuance, sessions, DB latency, anchor statusarcmint-lightning.json— channel balances, LN payment latency, mint-in/mint-outarcmint-security.json— double-spend attempts, verification failures, rate limiting (default home)
/metrics endpoints are only exposed on the internal Docker network.
# Smoke (CI)
cargo run -p arcmint-loadtest -- run-all --config loadtest/smoke.toml --output report.json
# Standard (pre-release)
cargo run -p arcmint-loadtest -- run-all --config loadtest/standard.toml
# Stress
cargo run -p arcmint-loadtest -- run-all --config loadtest/stress.toml
# Docker
docker compose --profile loadtest up arcmint-loadtest
# Spend race only
cargo run -p arcmint-loadtest -- run-spend-race --config loadtest/standard.tomlService-level objectives:
| Metric | Smoke | Standard | Stress |
|---|---|---|---|
| issuance p99 | 5000 ms | 2000 ms | 10000 ms |
| spend p99 | 3000 ms | 1000 ms | 5000 ms |
| signer RPC p99 | 1000 ms | 500 ms | 2000 ms |
| lightning settlement p99 | 10000 ms | 5000 ms | 30000 ms |
| signing failure rate | ≤ 1% | ≤ 0.1% | ≤ 5% |
| spend false negatives | 0 | 0 | 0 |
spend_false_negatives_allowed = 0 is never relaxed regardless of configuration.
cargo run -p arcmint-adversary -- run-all \
--coordinator-url https://localhost:7000 \
--gateway-url https://localhost:7002 \
--merchant-url https://localhost:7003 \
--signer-urls https://localhost:7001 \
--output report.jsonThe adversary suite covers:
| Attack | Expected outcome |
|---|---|
attack_forged_signature |
Rejected — invalid FROST signature |
attack_malformed_note_missing_pairs |
Rejected — empty commitment pairs |
attack_malformed_note_wrong_denomination |
Rejected — denomination tampered post-signing |
attack_registry_bypass_skip_issued_check |
Rejected — serial not in issued registry |
attack_wrong_commitment_opening |
Rejected — incorrect opening in spend proof |
attack_challenge_precomputation |
Rejected — proof generated for all-zero challenge |
attack_double_spend |
Rejected + identity recovered |
attack_double_spend_different_merchants |
Rejected + identity recovered |
attack_theta_recovery_verification |
Identity secrets not recoverable from honest notes |
attack_replay_spent_note |
Rejected — serial already spent |
attack_flood_issuance |
Rate limited at gateway |
attack_signer_direct_access |
Rejected — no coordinator client certificate |
attack_malformed_issuance_reveal |
Rejected — InvalidProof |
attack_expired_note |
Rejected — note past validity window |
A PASS means the system rejected the attack as expected. A FAIL indicates the system accepted an attack that should have been rejected and represents a potential vulnerability.
# Full workspace unit tests
cargo test --workspace
# End-to-end integration tests (requires running stack)
cargo test -p arcmint-integration --test full_flow -- --ignored
# Lightning integration tests (requires bitcoind + LND regtest)
docker compose up bitcoind lnd lnd-init miner
cargo test -p arcmint-integration --test full_flow -- --ignored --nocapture| Variable | Default | Description |
|---|---|---|
COORDINATOR_PORT |
7000 | Coordinator HTTP listen port |
SIGNER1_PORT |
7001 | Host port for signer-1 |
SIGNER2_PORT |
7002 | Host port for signer-2 |
SIGNER3_PORT |
7003 | Host port for signer-3 |
GATEWAY_PORT |
7002 | Host port for gateway |
MERCHANT_PORT |
7003 | Host port for merchant |
FEDERATION_DB |
federation.db |
SQLite path for signer registry |
GATEWAY_DB |
gateway.db |
SQLite path for gateway DB |
MERCHANT_DB |
merchant.db |
SQLite path for merchant DB |
FROST_KEY_FILE |
frost_key.json |
FROST key package file for signers |
FROST_PUBKEY_FILE |
frost_pubkey.json |
FROST public key package file |
SIGNER_ID |
1 | Signer identifier (unique per signer) |
GATEWAY_SECRET |
— | Shared HMAC secret for gateway tokens (≥ 32 bytes) |
FEDERATION_SECRET |
— | Gateway ↔ federation resolve secret (≥ 32 bytes) |
COORDINATOR_SECRET |
— | Coordinator ↔ signer secret (≥ 32 bytes) |
OPERATOR_SECRET |
— | Operator API secret; comma-separated for rotation (≥ 32 bytes each) |
GATEWAY_RESOLVE_URL |
https://localhost:7002/resolve |
Coordinator callback into gateway |
SIGNER_URLS |
https://localhost:7001 |
Comma-separated signer URLs |
ANCHOR_INTERVAL_SECS |
600 | Bitcoin anchoring interval |
ANCHOR_WALLET_WIF |
— | WIF private key for anchor transactions |
ANCHOR_CHANGE_ADDRESS |
— | Change address for anchor transactions |
ALLOWED_DENOMINATIONS |
(any) | Comma-separated allowed msats; empty = accept any |
BITCOIN_RPC_URL |
— | Bitcoin Core RPC endpoint |
BITCOIN_RPC_USER |
— | Bitcoin Core RPC username |
BITCOIN_RPC_PASS |
— | Bitcoin Core RPC password |
COORDINATOR_URL |
https://localhost:7000 |
Coordinator base URL |
GATEWAY_URL |
https://localhost:7002 |
Gateway base URL |
WALLET_DIR |
~/.arcmint |
Default wallet directory |
MAX_REGISTRATIONS_PER_HOUR |
10 | Max registrations per IP per hour |
TLS_CERT_FILE |
— | TLS certificate (gateway/signers) |
TLS_KEY_FILE |
— | TLS private key (gateway/signers) |
TLS_CA_FILE |
— | CA certificate for signer mTLS |
INTERNAL_CA_FILE |
— | Internal CA for mTLS clients |
COORDINATOR_TLS_CERT |
— | Coordinator TLS server certificate |
COORDINATOR_TLS_KEY |
— | Coordinator TLS private key |
COORDINATOR_CLIENT_CERT |
— | Coordinator client cert for mTLS to signers |
COORDINATOR_CLIENT_KEY |
— | Coordinator client key for mTLS to signers |
COORDINATOR_CN |
arcmint-coordinator |
Expected coordinator client cert CN |
GATEWAY_CLIENT_CA |
— | CA verifying gateway client certificate |
GATEWAY_CLIENT_CERT |
— | Gateway client cert for mTLS to coordinator |
GATEWAY_CLIENT_KEY |
— | Gateway client key for mTLS to coordinator |
GATEWAY_CN |
arcmint-gateway |
Expected gateway client cert CN |
ACME_DOMAIN |
— | Public domain for Let's Encrypt TLS |
ACME_EMAIL |
— | Contact email for Let's Encrypt |
ACME_CACHE_DIR |
/var/lib/arcmint/acme |
ACME account and cert cache |
ACME_STAGING |
false | Use Let's Encrypt staging environment |
- Trust assumption: An honest majority of federation signers is required. Collusion of a threshold-sized quorum can break unlinkability and forge notes.
- Anonymity set: Effective anonymity is bounded by issuance volume and denomination structure. Highly distinctive denominations reduce the anonymity set.
- Blindness proof: The blind threshold Schnorr construction holds heuristically under static corruption. A formal proof under adaptive corruption is an open research problem.
- Side channels: The implementation does not mitigate all side channels (timing, cache, network metadata). Deployments in sensitive environments should apply additional OS and network hardening.
- Anchor availability:
OP_RETURNcommitments timestamp the registry state but do not guarantee long-term data availability. Operators are responsible for archiving and monitoring anchor status.
- No
unsafecode anywhere in the workspace - All SQL queries use
sqlxparameterized binds - mTLS between all internal services with CN verification
- Rust edition 2021; zero compiler warnings required
- All error paths return
Result— no silent failures - All secrets implement
Zeroizeand are cleared on drop
Licensed under either of
at your option.