The key that never exists: a threshold ceremony in your browser
On this page three keyholders, alice, bob and carol, create one key together, and any two of them sign with it. Nobody ever holds the whole key, and there is no trusted dealer who makes it and splits it, so the key is born already in pieces. That is what threshold cryptography adds over a single key, where whoever reaches the one copy holds everything.
Everything below is real cryptography, compiled from Zig to WebAssembly and running locally in your tab. The library is zig-mpc. I wrote it, and it is on GitHub. Pick a curve and press Begin the ceremony. The page explains each step as you go.
Choose a key
Pick the curve for the chain you care about. The three keyholders and the 2-of-3 threshold are fixed for this page.
The cast is fixed: alice is party 1, bob is party 2, carol is party 3, and the threshold is 2 - any two can sign, any one alone can do nothing. In production each party is a separate machine; this page plays all three and shows you exactly which bytes would cross the network.
Two pieces of jargon before the rounds start. MPC is secure multi-party computation, several parties computing a result together without revealing their inputs to each other. It is what “MPC wallet” vendors sell, usually behind an API. A key ceremony is custody’s name for the formal procedure where key material gets created or used, traditionally in a vault with witnesses and dual controls. The one on this page is the real protocol, minus the vault.
The ceremony is the same for all three schemes, only the curve and the final signature format differ. Ed25519 and BIP-340 sign with FROST (Flexible Round-Optimized Schnorr Threshold signatures, RFC 9591), and the same distributed key generation (DKG) also feeds threshold ECDSA (CGGMP24) for chains that require it. The RFC covers signing only and says key generation “is out of scope”. The key generation on this page is zig-mpc’s own, Feldman secret sharing in the three-round shape of CGGMP’s keygen.
Every byte shown is a real protocol message, in the same wire format the zmpc CLI exchanges between machines. Nothing you type or click leaves the page. There is no server and no analytics, the page fetches one wasm file and then goes quiet.
Grow a key that never exists whole
Distributed key generation is three rounds plus a finalize, one command per machine per round with the CLI. Each round reads the frames that arrived and produces new ones, and the page delivers them the way you would between machines. A broadcast frame (…-t0) is copied to both other parties, a p2p frame (…-tN) goes to party N only.
Each party invents its own secret polynomial and hands the others single points on it. Add three polynomials together and you get a group secret nobody chose and nobody knows. Each party’s share is just its own stack of points. Commitments in round 1 stop anyone from picking their polynomial after seeing the others, and verification in round 3 catches anyone who dealt inconsistent points. Cheating does not corrupt the key, it aborts the ceremony. That includes a commitment with extra coefficients, which Trail of Bits found working against ten implementations in 2024 as a way for one party to raise the threshold behind the others’ backs. Round 3 here rejects any commitment whose length differs from the threshold. Naming the cheater is done in signing and not yet in key generation, where the library knows which share failed and does not report it.
If you know Shamir’s secret sharing the algebra will look familiar but the trust model will not. Shamir needs a dealer who holds the whole secret before splitting it, and using the secret means putting it back together, so the key exists whole at birth and at every use. Here each party deals only its own contribution, and reconstruction never happens, at generation or at signing.
The rounds unlock once you begin the ceremony above.
All three parties computed this same key
independently, from each other's commitments. The matching private key
has never existed anywhere: alice, bob and carol each hold
keyshare.zmpc - one point on a secret polynomial nobody
knows - and that file never leaves their machine.
Sign with any two
Signing is FROST: two rounds plus an aggregation, zmpc sign commit / share / aggregate in the CLI. Pick which two sit at the table. The third stays home and learns nothing, not even that a signature happened. Every signer must hold the same message bytes and a fresh signing-session id.
The output is an ordinary signature. In the BIP-340 mode a Bitcoin node verifies it, and in the Ed25519 mode a Solana validator does, each with its standard algorithm against the group public key. The plain secp256k1 mode is the RFC 9591 ciphersuite, which no chain accepts as is. No verifier can tell a ceremony happened, and because the threshold structure is invisible on-chain it works today on chains that have never heard of FROST.
Unlocks once the key above exists.
Try to break it
Each experiment below runs a fresh signing session with one thing wrong. A bit is flipped in transit, a message is swapped at verification, or a party signs below the threshold with its own share.
One byte of the first signer's commitment frame is flipped in transit - pick which one, or leave blank for a random position - and the other signer tries to use the frame.
Two signers run a fresh session over the message from the signing panel, then the signature is verified against whatever you type here. Try changing one character - then try typing the original message exactly.
Below the threshold: one party runs the entire signing protocol by itself, honestly, with its genuine share.
Unlocks once the key exists - the experiments need real shares to attack.
Alice’s lone attempt in the third experiment gets through commit and share and fails at aggregation, where the per-share check in RFC 9591 rejects it, because one point does not determine a degree-1 polynomial and so her signature does not verify. There is no if-statement in the library counting signers that anyone could patch out, only the algebra.
Each experiment’s message for the impossible outcome reads “that is a bug”. If you ever see it, please file an issue.
Under the hood
This page embeds zmpc.wasm (about 230 KB), the WebAssembly build of zig-mpc, written in Zig, compiled to wasm32-freestanding, no emscripten and no JS crypto. The primitives are not rolled here, with one exception. Group arithmetic comes from std.crypto.ecc, field math from std.crypto.ff, and SHA-2 and HMAC from the same place, and the wasm build carries its own faster secp256k1 scalar multiplication built on those types. zig-mpc adds the protocol layer, zeroes consumed secrets, and compares secret data in constant time in the source. WebAssembly itself promises no constant-time execution, so what the browser’s compiler does with that code is outside the library’s hands. The JavaScript on this page moves bytes in and out of linear memory, draws tables, and supplies every random byte the protocol asks for from the browser’s crypto.getRandomValues.
The DKG and FROST rounds running here are the same functions the zmpc CLI runs, and the frames are byte-compatible with it, so a keyshare.zmpc generated on this page works in a CLI signing session between real machines, and vice versa. On real hardware the same ceremony is one process per party, one command per round:
zmpc init --dir p1 --suite ed25519 --party 1 --n 3 --threshold 2
zmpc dkg round1 --dir p1 # ... deliver frames to the other machines ...
zmpc dkg round2 --dir p1
zmpc dkg round3 --dir p1
zmpc dkg finalize --dir p1 # -> artifacts/keyshare.zmpc, prints the public key
Everything is in the repo. zig build produces the CLI, which pulls in bitcoin-core’s libsecp256k1 as its default backend, and zig build wasm the module this page just loaded, which depends on nothing beyond the Zig standard library. The FROST implementation reproduces the RFC 9591 test vectors byte for byte, the Taproot mode passes all 19 official BIP-340 vectors, and the library also does threshold ECDSA (CGGMP24, end to end), proactive share refresh, and BIP-32/SLIP-10 HD derivation.
The point of MPC is that each party is a separate machine and the shares never meet. This tab plays all three parties so you can watch the messages. And zig-mpc is unaudited, pre-production software. It exists so you can see how a signing ceremony works, message by message. Do not put keys you care about anywhere near it yet. The repository’s roadmap spells out what has to happen first.
Correction. An earlier version said the key generation names a cheating party (it aborts without naming one), that Bitcoin or Solana nodes verify the output in every mode (the plain secp256k1 mode is the RFC ciphersuite), and that the lone-signer experiment runs without complaint (the aggregation step rejects it). It also said zig-mpc compares secret data in constant time without saying that WebAssembly guarantees nothing of the kind.