cheget is a single-binary Rust command-line tool that lets a fixed 51-of-100
group jointly control one Bitcoin Taproot address using FROST threshold Schnorr
signatures (RFC 9591, secp256k1, BIP340/341 key-path spend). On-chain, its spends
are indistinguishable from ordinary single-sig. The CLI is organized into three
personae — participant, coordinator, watcher — selected by subcommand.
Status: early. The cryptographic core (Phase 1) and the persistence & storage layer (Phase 2) are implemented today: an age/scrypt participant store, encrypted between-round ceremony checkpointing, a coordinator SQLite store, and two read-only inspector commands (
share-status,roster). It still runs entirely in-process on a single host — there is no networking and no relay/transport layer — and no CLI flow yet persists or consumes a secret share, so the persisted-key ceremony/signing flow that would wire the storage layer into fund custody is later-phase work. Everything under "Planned" below is designed but not built. See.planning/ROADMAP.mdand the draft design inSPEC-frost-cli.mdfor the full end-state vision.
The project aims to let a group of 100 jointly control a single Bitcoin address such that:
- any 51 can spend — a
t = 51,n = 100FROST threshold; - no individual ever holds the key — the group signing key exists only as a sharing, never reconstructed on any one machine;
- membership can rotate with zero on-chain footprint — refresh/enroll/repair change who holds a share without changing the address or touching the chain; and
- past compromise is truly revocable — by sweeping all funds to a pre-generated standby key.
The threshold is fixed: t = 51, n = 100, and never changes. Properties 3 and 4
are design goals for later phases; today the crypto core that makes properties 1
and 2 possible in-process is built, together with the Phase 2 persistence &
storage layer (not yet wired into a fund-custody signing flow).
Phase 1 proves the whole cryptographic value in-process — DKG → BIP341 address → two-round tweaked signing → a confirmed regtest key-spend — with zero transport. Phase 2 adds the persistence & storage layer beneath it. The Phase 1 crypto core is implemented and tested as follows:
| Capability | Where | Pinned by |
|---|---|---|
frost↔rust-bitcoin key bridge — FROST VerifyingKey → 33-byte SEC1 → x-only → XOnlyPublicKey → BIP341 P2TR address (merkle root None) and output key Q |
src/bridge/taproot.rs |
byte-level round-trip / known-answer test in tests/bridge_roundtrip.rs (against tests/vectors/bip341_keyspend.json) |
In-process DKG — simulated participants, no transport, producing a group PublicKeyPackage whose verifying key is the Taproot internal key P, with client-side key confirmation |
src/crypto/ |
tests/dkg_small.rs, tests/dkg_100_correctness.rs |
In-process two-round FROST signing — Taproot tweak via sign_with_tweak / aggregate_with_tweak(…, None), producing a 64-byte BIP340 signature that verifies against the output key Q, finalizing a PSBT and broadcasting a confirmed key-spend on regtest |
src/session/ |
crown-jewel test tests/inproc_sign_100.rs::inproc_sign_confirmed_regtest_key_spend_51_of_100 |
Transport trait + in-memory/in-process stub — the architectural seam later ceremony phases run against; no relay or Nostr code exists yet |
src/transport/ |
tests/transport_stub.rs |
ChainBackend trait — Bitcoin Core JSON-RPC and Esplora implementations plus a key-spend sighash helper |
src/chain/ |
tests/chain_backend_conformance.rs, tests/regtest_fixture.rs |
All of the above runs on a single host with no networking. The DKG uses simulated participants in one process ("simulate-all-seats"); signing runs the coordinator and all signers in-process against the in-memory transport stub.
Phase 2 — persistence & storage layer (shipped, not yet wired into custody).
The storage layer is implemented and tested: an age/scrypt participant store
for at-rest share encryption (with in-memory zeroize), encrypted
between-round ceremony checkpointing, and a coordinator SQLite store
(roster and related public state). Two read-only inspector commands expose it
from the CLI: participant share-status (lists held shares by reading the
plaintext manifest — no decryption, no passphrase prompt) and
coordinator roster (lists the roster from the public SQLite store). This is the
storage layer only: keygen still writes only the public PublicKeyPackage
envelope, sign still runs an in-process simulate-all-seats DKG, and no CLI
command persists or consumes a secret share yet — so the manifest
share-status reads is not populated by the current keygen/sign flows. The
persisted-key ceremony/signing flow that wires this layer into fund custody is
later-phase work.
The clap persona tree (participant / coordinator / watcher) dispatches to real
handlers. Seven subcommands run end-to-end from the command line — participant
keygen/sign/share-status, coordinator keygen/sign/roster, and watcher
address (participant and coordinator keygen/sign share the same in-process
handler). The two ceremony commands (keygen, sign) still run an in-process
simulate-all-seats DKG — there is no transport, no multi-party rounds over a
network, and no persisted secret share. share-status and roster are the two
read-only inspectors added in Phase 2.
cheget
├── participant
│ ├── keygen in-process DKG; writes ONLY the public key package
│ ├── sign in-process DKG + two-round signing over a supplied PSBT
│ └── share-status read-only; lists held shares from the plaintext manifest;
│ never unlocks or prompts for a passphrase
├── coordinator
│ ├── keygen (same in-process handler as participant keygen)
│ ├── sign (same in-process handler as participant sign)
│ └── roster read-only; lists the roster from the coordinator's public
│ SQLite store
└── watcher
└── address derive the group's BIP341 P2TR address (offline, no network)
-
watcher address— fully usable offline. Reads a public-artifact file (a serializedPublicKeyPackageenvelope:key_id,epoch,pubkey_package_hex) and prints the group's BIP341 P2TR address. No secret material is read, and no network call is made.cheget watcher address --pubkey <FILE> [--network <NETWORK>]--networkaccepts exactlybitcoin(mainnet,bc1p…),testnet(tb1p…),signet(tb1p…), orregtest(bcrt1p…). The default isbitcoin. -
participant keygen/coordinator keygen— run an in-process DKG and write only the publicPublicKeyPackageenvelope to--out; the secret shares live in the process for the run and are never serialized. Without--fulla fast 3-of-5 ceremony runs;--fullruns the real 51-of-100 acceptance target; explicit--seats/--thresholdoverride both.cheget coordinator keygen --key-id <ID> --out <FILE> [--full] \ [--seats <N>] [--threshold <T>] [--ceremony <NAME>]The written file is exactly what
watcher address --pubkey <FILE>consumes. -
participant sign/coordinator sign— a self-contained demonstration of the two-round signing pipeline. Because no secret share is persisted, the command runs its own in-process DKG, derives the group address, and signs the supplied--psbtagainst it. The PSBT must therefore spend the address this run derives — it is not signing of an externally-funded, persisted-key wallet (that persisted-key custody flow is later-phase work; Phase 2 shipped only the storage layer, not a signing flow that consumes a persisted share). The default network isregtest. Signing is gated by a display-before-sign acknowledgement;--yesskips only the interactive human ack (for automation/regtest), never the local sighash recompute.cheget coordinator sign --psbt <FILE> [--network <NETWORK>] [--yes] \ [--full] [--seats <N>] [--threshold <T>] [--session <NAME>] -
participant share-status— a read-only inspector introduced in Phase 2. It reads the plaintext share manifest from the store root and lists the held shares. It performs no decryption and never prompts for a passphrase (D-05) — it does not unlock any encrypted share and reads no--pubkeyfile. Note that no CLI flow yet persists a secret share, so on a normalkeygen/signinstall this manifest is empty; the command reports what the storage layer holds, which current ceremony flows do not populate.cheget participant share-status [--home <PATH>]--homeoverrides the store root (otherwiseCHEGET_HOMEor~/.cheget). -
coordinator roster— a read-only inspector introduced in Phase 2. It lists the roster from the coordinator's public SQLite store (STOR-03). It reads only public roster data — no secret material and no network call.cheget coordinator roster [--key-id <ID>] [--home <PATH>]--key-idselects the group-key label (active|standby, defaultactive);--homeoverrides the store root (otherwiseCHEGET_HOMEor~/.cheget).
Structural security controls (present from Phase 1):
- signing nonces use a non-serializable type — the code will not compile if you
try to persist them (
src/crypto/nonce.rs, proven by the compile-fail testtests/compile_fail.rs/tests/ui/nonce_no_serialize.rs); - display-before-sign: the sighash is recomputed locally from the PSBT before
round 2 (
src/session/display.rs); - the tweak/aggregate result is verified against the output key
Qbefore a signature is accepted; - FROST 3.0 cheater-detection culprits are surfaced on abort
(
tests/sign_adversarial.rs).
Everything in this section works today. There is no standing cheget daemon
that talks to a live regtest node — live signing all the way to a confirmed
key-spend is exercised through the integration test harness, which spins up a
throwaway node for you.
1. End-to-end confirmed key-spend (the crown jewel). The full pipeline — DKG →
address → fund → sign → aggregate-with-tweak → verify against Q → finalize →
broadcast → confirm — runs as an integration test that auto-spawns a throwaway
regtest bitcoind via the corepc-node dev-dependency. No manual node setup is
needed:
cargo test --release --test inproc_sign_100 -- --nocaptureRelease is strongly recommended: on one developer machine the confirmed 51-of-100
key-spend runs in roughly ~9 s under --release versus ~90 s in a debug build
(local measurements, not benchmarks). The scale is overridable via the
CHEGET_SIGN_T / CHEGET_SIGN_N environment variables (default 51 / 100) to
capture faster intermediate data points.
2. Derive a regtest address (offline). From any public-key-package file:
cheget watcher address --pubkey pk.json --network regtest
# → bcrt1p...3. Run the in-process signing pipeline locally. The sign command defaults to
the regtest network and drives the real two-round signing flow over the
in-memory transport stub. It is self-contained: it runs its own in-process DKG and
the supplied PSBT must spend the address it prints.
cheget coordinator sign --psbt spend.psbt --network regtest --yesOnly one mainnet action is possible today, and it is offline and safe: deriving
the group's mainnet P2TR address from a public PublicKeyPackage. It makes no
network calls and touches no secret material.
# 1. Produce a public key package (in-process DKG; writes only public data).
cheget coordinator keygen --key-id active --out pk.json
# 2. Derive the mainnet address from that public package.
cheget watcher address --pubkey pk.json --network bitcoin
# → bc1p...Safety / status — read this before doing anything with real funds. This is early, pre-custody software and is not production-audited. The full mainnet signing and custody ceremony — multi-party signing rounds over a transport, signing of an externally-funded PSBT, broadcast, watching, and the sweep to a standby key — is NOT yet wired and arrives in later phases (see Planned, below). The Phase 2 storage layer exists but is not yet wired into a fund-custody signing flow: the in-process
keygen/signcommands simulate all seats in one process and no CLI command persists or consumes a secret share, so they cannot custody funds for a real 100-member group. Do not entrust real funds to flows that do not exist yet. The only mainnet-safe action today is offline address derivation.
The following are designed in the roadmap and spec but not implemented. In particular, no transport, relay, or Nostr code exists yet, and the Phase 2 storage layer is not yet wired into any fund-custody signing flow — no CLI command persists or consumes a secret share.
- Phase 3 — DKG at scale (local): scale the in-process DKG to the full n=100 share set on one host and measure the O(n²) computation cost locally.
- Phase 4 — Membership rotation: refresh (removals + proactivize), enroll (repair/RTS + immediate refresh), and repair, over the in-memory stub, with the mandatory client-side same-key check and epoch discipline.
- Phase 5 — Key lifecycle & revocation: standby-key pre-generation, the sweep flow (all UTXOs → standby, rollover on confirmation), and a policy-driven watcher.
- Phase 6 — Hardening & security-reviewable release: reproducible builds,
pinned/audited dependencies (
cargo audit/cargo denyin CI), a locally-verifiable adversarial test suite, and external review. - Phase 7 — Transport & transport at scale: the real
Transportimplementations behind the existing trait — offlineFileTransportfirst, then a Nostr transport (NIP-44 v2 / NIP-42, multi-relay pool, roster pinning) — plus the gating n=100 DKG relay load test and transport-dependent adversarial tests. Nostr identity keys will be transport-only, independently generated, and never derived from or reused as FROST material.
See .planning/ROADMAP.md for the full phase breakdown.
cheget is a layered, trait-seamed monolith built bottom-up (see src/lib.rs):
bridge— pure, I/O-free: the single canonical frost→rust-bitcoin key seam (VerifyingKey→ x-only →XOnlyPublicKey→ BIP341 P2TR + output keyQ).crypto— pure, I/O-free: the FROST wrapper (DKG, tweaked signing, the non-serializable nonce type).chain— theChainBackendtrait plus Core RPC / Esplora implementations.transport— theTransporttrait plus its in-memory stub. This is the load-bearing seam: every later ceremony phase runs against this trait, so DKG, rotation, lifecycle, and hardening can all be proven locally with zero relay code, and Phase 7 swaps in real transport behind the same trait.session— the two-round signing session; owns the nonce lifetime (RAM only).cli— the clap persona tree (participant / coordinator / watcher).
bridge and crypto are kept I/O-free so the auditable, reproducible trusted
computing base stays small. For the full design rationale see
SPEC-frost-cli.md and .planning/ROADMAP.md.
There is no packaged release — the crate is not published to any registry. Build from source:
# Build (debug).
cargo build
# Build the optimized release binary.
cargo build --release
# Run the standard test suite.
cargo testTwo full-scale tests exercise the real 51-of-100 acceptance target and now run by
default (no #[ignore] — an earlier version of this README wrongly claimed they
had to be run with --ignored). Because both are O(n²), the release profile is
strongly recommended:
# Crown-jewel: confirmed regtest key-spend at t=51, n=100 (auto-spawns bitcoind).
cargo test --release --test inproc_sign_100 -- --nocapture
# Full n=100 in-process DKG correctness + O(n²) timing/memory instrumentation.
cargo test --release --test dkg_100_correctness -- --nocaptureBoth accept environment overrides so you can capture faster, smaller data points:
the signing test honors CHEGET_SIGN_T / CHEGET_SIGN_N and the DKG test honors
CHEGET_DKG_T / CHEGET_DKG_N (each defaults to 51 / 100). On one developer
machine the confirmed regtest key-spend runs in ~9 s under --release versus ~90 s
in debug; these are local measurements, not benchmarks.
MSRV is Rust 1.85, and Cargo.lock is committed so builds are reproducible —
verifiability is a first-class goal (many people must be able to verify what they
run).
Only properties the code enforces today are claimed:
- Nonces are never persisted. Signing nonces use a type that cannot be serialized; attempting to persist one is a compile error, enforced by a compile-fail test.
- Display-before-sign. Each signer recomputes the key-spend sighash locally from the PSBT before round 2 — no blind signing of a coordinator-supplied hash.
- No individual holds the key. The group signing key exists only as a FROST sharing produced by in-process DKG; it is never reconstructed on one machine.
- Signatures are verified against
Q. The tweak/aggregate output is checked against the Taproot output keyQbefore acceptance, and FROST 3.0 cheater-detection surfaces culprits on abort.
The Phase 2 storage layer provides at-rest share encryption (age/scrypt with
in-memory zeroize), but no CLI signing flow yet persists or consumes a secret
share, so this protection is not yet exercised in a fund-custody path. Other
security properties tied to later phases — epoch discipline, same-key rotation
checks, standby/sweep revocation, and Nostr↔FROST key separation — are not yet
enforced and are described above as planned.
Licensed under either of MIT or Apache-2.0 at your option.