Skip to content
rademenezbtcPublic

About

A Cashu mint no single operator can print from: every mint key is split 2-of-3 across guardians who coordinate over Marmot (MLS over Nostr). Stock cdk-mintd holds no keys, and wallets see a normal mint.

Topics

Resources

Stars

0 stars

Watchers

0 watching

Forks

Latest commit

 

History

20 Commits

Folders and files

Repository files navigation

SplitNut

A Cashu mint whose signing keys are split across guardians, so no single party holds them.

A Cashu mint's private keys normally sit with one operator, who can sign unbacked ecash at will, and no wallet can tell. In SplitNut every mint key exists only as shares held by n guardians, who generate it together (distributed key generation), so no one ever sees the whole key. Any t of them must cooperate to produce a signature, so neither the mint server nor any one guardian has a key to steal or misuse.

The guardians also check what they sign. A small CDK change makes the mint tell the signer what each signature is for: a swap's inputs, a melt's quote and inputs, the quotes behind an issuance. Each guardian checks it before releasing its share. It refuses swaps whose outputs exceed their inputs, inputs it cannot verify as genuine, inputs already spent (including by a melt), and issuance beyond what a quote was paid. So swaps and melts cannot create ecash, even if the mint host is compromised. The one thing guardians must still take on trust is that a mint quote was paid: that is capped per hour, allowed once per quote and recorded in every guardian's ledger. See operation context and the threat model.

  • Wallets see a normal mint. Standard keysets, token format and NUT-12 DLEQ proofs. Tested with cdk-cli, which verifies every threshold signature's DLEQ; cashu-ts (used by cashu.me) checks DLEQs the same way.
  • Almost no mint fork. cdk-mintd already sends all signing to a remote signer over gRPC, and SplitNut is that signer: a coordinator that gathers threshold shares from guardians. The one change is a backwards-compatible CDK commit that passes operation context to the signer (rademenezbtc/cdk@702e5bc), to be proposed upstream.
  • Guardians coordinate over Marmot (MLS over Nostr): an end-to-end encrypted group where relays see only ciphertext. Any Nostr relays work; splitnut-relay is a local one with a wire tap that counts events and flags any plaintext.

Built for Nut November 2026.

Demo

Video (2 minutes): [video link]: a wallet mints and spends while guardians sign; one guardian goes offline and minting continues; a second goes offline and the mint refuses; the guardians' ledgers match the wallets and the relay read nothing.

SplitNut dashboard: guardians around the mint, live ledgers, what the relay sees, and the request log

Try it (needs cdk-mintd and cdk-cli from CDK v0.18.3 plus the operation-context commit, see docs/RUNNING.md):

CDK_BIN=/path/to/cdk/target/release ./scripts/demo.sh

One command starts a relay, three guardians, the coordinator and a keyless cdk-mintd. Mint from any wallet at http://127.0.0.1:8085/, watch http://127.0.0.1:8090/, and switch guardians off and on with ./scripts/guardian.sh stop 3 / start 3. Public relays, phone wallets and all settings: docs/RUNNING.md.

Status

  • Milestone 1, spike: cdk-mintd with no local keys, signing through a remote signer; mint, swap and melt with cdk-cli (scripts/spike.sh, findings in docs/spike.md)
  • Milestone 2, threshold signing in process: stock cdk-mintd signs through splitnut-coord with three guardians, 2-of-3. cdk-cli mints, swaps and melts, and checks the DLEQ of every combined signature. One guardian down still works; two down is refused with "quorum unavailable" (scripts/m2.sh)
  • Milestone 3, guardians over Marmot: three guardian processes with their own Nostr keys and the coordinator form one MLS group over Nostr. Killing a guardian keeps the mint working, killing a second makes it refuse, and restarting one resumes signing from its encrypted state. The relay sees only ciphertext (scripts/m3.sh)
  • Milestone 4, rules and dashboard: each guardian keeps its own ledger (issuance it has verified by DLEQ, redemptions it has checked) and an hourly signing cap; heartbeats make quorum loss an immediate, explained refusal; a live dashboard shows each request, the guardians, their ledgers and what the relay sees (scripts/m4.sh)
  • Milestone 5, demo and submission (in progress): one-command demo (scripts/demo.sh), demo video (re-recordable with scripts/video/record.py), deck, submission kit (docs/SUBMISSION.md); still to do: a run on public relays and a phone wallet
  • Operation context: a CDK commit passes each operation to the signer, and guardians check it: swaps and melts must balance, inputs must carry evidence that they are genuine, no input may be spent twice, issuance is bounded by its quotes, and the cap applies to new money only. examples/evil_mintd.rs attacks the guardians as a compromised mint host; m4.sh runs it and every attack is refused (docs/operation-context.md)
  • Distributed key generation: the guardians generate the keyset among themselves over the Marmot group (joint Feldman VSS; shares NIP-44-encrypted guardian to guardian, so the coordinator relaying them cannot read them). Nobody ever holds a whole mint key; the trusted dealer remains only as an option (KEYGEN=dealer). m4.sh and demo.sh use DKG (docs/dkg.md)
  • Catch-up and public ledgers: guardians learn every spend from each other's signed replies in the group, not only from the coordinator; a guardian back from being offline replays the history it missed and refuses to sign until it has. Every guardian publishes its totals as a signed Nostr event, and examples/audit.rs lets anyone fetch and compare them. In m4.sh a guardian offline for most of the run catches up to identical totals, and the audit finds all three agreeing with the wallets (docs/catch-up-and-ledger.md)

