---
# System prepended metadata

title: DKG Signing Using frost-tools

---

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