Educational reference implementation of FROST threshold Schnorr signatures for secp256k1/BIP340.
FROST (Flexible Round-Optimized Schnorr Threshold Signatures) enables t-of-n participants to collaboratively produce a valid Schnorr signature without ever reconstructing the shared secret key. This implementation is built from first principles: secp256k1 field arithmetic, curve point operations, and polynomial secret sharing are all self-contained with zero external crypto dependencies. All naming follows BIP340/Bitcoin conventions throughout.
uv sync # install dependencies
uv run python -m pytest # run tests (85 tests, ~36s)A 2-of-3 threshold signing ceremony:
from frost import Participant, Aggregator, Point
# --- DKG: Distributed Key Generation ---
threshold, n = 2, 3
participants = [Participant(index=i, threshold=threshold, participants=n) for i in range(1, n + 1)]
for p in participants:
p.init_keygen()
p.generate_shares()
for receiver in participants:
other_shares = tuple(
sender.shares[receiver.index - 1]
for sender in participants if sender.index != receiver.index
)
receiver.aggregate_shares(other_shares)
for p in participants:
other_commitments = tuple(
other.coefficient_commitments[0]
for other in participants if other.index != p.index
)
p.derive_public_key(other_commitments)
for p in participants:
other_ccs = tuple(
other.coefficient_commitments
for other in participants if other.index != p.index
)
p.derive_group_commitments(other_ccs)
# --- Signing: any 2 of 3 ---
signers = [participants[0], participants[1]]
signer_indexes = tuple(p.index for p in signers)
message = b"Hello from FROST!"
for signer in signers:
signer.generate_nonce_pair()
nonce_pairs = [None] * n
for signer in signers:
nonce_pairs[signer.index - 1] = signer.nonce_commitment_pair
for i in range(n):
if nonce_pairs[i] is None:
nonce_pairs[i] = (Point(), Point())
nonce_commitment_pairs = tuple(nonce_pairs)
shares = tuple(p.sign(message, nonce_commitment_pairs, signer_indexes) for p in signers)
aggregator = Aggregator(
signers[0].public_key, message,
nonce_commitment_pairs, signer_indexes,
group_commitments=participants[0].group_commitments,
)
signature = aggregator.signature(shares)
print(f"BIP340 signature: {signature}")The output is a standard 64-byte BIP340 Schnorr signature, indistinguishable from a single-signer signature.
Three layers, each building on the last:
Companion Guide -- structured walkthrough from finite fields through FROST signing (9 chapters).
Tutorial Notebooks -- interactive step-by-step, tracking the guide:
| Notebook | Topic |
|---|---|
| tutorial-01 | Scalars and the finite field |
| tutorial-02 | Points on the curve |
| tutorial-03 | Schnorr signatures (BIP340) |
| tutorial-04 | Secret sharing with polynomials |
| tutorial-05 | Running a DKG ceremony |
| tutorial-06 | Signing with FROST |
Experiment Notebooks -- "what if" deep dives for readers with context:
| Notebook | Question |
|---|---|
| experiment-01 | Does our field satisfy the axioms? |
| experiment-02 | What happens with corrupted shares? |
| experiment-03 | Does signing order matter? |
| experiment-04 | How does share repair work? |
| experiment-05 | How do you add/remove participants? |
| experiment-06 | Can you change the threshold? |
| experiment-07 | How fast is pure Python crypto? |
| Module | Description |
|---|---|
frost.scalar |
Scalar field arithmetic (Z_Q) |
frost.point |
secp256k1 curve point operations |
frost.constants |
Curve constants (P, Q, G coordinates) |
frost.tagged_hash |
BIP340 tagged hash utility |
frost.polynomial |
Polynomial generation and evaluation |
frost.lagrange |
Lagrange interpolation coefficients |
frost.matrix |
Matrix operations for threshold changes |
frost.keygen |
Distributed Key Generation (DKG) |
frost.signing |
FROST two-round signing protocol |
frost.aggregator |
Signature aggregation and share verification |
frost.repair |
Share repair and enrollment |
frost.threshold |
Threshold increase and decrease |
The Participant class provides a stateful facade over the protocol modules.
For learning, read the modules directly.
85 tests covering protocol correctness and edge cases:
- DKG ceremonies, signing, repair, enrollment, threshold changes
- Property-based tests (Hypothesis) for 7 protocol invariants: roundtrip serialization, threshold reconstruction, signature validity, share verification, signing commutativity, repair correctness, enrollment safety
- Error path tests for parameter validation and missing state
uv run python -m pytest -v # verbose output
uv run python -m pytest -k sign # just signing testsPython 3.13+. No external dependencies for core crypto. Dev dependencies (pytest, Hypothesis, ruff, ty) are managed by uv.
This is an educational implementation. It is NOT suitable for production use: no constant-time operations, no side-channel resistance, pure Python performance. For production FROST, see ZcashFoundation/frost (Rust) or secp256k1-zkp.
This work is made possible with the support of Brink.