Next

The goal is for every guardian to sign only what it can justify itself, without trusting the mint host or the coordinator. Swaps and melts now meet that goal; what is left:

  1. Upstream the operation-context change to CDK.
  2. Less trust in mint quotes: guardians with their own view of incoming payments (read-only access to the Lightning node, or reserves on chain at an address the guardians control).
  3. Stronger key generation: Pedersen-based DKG (no key bias) and resharing, so the guardian set can change without a new keyset.

Architecture

wallet ──HTTP──▶ cdk-mintd (no keys; passes operation context)
                    │ gRPC (CDK signatory API, mTLS)
                    ▼
              splitnut-coord ──Marmot group──▶ splitnut-guardian × n
              (combines + checks shares,        (one share of every key,
               holds no key material)            rules, ledger)
Crate Role
splitnut-core Share maths, Lagrange combination, DLEQ round logic, distributed key generation
splitnut-proto Messages on the Marmot channel
splitnut-dealer Optional trusted-dealer key generation (tests, fallback)
splitnut-guardian Guardian daemon: dkg takes part in key generation, run signs by the rules
splitnut-coord Remote signer for cdk-mintd plus Marmot fan-out and dashboard; --dkg runs key generation
splitnut-marmot Marmot node: MDK + Nostr client in one actor, group setup, encrypted MLS state
splitnut-relay Local in-memory relay with a wire tap

Tests

cargo test --workspace
git clone https://github.com/rademenezbtc/cdk && cd cdk
git checkout 702e5bc76b0cfa88b6b3f4ecb5920bfbf7edd4ab   # v0.18.3 + operation context
cargo build --release -p cdk-mintd -p cdk-cli   # needs protoc
cd ../splitnut && CDK_BIN=../cdk/target/release ./scripts/m4.sh

m4.sh deals a 2-of-3 keyset, gives the coordinator and each guardian a Nostr key, starts a local relay, three guardian processes, the coordinator and a keyless cdk-mintd, then runs cdk-cli through mint, send, receive, double spend and melt, kills guardians one by one, restarts one, tries to mint over the hourly cap, attacks the guardians as a compromised mint host (each attack must be refused), and checks that every online guardian's outstanding total equals the wallets' combined balance. To use public relays instead: RELAYS="wss://relay.damus.io wss://nos.lol" CDK_BIN=... ./scripts/m4.sh.

Typical run on a laptop-class machine with a local relay: the group forms in about 4 s; a two-round threshold signature takes 0.3 to 0.9 s for 1 to 30 outputs, and a proof check about 0.2 s.

How the signing works

Each mint key k (one per amount) is shared t-of-n among the guardians by distributed key generation, so k itself is never computed anywhere. For a blinded message B' the coordinator runs two rounds with t guardians: each returns k_i·B' plus nonce commitments r_i·G and r_i·B', then s_i = r_i + e·k_i for the NUT-12 challenge e. Lagrange-combined, this is exactly the signature and DLEQ proof a single-key mint would produce, so wallets cannot tell the difference. Every share is checked against the guardian's public share before combining, and a cheating guardian is named and excluded. Proof checks (verify_proofs) work the same way in one round. Details are in crates/splitnut-core.

Threat model

SplitNut guarantees two things: no single party, the mint operator included, holds a mint key, nor ever did (the guardians generate the keys together), and a valid signature needs t guardians; and swaps and melts cannot create ecash, because every guardian checks them itself. New ecash can only come from a mint quote, which guardians cap and count but cannot verify.

