# 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)
```