Algebraic Setting
The protocol is parameterized by an abstract scheme structure. This structure defines the algebraic objects and operations. Concrete instantiations choose specific rings and modules.
Scheme Record
The Scheme structure bundles the algebraic setting with cryptographic oracles. It keeps types abstract so proofs and implementations can swap in concrete rings and primitives.
structure Scheme where
PartyId : Type
Message : Type
Secret : Type
Public : Type
Challenge : Type
Scalar : Type
[scalarSemiring : Semiring Scalar]
[secretAdd : AddCommGroup Secret]
[publicAdd : AddCommGroup Public]
[secretModule : Module Scalar Secret]
[publicModule : Module Scalar Public]
[challengeSMulSecret : SMul Challenge Secret]
[challengeSMulPublic : SMul Challenge Public]
A : Secret →ₗ[Scalar] Public
Commitment : Type
Opening : Type
commit : Public → Opening → Commitment
commitBinding : ∀ {x₁ x₂ o₁ o₂}, commit x₁ o₁ = commit x₂ o₂ → x₁ = x₂
-- Domain-separated hash functions (FROST pattern)
hashCollisionResistant : Prop
hashToScalar : ByteArray → ByteArray → Scalar -- H(domain, data)
hashDkg : PartyId → Public → Public → Scalar -- H_dkg for PoK
hash : Message → Public → List PartyId → List Commitment → Public → Challenge
-- Optional identifier derivation
deriveIdentifier : ByteArray → Option PartyId := fun _ => none
normOK : Secret → Prop
[normOKDecidable : DecidablePred normOK]
Modules and Scalars
Let denote a commutative semiring with unity. This serves as the scalar ring. Common choices include for a prime .
The secret space is an additive commutative group. It carries an -module structure. Secrets are elements of .
The public space is also an additive commutative group with an -module structure. Public keys are elements of .
In lattice instantiations both spaces may be polynomial rings or vectors over .
The Linear Map A
The core algebraic structure is a linear map . This map is -linear. It satisfies
and
for all secrets and scalars .
The map connects private shares to their public counterparts. If is a secret share then is the corresponding public share.
Linearity ensures that aggregation in corresponds to aggregation in . The sum of public shares equals the image of the sum of secret shares. This property underlies the threshold aggregation.
Linearity in Lattice Signatures
Dilithium and other lattice-based signature schemes inherit a fundamental linearity property from their algebraic structure. This linearity is precisely what enables threshold signing.
Why Dilithium is Linear
In Dilithium signing, the response is computed as where is random and is the secret key. This sum respects linearity in both variables. Unlike schemes where the signature is output as a function of intermediate hash values, Dilithium's response is a direct linear combination.
The same linearity holds in threshold settings. If (secret shares) and each signer produces , then the sum produces the same response as a single signer with full secret.
This is the core insight of FROST applied to the lattice setting. The threshold scheme is not a different scheme; it is the base scheme distributed and recombined through linearity.
Norm Bounds via Triangle Inequality
In single-signer Dilithium, the response must satisfy a global norm bound. In threshold signing, each signer produces a partial , and these are summed to produce the global response .
By the triangle inequality, .
If we ensure that each partial satisfies a local bound , and we aggregate at most partials, then the global response is guaranteed to satisfy
By setting , we obtain a hard guarantee that any combination of or fewer valid partials automatically satisfies the global Dilithium bound. This is a mathematical certainty, not a probabilistic property.
Each signer enforces the local bound via internal rejection sampling. The aggregator enforces it by checking each partial before inclusion. Once the aggregator has collected valid partials (all satisfying ), it is mathematically guaranteed that their sum satisfies the global bound. No further norm checking is needed; no distributed coordination or restart is required.
Design Choice: No Explicit Error Term
In Dilithium, the public key includes an error term: where is small. This makes indistinguishable from random under MLWE. During verification, Dilithium uses a HighBits() function to absorb the error term .
We deliberately keep the abstract map without an explicit error term for these reasons:
-
Abstraction boundary. The error term is a key generation detail. Once the public key is computed, signing and verification only need the linear relationship .
-
HighBits is an implementation detail. The truncation that absorbs can be encoded in the concrete instantiation:
- The
hashfunction can hashHighBits(w)rather than full - The
normOKpredicate can include the HighBits consistency check
- The
-
Clean protocol logic. The signing and verification math stays clean: The error handling lives in the concrete instantiation, not the abstract protocol.
-
Instantiation flexibility. Different lattice schemes handle errors differently (Dilithium vs Falcon vs others). The abstract Scheme lets each instantiation encode its approach appropriately.
For a Dilithium instantiation, the Scheme record would be configured as:
def dilithiumScheme (params : DilithiumParams) : Scheme :=
{ Secret := PolyVec params.l -- s₁ ∈ R_q^l
, Public := PolyVec params.k -- t = As₁ + s₂ (precomputed at keygen)
, A := dilithiumKeyMap -- maps response z to Az
, normOK := fun z =>
dilithiumNormCheck z params ∧
highBitsConsistent z params -- both quality checks
, hash := fun m pk ... w =>
hashHighBits (highBits w params.gamma2) m -- hash truncated w
, ... }
The error term is baked into the public key during key generation. The normOK predicate enforces both the coefficient bound and the HighBits consistency check. The hash function operates on truncated values as Dilithium requires.
HighBits Specification
The HighBits function is formally specified in Proofs/Core/HighBits.lean:
structure HighBitsSpec (P : Type*) [AddCommGroup P] where
highBits : P → P
gamma2 : Nat
isSmall : P → Prop
idempotent : ∀ x, highBits (highBits x) = highBits x
absorbs_small : ∀ (w e : P), isSmall e →
highBits (w + e) = highBits w ∨ highBits (w - e) = highBits w
The key property is absorption: small perturbations don't change HighBits. During verification:
- Signer computes:
- Verifier computes:
Since and :
The absorption property ensures when .
Challenges
The challenge space is a separate type. In typical instantiations challenges are scalars from or a related ring. Challenges act on secrets and publics via scalar multiplication.
For a challenge and secret the product is a secret. For a challenge and public the product is a public. This action must be compatible with the module structure.
Commitments
A commitment scheme binds a public value to a commitment string. Let denote the commitment function. Here is the opening space and is the commitment space.
The binding property requires that
for all and .
This property ensures that a commitment uniquely determines the committed value. Adversaries cannot produce two different openings for the same commitment.
Hash Functions
Following FROST, we use domain-separated hash functions for different protocol operations. This prevents cross-protocol attacks where a hash collision in one context could be exploited in another.
Domain Separation
Each hash function uses a unique domain prefix:
| Domain | FROST Name | Purpose | Prefix |
|---|---|---|---|
| Binding factor | H1 | Compute binding factors | ice-nine-v1-rho |
| Challenge | H2 | Fiat-Shamir challenge | ice-nine-v1-chal |
| Nonce | H3 | Nonce derivation | ice-nine-v1-nonce |
| Message | H4 | Message pre-hashing | ice-nine-v1-msg |
| Commitment list | H5 | Commitment encoding | ice-nine-v1-com |
| DKG | HDKG | Proof of knowledge | ice-nine-v1-dkg |
| Identifier | HID | Party ID derivation | ice-nine-v1-id |
Challenge Hash (H2)
The primary hash function maps protocol inputs to challenges:
The inputs are the message , the global public key , the participant set , the list of commitments, and the aggregated nonce .
DKG Hash (HDKG)
Used for proof of knowledge in DKG:
Maps (party ID, public key, commitment) to a scalar challenge.
Identifier Derivation (HID)
The deriveIdentifier function maps arbitrary byte strings to party identifiers:
Uses domain prefix ice-nine-v1-id. Returns none if the derived value is zero (invalid identifier).
-- In Scheme record
deriveIdentifier : ByteArray → Option PartyId := fun _ => none
-- Convenience function
def Scheme.deriveId (S : Scheme) (data : ByteArray) : Option S.PartyId :=
S.deriveIdentifier data
This enables deterministic identifier derivation from human-readable strings or external identifiers.
Random Oracle Model
In security proofs the hash is modeled as a random oracle. This means it behaves as a uniformly random function. The random oracle model enables simulation-based security arguments.
Norm Predicate
A norm predicate gates acceptance. Signatures with large response vectors are rejected.
The predicate captures a bound on the norm of the response. For lattice signatures this prevents leakage through the response distribution. Rejection sampling or abort strategies ensure the bound holds.
In the abstract model the predicate is a parameter. Concrete instantiations define it based on the lattice parameters.
Dilithium-Style Norm Bounds
For Dilithium-style signatures, the norm check uses:
- norm:
- Rejection bound: where
structure DilithiumParams where
n : Nat := 256 -- ring dimension
q : Nat := 8380417 -- modulus
tau : Nat -- challenge weight (number of ±1)
eta : Nat -- secret coefficient bound
gamma1 : Nat -- nonce coefficient range
gamma2 : Nat -- low-bits range
def dilithiumNormOK (p : DilithiumParams) (z : List Int) : Prop :=
vecInfNorm z < p.gamma1 - p.tau * p.eta
Standard parameter sets (defined and validated in Proofs/Core/Assumptions.lean):
| Level | τ | η | γ₁ | γ₂ | β | Security |
|---|---|---|---|---|---|---|
| Dilithium2 | 39 | 2 | 2¹⁷ | 95232 | 78 | 128-bit |
| Dilithium3 | 49 | 4 | 2¹⁹ | 261888 | 196 | 192-bit |
| Dilithium5 | 60 | 2 | 2¹⁹ | 261888 | 120 | 256-bit |
structure DilithiumSigningParams where
tau : Nat -- challenge weight
eta : Nat -- secret coefficient bound
gamma1 : Nat -- nonce coefficient range
gamma2 : Nat -- low-bits truncation range
def DilithiumSigningParams.beta (p : DilithiumSigningParams) : Nat :=
p.tau * p.eta
-- Critical security constraints (proven for each parameter set):
-- γ₁ > β ensures rejection sampling has room
-- γ₂ > β ensures HighBits absorbs error
theorem dilithiumL2_gamma1_ok : dilithiumL2.gamma1 > dilithiumL2.beta
theorem dilithiumL2_gamma2_ok : dilithiumL2.gamma2 > dilithiumL2.beta
Security Parameter Guidance
When selecting parameters for threshold deployment, consider how threshold configuration affects security margins.
Threshold and Local Bounds. The local rejection bound is B_local = B_global / T where T is the number of signers. More signers means each signer must produce smaller partials. This affects rejection rates but not core security assumptions.
Security Level Selection. Choose the security level based on your threat model, not performance concerns.
| Use Case | Recommended Level | Rationale |
|---|---|---|
| General purpose | Dilithium2 (128-bit) | Sufficient for most applications |
| High-value assets | Dilithium3 (192-bit) | Conservative security margin |
| Long-term secrets | Dilithium5 (256-bit) | Maximum security against future attacks |
The 128-bit level provides security comparable to AES-128. The 192-bit and 256-bit levels provide additional margins against potential improvements in lattice algorithms.
Threshold and Fault Tolerance. The default 2/3+1 threshold balances security and liveness.
| Threshold | Fault Tolerance | Trade-off |
|---|---|---|
| n-of-n | Zero faults tolerated | Maximum security, fragile |
| 2/3+1 | Up to 1/3 faulty | Good balance (BFT standard) |
| 1/2+1 | Up to 1/2 faulty | More fault tolerant, weaker security |
Lower thresholds improve liveness but reduce the cost of compromise. An adversary needs fewer corrupted parties to forge signatures.
Signer Count Limits. The local bound B_local decreases as T increases. Very large signer counts reduce B_local to the point where rejection rates become impractical.
| Signers (T) | B_local (Dilithium2) | Practical? |
|---|---|---|
| 3-10 | 13099-43664 | Yes |
| 20 | 6549 | Marginal |
| 50+ | <2620 | Not recommended |
For deployments with more than 20 signers, consider using Dilithium3 or Dilithium5 for larger global bounds.
Parameter Validation. Always validate parameter configurations using ThresholdConfig.validate. The validation checks:
- Threshold does not exceed party count
- maxSigners is at least threshold
- Local bound is positive after division
- Global bound is sufficient for the configuration
Configurations from untrusted sources should be validated before use even if they were created through ThresholdConfig.create.
Concrete Scheme Instantiations
The implementation provides a lattice-friendly scheme in Instances.lean:
Lattice Scheme
Integer vectors with hash-based commitments and Dilithium-style norm checking.
structure LatticeParams where
n : Nat := 256 -- dimension
q : Nat := 8380417 -- modulus
bound : Nat := 1 <<< 19 -- ℓ∞ bound (approx Dilithium γ1)
def latticeScheme (p : LatticeParams := {}) : Scheme :=
{ PartyId := Nat
, Message := ByteArray
, Secret := Fin p.n → Int
, Public := Fin p.n → Int
, Challenge := Int
, Scalar := Int
, A := LinearMap.id
, Commitment := LatticeCommitment (Fin p.n → Int) (Fin p.n → Int)
, Opening := Fin p.n → Int
, commit := latticeCommit -- hash-based: H(encode(w, nonce))
, normOK := fun v => intVecInfLeq p.bound v
, ... }
This models the SIS/LWE structure:
- Secret key: (integer vector with bounded coefficients)
- Public key: (identity map for simplicity)
- Norm bound: for signature validity
- Commitments: Hash-based with axiomatized binding
The hash-based commitment uses hashBytes(encodePair(w, nonce)) where encodePair serializes the value and opening. Binding is axiomatized via HashBinding assumption.
Semilattice Structure
Protocol state at each phase forms a join-semilattice. A semilattice is an algebraic structure with a binary join operation that is commutative, associative, and idempotent.
The join induces a partial order: iff .
Message Map Semilattice
Protocol messages are stored in MsgMap structures, which are hash maps keyed by sender ID. This design makes conflicting messages from the same sender un-expressable at the type level.
structure MsgMap (S : Scheme) (M : Type*) [BEq S.PartyId] [Hashable S.PartyId] where
map : Std.HashMap S.PartyId M
instance : Join (MsgMap S M) := ⟨MsgMap.merge⟩
instance : LE (MsgMap S M) := ⟨fun a b => a.map.toList.all (fun (k, _) => b.map.contains k)⟩
The merge operation takes the union of keys, keeping the first value on conflict. This provides:
- Conflict-freedom by construction: At most one message per sender
- Commutativity: Merge order doesn't affect the set of senders
- Idempotence: Merging a map with itself is identity
- Strict mode:
tryInsertreturns conflict indicators for misbehavior detection
This provides the foundation for merging commit maps, reveal maps, and share maps while preventing Byzantine parties from injecting multiple conflicting messages.
Product Semilattice
If and are semilattices, their product is a semilattice under componentwise join.
instance prodJoin (α β) [Join α] [Join β] : Join (α × β) :=
⟨fun a b => (a.1 ⊔ b.1, a.2 ⊔ b.2)⟩
This allows combining protocol state with auxiliary data (refresh masks, repair bundles, rerandomization masks).
CRDT Semantics
Semilattice merge ensures conflict-free replicated data type (CRDT) semantics. Replicas can merge states in any order and converge to the same result. This is essential for handling:
- Out-of-order message delivery
- Network partitions and rejoins
- Concurrent protocol executions
Instance Constraint Patterns
The protocol modules use consistent instance requirements:
For HashMap-based structures (MsgMap, NonceRegistry):
[BEq S.PartyId]- Boolean equality[Hashable S.PartyId]- Hash function for HashMap
For decidable equality (if-then-else, match guards):
[DecidableEq S.PartyId]- Prop-valued equality with decision procedure
For field arithmetic (Lagrange coefficients):
[Field S.Scalar]- Division required for λ_i = Π x_j / (x_j - x_i)[DecidableEq S.Scalar]- For checking degeneracy
Lagrange Interpolation
The Protocol/Core/Lagrange.lean module provides a unified API for computing Lagrange interpolation coefficients used across threshold protocols.
Core Function
def coeffAtZero [Field F] [DecidableEq F]
(partyScalar : F)
(allScalars : List F)
: F :=
let others := allScalars.filter (· ≠ partyScalar)
others.foldl (fun acc xj => acc * (xj / (xj - partyScalar))) 1
This computes for evaluating at 0.
Scheme-Aware API
def schemeCoeffAtZero (S : Scheme)
[Field S.Scalar] [DecidableEq S.Scalar] [DecidableEq S.PartyId]
(pidToScalar : S.PartyId → S.Scalar)
(allParties : List S.PartyId)
(party : S.PartyId)
: S.Scalar
def buildPartyCoeffs (S : Scheme)
[Field S.Scalar] [DecidableEq S.Scalar] [DecidableEq S.PartyId]
(pidToScalar : S.PartyId → S.Scalar)
(parties : List S.PartyId)
: List (PartyCoeff S)
These functions integrate with the Scheme type for protocol use, including batch computation and precomputed coefficient storage.
Secret Wrappers
Secrets and nonces are wrapped in opaque structures to discourage accidental duplication or exposure.
/-- Wrapper for secrets with private constructor.
Forces explicit acknowledgment when accessing secret material. -/
structure SecretBox (α : Type*) where
private mk ::
val : α
/-- Wrap a secret value. -/
def SecretBox.wrap (secret : α) : SecretBox α := ⟨secret⟩
/-- Wrapper for nonces to highlight their ephemeral nature.
Nonce reuse is catastrophic: treat these as single-use. -/
structure NonceBox (α : Type*) where
private mk ::
val : α
/-- Create from a freshly sampled nonce. -/
def NonceBox.fresh (nonce : α) : NonceBox α := ⟨nonce⟩
The private constructors prevent arbitrary creation of wrapped values. Use SecretBox.wrap and NonceBox.fresh to create instances. This lightweight discipline signals intent. Code that accesses the val field must explicitly acknowledge it is handling secret material.
Both types have Zeroizable and ConstantTimeEq marker instances to indicate security requirements for production implementations.
Key Share Integration:
structure KeyShare (S : Scheme) where
pid : S.PartyId -- party identifier
sk_i : SecretBox S.Secret -- wrapped secret share
pk_i : S.Public -- public share = A(sk_i)
pk : S.Public -- global public key
/-- Create KeyShare from unwrapped secret (use during DKG). -/
def KeyShare.create (S : Scheme) (pid : S.PartyId) (sk : S.Secret) (pk_i pk : S.Public)
: KeyShare S :=
{ pid := pid, sk_i := ⟨sk⟩, pk_i := pk_i, pk := pk }
/-- Access secret for cryptographic operations. -/
def KeyShare.secret (share : KeyShare S) : S.Secret :=
share.sk_i.val
All protocol code that needs the secret share calls share.secret rather than pattern-matching on the SecretBox. This creates a clear audit trail for secret access.
Single-Signer Schnorr Relation
The threshold protocol generalizes the single-signer Schnorr pattern. The single-signer version proceeds as follows.
Key generation. Sample a short secret . Compute the public key .
Signing. Given message :
- Sample a short ephemeral secret .
- Compute the nonce .
- Compute the challenge .
- Compute the response .
- Output signature .
Verification. Given signature and message :
- Compute .
- Check .
- Compute .
- Accept if .
The threshold protocol distributes among parties. Each party holds a share with . The signing procedure produces partial responses that aggregate to .