Protected

  • Key theft. cdk-mintd holds no keys, and one guardian's share is useless alone. Stealing the mint server or a single guardian does not yield a mint key. Whoever controls the mint host can still request signatures, but the guardians check every request (below).
  • A rogue or offline guardian, below threshold. Every share is checked against that guardian's public share before combining; a bad share names the guardian, who is excluded while the request restarts with fresh nonces. Offline guardians are skipped.
  • Relay operators. Guardians talk inside an MLS group (Marmot); relays see gift wraps and ciphertext only. The bundled relay's wire tap checks this on every event.
  • The coordinator. It holds no key material and cannot forge signatures or combine a bad share undetected. It cannot vouch for an input either: guardians check the evidence themselves. It can refuse service or delay requests.
  • A dishonest or compromised mint host, for swaps and melts. Guardians refuse unbalanced swaps, inputs without valid evidence, double spends (across swaps and melts) and excess melt change. examples/evil_mintd.rs tries all of these.

Rules guardians enforce (details in docs/operation-context.md)

  • Context required. A request without operation context (a stock cdk-mintd) is refused, unless the guardian runs with --allow-contextless (v1 rules).
  • Caught up first. A guardian that restarts replays the group history it missed and refuses to sign or check proofs until it has.
  • Swaps and melts. Every input comes with t guardians' partial evaluations and DLEQs, which each guardian checks combine to the proof's C. Outputs ≤ inputs − fees (melt change also minus the amount paid). No input already spent by another operation. The same inputs are never signed for two different output sets.
  • Issuance. Outputs ≤ what the quotes were paid minus what this guardian already issued against them; one bolt11 payment hash per quote.
  • Hourly cap on new money. Each guardian refuses to help issue more than its cap per rolling hour against mint quotes, checked and recorded in round 1 (whose answer already contains its share). A coordinator that rotates signer sets can reach at most cap × n / t per hour.
  • Independent ledgers. After each signature the coordinator announces the finished signatures to all guardians; each guardian checks every DLEQ against the public mint key before counting it, so its issued total is issuance it can prove happened. Proof checks go to all guardians, and each proof counts as redeemed once. outstanding = issued − redeemed is the ecash in circulation as that guardian sees it, to compare with the mint's Lightning balance. In the end-to-end test every online guardian's outstanding equals the wallets' combined balance.

Not protected in v1

  • Mint quotes. A guardian cannot prove that a mint quote's invoice was paid: the mint created the invoice and knows its preimage. (A preimage does prove a melt was paid, because there the invoice comes from someone else.) So a dishonest host can still issue unbacked ecash against quotes that were never paid, up to cap × n / t per hour; the ledgers record it. Verifying deposits needs guardians with their own view of the payment, for example reserves on chain at an address the guardians control; that is roadmap.
  • Relays that withhold. Guardians learn of spends they did not sign from each other's replies in the group, and a guardian back from being offline replays the group's history from the relays before signing again. A relay operator colluding with t − 1 guardians could withhold messages from the rest, so guardians should use several relays, at least one not run by the mint operator (in the demo the local relay is the operator's). A message dropped while a guardian is online is logged loudly but not recovered until its next restart.
  • Spending conditions. P2PK and HTLC locks are checked by the mint only, so a dishonest host can let the wrong person spend a locked token. That moves ecash; it does not create any.
  • Custody. The operator still holds the Lightning funds. SplitNut targets unbacked issuance (inflation), not theft of the reserves.
  • Key generation. Distributed key generation (joint Feldman VSS) needs all n guardians online for the ceremony, aborts rather than repairs if any share is invalid (naming the dealer), and lets a guardian that colludes with the coordinator bias the public key's distribution, but not learn the key. Guardians take each other's Nostr keys from a roster file they hold themselves, so the coordinator cannot substitute its own. The optional trusted dealer (KEYGEN=dealer) sees every full key once; use it only for tests.
  • Collusion. t guardians together can do anything a single operator could. Choose t and n accordingly.
  • Liveness. With fewer than t guardians online the mint stops issuing and redeeming. That is the intended failure mode, and it is an immediate, explained refusal rather than a hang.
  • Ledger caveats. Issuance is counted when guardians approve outputs, so a signature that was approved but never completed still counts: the error is on the safe side (liabilities overstated). Proofs count as redeemed only once a guardian has checked evidence that they are genuine; "checked twice" counts refused double-spend attempts. Proofs from a failed melt can afterwards only be retried for the same melt quote. Published totals show liabilities; comparing them with reserves still needs the operator's proof of reserves.
  • Relay rate limits. Relays typically accept about one event per second per publisher. SplitNut batches identical requests into one message and retries rejected publishes, and the test runs under a tighter-than-default limit, but very busy mints would need relays that allow more.

Licence

MIT. See LICENSE.

About

A Cashu mint no single operator can print from: every mint key is split 2-of-3 across guardians who coordinate over Marmot (MLS over Nostr). Stock cdk-mintd holds no keys, and wallets see a normal mint.

Topics

Resources

Stars

0 stars

Watchers

0 watching

Forks

Releases

Packages

Contributors

Languages