# DKG Signing Using `frost-tools` ## Clone the repo and check out the branch ```bash git clone https://github.com/BlockchainCommons/zcash-frost-tools cd zcash-frost-tools git checkout taproot-tweak ``` ## Install the tools - This installs `frost-client`, `coordinator`, `dkg`, `participant`, `trusted-dealer`, and `frostd`. - This demo only uses `frostd` and `frost-client`. ```bash cargo install --path frost-client && cargo install --path frostd ``` ## Install toml-cli We will use this to extract the public keys from the TOML files. ```bash cargo install toml-cli ``` ## Add a Certification Authority (CA) for test development. The instructions below are for macOS. See [the `mkcert` repo](https://github.com/FiloSottile/mkcert) for more information. ```bash brew install mkcert β”‚ ... β”‚ 🍺 /opt/homebrew/Cellar/mkcert/1.4.4: 7 files, 4.0MB β”‚ ... ``` ```bash mkcert -install β”‚ Created a new local CA πŸ’₯ β”‚ Sudo password: <enter login password> β”‚ The local CA is now installed in the system trust store! ⚑️ β”‚ The local CA is now installed in the Firefox trust store (requires browser restart)! 🦊 ``` ## Set up the demo environment ```bash export DEMO_DIR=`pwd`/demo mkdir -p "$DEMO_DIR" cd "$DEMO_DIR" ``` ## Create certificates for localhost ```bash mkcert localhost 127.0.0.1 ::1 β”‚ Created a new certificate valid for the following names πŸ“œ β”‚ - "localhost" β”‚ - "127.0.0.1" β”‚ - "::1" β”‚ β”‚ The certificate is at "./localhost+2.pem" and the key at "./localhost+2-key.pem" βœ… β”‚ β”‚ It will expire on 8 November 2027 πŸ—“ ``` ## Terminal 1: Start the FROST server ```bash frostd \ --tls-cert localhost+2.pem \ --tls-key localhost+2-key.pem β”‚ 2025-08-08T23:47:22.551822Z INFO frostd: server running β”‚ 2025-08-08T23:47:22.559393Z INFO frostd: starting HTTPS server at 0.0.0.0:2744 ``` ## Create client configs (one per participant) Use `frost-client` to create each participant’s local config + network identity and then exchange β€œcontact” strings. On each machine (or each terminal), run: ```bash frost-client init -c alice.toml frost-client init -c bob.toml frost-client init -c eve.toml β”‚ Generating keypair... β”‚ Writing to config file at alice.toml... β”‚ Done. β”‚ WARNING: the config file will contain your private FROST shares in clear. Keep it safe and never share it with anyone. Future versions of this tool might encrypt the config file. β”‚ Generating keypair... β”‚ Writing to config file at bob.toml... β”‚ Done. β”‚ WARNING: the config file will contain your private FROST shares in clear. Keep it safe and never share it with anyone. Future versions of this tool might encrypt the config file. β”‚ Generating keypair... β”‚ Writing to config file at eve.toml... β”‚ Done. β”‚ WARNING: the config file will contain your private FROST shares in clear. Keep it safe and never share it with anyone. Future versions of this tool might encrypt the config file. ``` ## Extract the participant's public keys Run this in all the participants' terminals. ```bash ALICE_PUBKEY=`toml get alice.toml 'communication_key.pubkey' | sed 's/"//g'` BOB_PUBKEY=`toml get bob.toml 'communication_key.pubkey' | sed 's/"//g'` EVE_PUBKEY=`toml get eve.toml 'communication_key.pubkey' | sed 's/"//g'` echo $ALICE_PUBKEY echo $BOB_PUBKEY echo $EVE_PUBKEY β”‚ 22f2f696f4d8074aa842b0ab0465626678ba3e618cc512523608f1b5ce1d8927 β”‚ d1129058500ee4f1eb9cb13ae5438898ca85214a2461b81025987b84956df754 β”‚ 7ac45af5e45841e4a17533066d22d7132101aa87b16634a545810f426bd36221 ``` ## Export the client contact strings ```bash ALICE_CONTACT=`frost-client export --name "Alice" -c alice.toml 2>&1 | tail -n 1` BOB_CONTACT=`frost-client export --name "Bob" -c bob.toml 2>&1 | tail -n 1` EVE_CONTACT=`frost-client export --name "Eve" -c eve.toml 2>&1 | tail -n 1` echo $ALICE_CONTACT echo $BOB_CONTACT echo $EVE_CONTACT β”‚ zffrost1qyqq2stvd93k2gpz7tmfdaxcqa92ss4s4vzx2cnx0zarucvvc5f9ydsg7x6uu8vfyuta0sh6 β”‚ zffrost1qyqqxsn0vgsdzy5stpgqae83awwtzwh9gwyf3j59y99zgcdczqjes7uyj4klw4qvcu8x4 β”‚ zffrost1qyqqx3tkv5s843z67hj9ss0y596nxpndytt3xggp42rmze3554zczr6zd0fkyggl7xyqk ``` ## Each client imports the other two clients' contact strings ```bash frost-client import -c alice.toml $BOB_CONTACT frost-client import -c alice.toml $EVE_CONTACT frost-client import -c bob.toml $ALICE_CONTACT frost-client import -c bob.toml $EVE_CONTACT frost-client import -c eve.toml $ALICE_CONTACT frost-client import -c eve.toml $BOB_CONTACT β”‚ Imported this contact: β”‚ Name: Bob β”‚ Public Key: d1129058500ee4f1eb9cb13ae5438898ca85214a2461b81025987b84956df754 β”‚ Imported this contact: β”‚ Name: Eve β”‚ Public Key: 7ac45af5e45841e4a17533066d22d7132101aa87b16634a545810f426bd36221 β”‚ Imported this contact: β”‚ Name: Alice β”‚ Public Key: 22f2f696f4d8074aa842b0ab0465626678ba3e618cc512523608f1b5ce1d8927 β”‚ Imported this contact: β”‚ Name: Eve β”‚ Public Key: 7ac45af5e45841e4a17533066d22d7132101aa87b16634a545810f426bd36221 β”‚ Imported this contact: β”‚ Name: Alice β”‚ Public Key: 22f2f696f4d8074aa842b0ab0465626678ba3e618cc512523608f1b5ce1d8927 β”‚ Imported this contact: β”‚ Name: Bob β”‚ Public Key: d1129058500ee4f1eb9cb13ae5438898ca85214a2461b81025987b84956df754 ``` ## Create a DKG session NOTE: If you get the error `Error: user has more than one FROST session active`, you can restart `frostd` to clear its in-memory state. There are more elegant ways to to this but for the demo it's not a concern. On Alice's terminal: ```bash frost-client dkg \ -d "Demo DKG: Alice, Bob, Eve" \ -s https://127.0.0.1:2744 \ -S $BOB_PUBKEY,$EVE_PUBKEY \ -t 2 \ -C "secp256k1-tr" \ -c alice.toml β”‚ Logging in... β”‚ Creating DKG session... β”‚ Getting session info... β”‚ Waiting for other participants to send their Round 1 Packages..... ``` On Bob's terminal: ```bash frost-client dkg \ -d "Demo DKG: Alice, Bob, Eve" \ -s https://127.0.0.1:2744 \ -t 2 \ -C "secp256k1-tr" \ -c bob.toml β”‚ Logging in... β”‚ Joining DKG session... β”‚ Getting session info... β”‚ Waiting for other participants to send their Round 1 Packages..... ``` On Eve's terminal: ```bash frost-client dkg \ -d "Demo DKG: Alice, Bob, Eve" \ -s https://127.0.0.1:2744 \ -t 2 \ -C "secp256k1-tr" \ -c eve.toml β”‚ Logging in... β”‚ Joining DKG session... β”‚ Getting session info... β”‚ Waiting for other participants to send their Round 1 Packages..... ``` ## The participants exchange their packets: Alice ``` β”‚ Waiting for other participants to send their broadcasted Round 1 Packages.... β”‚ Waiting for other participants to send their Round 2 Packages..... β”‚ Taproot DKG: storing P in PublicKeyPackage for FROST signing β”‚ Internal key (P): eadd7c4c22aafa5bee71f0180127b2c16d42c5da54aef01a0a4eb9f760409758 β”‚ Tweaked key (Q): 4dd04bcb610e10c69cc339f72fb50459f018cd5b37c2b798a8f2507c99edd0a9 β”‚ Group ID will use Q for identification β”‚ Group created; information written to alice.toml ``` Bob ``` β”‚ Waiting for other participants to send their broadcasted Round 1 Packages..... β”‚ Waiting for other participants to send their Round 2 Packages.... β”‚ Taproot DKG: storing P in PublicKeyPackage for FROST signing β”‚ Internal key (P): eadd7c4c22aafa5bee71f0180127b2c16d42c5da54aef01a0a4eb9f760409758 β”‚ Tweaked key (Q): 4dd04bcb610e10c69cc339f72fb50459f018cd5b37c2b798a8f2507c99edd0a9 β”‚ Group ID will use Q for identification β”‚ Group created; information written to bob.toml ``` Eve ``` β”‚ Waiting for other participants to send their broadcasted Round 1 Packages..... β”‚ Waiting for other participants to send their Round 2 Packages..... β”‚ Taproot DKG: storing P in PublicKeyPackage for FROST signing β”‚ Internal key (P): eadd7c4c22aafa5bee71f0180127b2c16d42c5da54aef01a0a4eb9f760409758 β”‚ Tweaked key (Q): 4dd04bcb610e10c69cc339f72fb50459f018cd5b37c2b798a8f2507c99edd0a9 β”‚ Group ID will use Q for identification β”‚ Group created; information written to eve.toml ``` ## Inspect what DKG wrote Run this in all participant terminals: ```bash GROUP_ID=$(grep -oE '^\[group\.[0-9a-f]+' alice.toml | head -1 | sed 's/^\[group\.//') DESC=$(toml get alice.toml "group.$GROUP_ID.description" | tr -d '"') CS=$(toml get alice.toml "group.$GROUP_ID.ciphersuite" | tr -d '"') SRV=$(toml get alice.toml "group.$GROUP_ID.server_url" | tr -d '"') PUBKEY_PKG=$(toml get alice.toml "group.$GROUP_ID.public_key_package" | tr -d '"') PX=$(echo $PUBKEY_PKG | tail -c 65) KEY_PKG=$(toml get alice.toml "group.$GROUP_ID.key_package" | tr -d '"') echo "group ID: $GROUP_ID" echo "description: $DESC" echo "ciphersuite: $CS" echo "server_url: $SRV" echo "group_id: $GROUP_ID" echo "pubkey_pkg: ${#PUBKEY_PKG} hex chars" echo "px: $PX" echo "key_pkg: ${#KEY_PKG} hex chars" β”‚ group ID: 024dd04bcb610e10c69cc339f72fb50459f018cd5b37c2b798a8f2507c99edd0a9 β”‚ description: Demo DKG: Alice, Bob, Eve β”‚ ciphersuite: FROST-secp256k1-SHA256-TR-v1 β”‚ server_url: https://127.0.0.1:2744 β”‚ group_id: 024dd04bcb610e10c69cc339f72fb50459f018cd5b37c2b798a8f2507c99edd0a9 β”‚ pubkey_pkg: 468 hex chars β”‚ px: eadd7c4c22aafa5bee71f0180127b2c16d42c5da54aef01a0a4eb9f760409758 β”‚ key_pkg: 272 hex chars ``` Participants listed under this group: ```bash grep -oE "^\[group\.$GROUP_ID\.participant\.[0-9a-f]+\]" alice.toml \ | sed -E "s/^\[group\.$GROUP_ID\.participant\.([0-9a-f]+)\]/\1/" \ | while read -r ID; do PK=$(toml get alice.toml "group.$GROUP_ID.participant.$ID.pubkey" | tr -d '"') echo "participant id=$ID pubkey=$PK" done β”‚ participant id=bc309f74848d79ef96301c2863f04d82e83ea0268c2b0e4625fa19786df92170 pubkey=d1129058500ee4f1eb9cb13ae5438898ca85214a2461b81025987b84956df754 β”‚ participant id=d1166ce67bc5998ad2738282df837b88147c7ce24bea62fda5b5c46e86663465 pubkey=22f2f696f4d8074aa842b0ab0465626678ba3e618cc512523608f1b5ce1d8927 β”‚ participant id=f38c8c14b55cb580afa75cb478662e23f4d08dbf7aafecdd50b50c42be9cc72d pubkey=7ac45af5e45841e4a17533066d22d7132101aa87b16634a545810f426bd36221 ``` ## Sign (2-of-3) with this group Your earlier DKG used the description, so keep using that to select the group. For secp256k1-tr you must sign a 32-byte message (hash). Make a message digest. Run this in all participant terminals: ```bash MSG=$(printf 'hello world' | shasum -a 256 | awk '{print $1}') echo $MSG β”‚ b94d27b9934d3e08a52e52d7da7dabfac484efe37a5380ee9088f7ace2efcde9 ``` ## Stop and restart `frostd` to remove old sessions ```bash frostd \ --tls-cert localhost+2.pem \ --tls-key localhost+2-key.pem ``` ## Sign the message with Alice acting as Coordinator ```bash SIGNERS="$ALICE_PUBKEY,$BOB_PUBKEY" printf '%s\n' "$MSG" | frost-client coordinator \ --group "$GROUP_ID" \ -s https://127.0.0.1:2744 \ -S "$SIGNERS" \ -m - \ -o sig.raw \ -c alice.toml β”‚ Taproot Option A active: verify shares under P, aggregate_with_tweak to Q (key‑path) β”‚ The message to be signed (hex encoded) β”‚ Taproot Option A: verifying shares under P, aggregating with tweak to Q (key‑path) β”‚ Logging in... β”‚ Creating signing session... β”‚ Note: Your key is included in --signers. Run 'frost-client participant' with your own config to contribute your commitments and signature share. β”‚ Waiting for participants to send their commitments... β”‚ Sending SigningPackage to participants... β”‚ Waiting for participants to send their SignatureShares... β”‚ ...... ``` Alice also needs to act as a participant: ```bash frost-client participant \ --group "$GROUP_ID" \ -s https://127.0.0.1:2744 \ -c alice.toml β”‚ Taproot Option A active: using standard FROST (no rerandomization), P for challenge β”‚ Taproot participant: computing challenge with Q via sign_with_tweak (no rerandomization) β”‚ Logging in... β”‚ Joining signing session... β”‚ Sending commitments to coordinator... β”‚ Waiting for coordinator to send signing package... β”‚ Signing package received β”‚ Message to be signed (hex-encoded): β”‚ b94d27b9934d3e08a52e52d7da7dabfac484efe37a5380ee9088f7ace2efcde9 β”‚ Do you want to sign it? (y/n) y β”‚ Sending signature share to coordinator... β”‚ Done ``` Bob is the other signer: ```bash frost-client participant \ --group "$GROUP_ID" \ -s https://127.0.0.1:2744 \ -c bob.toml β”‚ Taproot Option A active: using standard FROST (no rerandomization), P for challenge β”‚ Taproot participant: computing challenge with Q via sign_with_tweak (no rerandomization) β”‚ Logging in... β”‚ Joining signing session... β”‚ Sending commitments to coordinator... β”‚ Waiting for coordinator to send signing package.. β”‚ Signing package received β”‚ Message to be signed (hex-encoded): β”‚ b94d27b9934d3e08a52e52d7da7dabfac484efe37a5380ee9088f7ace2efcde9 β”‚ Do you want to sign it? (y/n) y β”‚ Sending signature share to coordinator... β”‚ Done ``` Coordinator terminal: ```bash Taproot Option A: verifying shares under P, aggregating with tweak to Q (key‑path) Raw signature written to sig.raw ``` ```bash ls -l sig.raw β”‚ -rw-r--r-- 1 wolf staff 64 Aug 12 18:44 sig.raw ``` ## Verify the signature ```bash # Qx from your group id (strip the leading 0x02) QX=${GROUP_ID:2} # the exact digest you signed (32‑byte hex) MSG=$(printf 'hello world' | shasum -a 256 | awk '{print $1}') # sanity: both should be 64 hex chars echo $QX echo $MSG β”‚ 4dd04bcb610e10c69cc339f72fb50459f018cd5b37c2b798a8f2507c99edd0a9 β”‚ b94d27b9934d3e08a52e52d7da7dabfac484efe37a5380ee9088f7ace2efcde9 ``` ```bash mkdir -p verify && cd verify cat > Cargo.toml << 'EOF' [package] name = "bip340-verify" version = "0.1.0" edition = "2021" [dependencies] secp256k1 = { version = "^0.31", features = ["global-context"] } hex = "^0.4" [workspace] EOF ``` ```bash mkdir -p src cat > src/main.rs << 'EOF' use secp256k1::{Secp256k1, XOnlyPublicKey}; use secp256k1::schnorr::Signature; use std::{env, fs}; // cargo run -- $QX $MSG ../sig.raw fn main() { let args: Vec<String> = env::args().collect(); if args.len() != 4 { eprintln!("Usage: {} <Qx-hex> <msg-hex-32B> <sig-file>", args[0]); std::process::exit(2); } // Qx (x-only) = 32 bytes let qx_vec = hex::decode(&args[1]).expect("bad Qx hex"); assert_eq!(qx_vec.len(), 32, "Qx must be 32 bytes"); let mut qx = [0u8; 32]; qx.copy_from_slice(&qx_vec); let xpk = XOnlyPublicKey::from_byte_array(qx).expect("Qx parse"); // message is already a 32-byte digest let msg_vec = hex::decode(&args[2]).expect("bad msg hex"); assert_eq!(msg_vec.len(), 32, "message must be 32 bytes"); let msg_bytes = msg_vec; // &[u8] via coercion below // 64-byte BIP340 sig let sig_bytes = fs::read(&args[3]).expect("read sig"); assert_eq!(sig_bytes.len(), 64, "sig must be 64 bytes"); let mut sig_arr = [0u8; 64]; sig_arr.copy_from_slice(&sig_bytes); let sig = Signature::from_byte_array(sig_arr); // returns Signature on 0.31 let secp = Secp256k1::verification_only(); secp.verify_schnorr(&sig, &msg_bytes, &xpk).expect("verify failed"); println!("VALID (BIP-340 Schnorr over Q)"); } EOF ``` ```bash cd verify cargo run -- $QX $MSG ../sig.raw β”‚ VALID (BIP-340 Schnorr over Q) ```