Documentation
¶
Overview ¶
Package frost implements the FROST (Flexible Round-Optimized Schnorr Threshold) signature scheme over an arbitrary elliptic curve group.
FROST is a threshold signature scheme that allows t-of-n participants to collaboratively generate a Schnorr signature without any single participant knowing the full private key. The scheme consists of two main phases:
Distributed Key Generation (DKG) ¶
Before signing, participants must run a distributed key generation protocol to establish their key shares. The DKG proceeds in rounds:
- Each participant generates a random polynomial and broadcasts commitments to its coefficients using Participant.Round1Broadcast.
- Each participant sends private shares to all other participants using FROST.Round1PrivateSend.
- Each participant verifies received shares against the broadcasted commitments using FROST.Round2ReceiveShare.
- Each participant computes their final key share using FROST.Finalize.
Threshold Signing ¶
Once key shares are established, any t participants can collaboratively sign a message:
- Each signer generates nonces and commitments using FROST.SignRound1.
- Each signer computes their signature share using FROST.SignRound2.
- Signature shares are aggregated into a final signature using FROST.Aggregate.
- Anyone can verify the signature using FROST.Verify.
Example ¶
Basic usage with 2-of-3 threshold:
// Create FROST instance
f, _ := frost.New(group, 2, 3)
// Run DKG (simplified - see tests for full example)
participants := make([]*frost.Participant, 3)
for i := range participants {
participants[i], _ = f.NewParticipant(rand.Reader, i+1)
}
// ... exchange broadcasts and shares ...
keyShares := make([]*frost.KeyShare, 3)
for i, p := range participants {
keyShares[i], _ = f.Finalize(p, broadcasts)
}
// Sign with 2 participants
message := []byte("hello")
nonce1, commit1, _ := f.SignRound1(rand.Reader, keyShares[0])
nonce2, commit2, _ := f.SignRound1(rand.Reader, keyShares[1])
commitments := []*frost.SigningCommitment{commit1, commit2}
share1, _ := f.SignRound2(keyShares[0], nonce1, message, commitments)
share2, _ := f.SignRound2(keyShares[1], nonce2, message, commitments)
sig, _ := f.Aggregate(message, commitments, []*frost.SignatureShare{share1, share2})
// Verify
valid := f.Verify(message, sig, keyShares[0].GroupKey)
Security Considerations ¶
This implementation assumes a trusted dealer-free setup where all participants are honest during DKG. The scheme provides security against a passive adversary controlling up to t-1 participants during signing.
Nonces generated in FROST.SignRound1 must never be reused. Each signing session requires fresh nonces.
Index ¶
- Constants
- Variables
- type Blake2bHasher
- func (h *Blake2bHasher) H1(g group.Group, msg, encCommitList, signerID []byte) group.Scalar
- func (h *Blake2bHasher) H2(g group.Group, R, Y, msg []byte) group.Scalar
- func (h *Blake2bHasher) H3(g group.Group, seed, rho, msg []byte) group.Scalar
- func (h *Blake2bHasher) H4(g group.Group, msg []byte) []byte
- func (h *Blake2bHasher) H5(g group.Group, encCommitList []byte) []byte
- type FROST
- func (f *FROST) Aggregate(message []byte, commitments []*SigningCommitment, shares []*SignatureShare) (*Signature, error)
- func (f *FROST) AggregateWithVerification(message []byte, commitments []*SigningCommitment, shares []*SignatureShare, ...) (*Signature, error)
- func (f *FROST) BootstrapRound1(r io.Reader, state *PiggybackNonceState, share *KeyShare) (currentNonce *SigningNonce, currentCommitment *SigningCommitment, err error)
- func (f *FROST) BootstrapRound2(share *KeyShare, state *PiggybackNonceState, nonce *SigningNonce, ...) (*PiggybackSignatureShare, error)
- func (f *FROST) ComputeGroupCommitment(message []byte, commitments []*SigningCommitment) (group.Point, error)
- func (f *FROST) Finalize(p *Participant, allBroadcasts []*Round1Data) (*KeyShare, error)
- func (f *FROST) NewParticipant(r io.Reader, id int) (*Participant, error)
- func (f *FROST) NewRefreshParticipant(r io.Reader, id group.Scalar) (*RefreshParticipant, error)
- func (f *FROST) NewReshareNewMember(id group.Scalar) (*ReshareNewMember, error)
- func (f *FROST) NewReshareOldMember(r io.Reader, share *KeyShare, oldMemberIDs []group.Scalar, newThreshold int) (*ReshareOldMember, error)
- func (f *FROST) PiggybackAggregate(message []byte, sessionIndex uint64, currentCommitments []*SigningCommitment, ...) (*Signature, *PiggybackSessionCollector, error)
- func (f *FROST) PiggybackSign(r io.Reader, share *KeyShare, state *PiggybackNonceState, message []byte, ...) (*PiggybackSignatureShare, error)
- func (f *FROST) PiggybackVerify(message []byte, sessionIndex uint64, sig *Signature, groupKey group.Point) bool
- func (f *FROST) RefreshFinalize(rp *RefreshParticipant, existingShare *KeyShare, ...) (*KeyShare, error)
- func (f *FROST) RefreshRound1PrivateSend(rp *RefreshParticipant, recipientID group.Scalar) (*RefreshRound1PrivateData, error)
- func (f *FROST) RefreshRound2ReceiveDelta(rp *RefreshParticipant, data *RefreshRound1PrivateData, ...) error
- func (f *FROST) ReshareFinalize(rnm *ReshareNewMember, allBroadcasts []*ReshareRound1Data, ...) (*KeyShare, error)
- func (f *FROST) ReshareNewMemberReceiveShare(rnm *ReshareNewMember, data *ReshareRound1PrivateData, ...) error
- func (f *FROST) ReshareOldMemberSendShare(rom *ReshareOldMember, newMemberID group.Scalar) (*ReshareRound1PrivateData, error)
- func (f *FROST) Round1PrivateSend(p *Participant, recipientID int) *Round1PrivateData
- func (f *FROST) Round2ReceiveShare(p *Participant, data *Round1PrivateData, senderCommitments []group.Point) error
- func (f *FROST) SignRound1(r io.Reader, share *KeyShare) (*SigningNonce, *SigningCommitment, error)
- func (f *FROST) SignRound2(share *KeyShare, nonce *SigningNonce, message []byte, ...) (*SignatureShare, error)
- func (f *FROST) Threshold() int
- func (f *FROST) Total() int
- func (f *FROST) Verify(message []byte, sig *Signature, groupKey group.Point) bool
- func (f *FROST) VerifyShare(share *SignatureShare, publicKey group.Point, message []byte, ...) (bool, error)
- type Hasher
- type KeyShare
- type Participant
- type PiggybackNonceState
- type PiggybackSessionCollector
- type PiggybackSignatureShare
- type PoseidonHasher
- func (h *PoseidonHasher) H1(g group.Group, msg, encCommitList, signerID []byte) group.Scalar
- func (h *PoseidonHasher) H2(g group.Group, R, Y, msg []byte) group.Scalar
- func (h *PoseidonHasher) H3(g group.Group, seed, rho, msg []byte) group.Scalar
- func (h *PoseidonHasher) H4(g group.Group, msg []byte) []byte
- func (h *PoseidonHasher) H5(g group.Group, encCommitList []byte) []byte
- type RailgunHasher
- func (h *RailgunHasher) H1(g group.Group, msg, encCommitList, signerID []byte) group.Scalar
- func (h *RailgunHasher) H2(g group.Group, R, Y, msg []byte) group.Scalar
- func (h *RailgunHasher) H3(g group.Group, seed, rho, msg []byte) group.Scalar
- func (h *RailgunHasher) H4(g group.Group, msg []byte) []byte
- func (h *RailgunHasher) H5(g group.Group, encCommitList []byte) []byte
- type RefreshParticipant
- type RefreshRound1Data
- type RefreshRound1PrivateData
- type ReshareNewMember
- type ReshareOldMember
- type ReshareRound1Data
- type ReshareRound1PrivateData
- type Round1Data
- type Round1PrivateData
- type SHA256Hasher
- func (h *SHA256Hasher) H1(g group.Group, msg, encCommitList, signerID []byte) group.Scalar
- func (h *SHA256Hasher) H2(g group.Group, R, Y, msg []byte) group.Scalar
- func (h *SHA256Hasher) H3(g group.Group, seed, rho, msg []byte) group.Scalar
- func (h *SHA256Hasher) H4(g group.Group, msg []byte) []byte
- func (h *SHA256Hasher) H5(g group.Group, encCommitList []byte) []byte
- type Secp256k1Hasher
- func (h *Secp256k1Hasher) H1(g group.Group, msg, encCommitList, signerID []byte) group.Scalar
- func (h *Secp256k1Hasher) H2(g group.Group, R, Y, msg []byte) group.Scalar
- func (h *Secp256k1Hasher) H3(g group.Group, seed, rho, msg []byte) group.Scalar
- func (h *Secp256k1Hasher) H4(g group.Group, msg []byte) []byte
- func (h *Secp256k1Hasher) H5(g group.Group, encCommitList []byte) []byte
- type Signature
- type SignatureShare
- type SigningCommitment
- type SigningNonce
Constants ¶
const MaxParticipants = 100
MaxParticipants is the maximum number of participants supported. This limits resource usage (Poseidon sponge rounds, commitment encoding) while supporting any practical threshold signing configuration.
Variables ¶
var ( // ErrPiggybackInactive is returned when PiggybackSign is called on an // inactive state (before bootstrap or after failure). ErrPiggybackInactive = errors.New("frost: piggyback state is not active") // ErrSessionOverflow is returned when SessionIndex reaches math.MaxUint64. // The signer must call ReBootstrap to reset the counter. ErrSessionOverflow = errors.New("frost: session index overflow (MaxUint64)") // ErrCommitmentIDMismatch is returned by the collector when a piggybacked // commitment's ID does not match the signature share's ID. ErrCommitmentIDMismatch = errors.New("frost: piggybacked commitment ID does not match share ID") // ErrDuplicateSigner is returned when duplicate signer IDs are detected // in piggybacked shares. ErrDuplicateSigner = errors.New("frost: duplicate signer ID in piggybacked shares") // ErrIdentityCommitment is returned when a piggybacked commitment // contains an identity point (RFC 9591 Section 4.3). ErrIdentityCommitment = errors.New("frost: piggybacked commitment contains identity point") // ErrNilNextCommitment is returned when a piggybacked share has a nil // NextCommitment field. ErrNilNextCommitment = errors.New("frost: nil NextCommitment in piggybacked share") // ErrSessionIndexMismatch is returned when a piggybacked share's // SessionIndex does not match the expected value. ErrSessionIndexMismatch = errors.New("frost: session index mismatch in piggybacked share") )
Error sentinels for the piggyback nonce preprocessing protocol.
var (
ErrInvalidCommitment = errors.New("invalid or empty commitment list")
)
Errors returned by the FROST protocol.
Functions ¶
This section is empty.
Types ¶
type Blake2bHasher ¶
type Blake2bHasher struct {
// Prefix is the domain separation prefix.
// Default: "FROST-EDBABYJUJUB-BLAKE512-v1"
Prefix string
}
Blake2bHasher implements Hasher using Blake2b-512 with domain separation. This is compatible with Ledger/iden3 FROST implementations.
Domain separation format: prefix + tag + input Output is interpreted as little-endian before reducing mod curve order.
func NewBlake2bHasher ¶
func NewBlake2bHasher() *Blake2bHasher
NewBlake2bHasher creates a Blake2bHasher with the Ledger-compatible prefix.
type FROST ¶
type FROST struct {
// contains filtered or unexported fields
}
FROST holds the cryptographic group and threshold parameters for the FROST signature scheme. Create instances using New or NewWithHasher.
A FROST value is safe for concurrent use by multiple goroutines. All methods are stateless with respect to the FROST struct.
func New ¶
New creates a FROST instance with the given group and threshold parameters. It uses SHA-256 as the default hash function. Use NewWithHasher for alternative hash configurations such as Blake2b for Ledger compatibility.
The threshold parameter specifies the minimum number of signers required (t) to produce a valid signature. It must be at least 2.
The total parameter specifies the total number of participants (n) in the scheme. It must be greater than or equal to threshold.
func NewWithHasher ¶
NewWithHasher creates a FROST instance with a custom hash function. Use this constructor for Ledger/iden3 compatibility with Blake2bHasher or other custom hash implementations.
The total parameter must not exceed MaxParticipants (100).
Example for Ledger compatibility:
f, err := frost.NewWithHasher(g, 2, 3, frost.NewBlake2bHasher())
func (*FROST) Aggregate ¶
func (f *FROST) Aggregate( message []byte, commitments []*SigningCommitment, shares []*SignatureShare, ) (*Signature, error)
Aggregate combines individual signature shares into a complete Schnorr signature. The resulting signature can be verified using FROST.Verify.
All signature shares must be from the same signing session (same message and commitments).
WARNING: Aggregate does not verify individual signature shares. Use FROST.AggregateWithVerification to prevent rogue-key attacks from malicious signers. Only use Aggregate when shares have been independently verified via FROST.VerifyShare.
func (*FROST) AggregateWithVerification ¶
func (f *FROST) AggregateWithVerification( message []byte, commitments []*SigningCommitment, shares []*SignatureShare, publicKeys map[string]group.Point, groupKey group.Point, ) (*Signature, error)
AggregateWithVerification combines individual signature shares into a complete Schnorr signature, verifying each share before aggregation. This prevents a single malicious signer from producing an invalid signature.
The publicKeys map must contain the public key for each signer, keyed by the string representation of their ID bytes.
func (*FROST) BootstrapRound1 ¶
func (f *FROST) BootstrapRound1( r io.Reader, state *PiggybackNonceState, share *KeyShare, ) (currentNonce *SigningNonce, currentCommitment *SigningCommitment, err error)
BootstrapRound1 generates nonces for the current (bootstrap) session and pre-generates the NEXT session's nonces. The current nonce and commitment are returned for broadcast; the next nonce pair is stored in the state.
After BootstrapRound1, the caller must broadcast currentCommitment, collect all commitments, and then call BootstrapRound2.
If BootstrapRound2 is not subsequently called (e.g., due to network failure), the caller MUST call state.ReBootstrap() to securely erase the pre-generated nonce. The returned currentNonce must also be zeroed by the caller in that case.
func (*FROST) BootstrapRound2 ¶
func (f *FROST) BootstrapRound2( share *KeyShare, state *PiggybackNonceState, nonce *SigningNonce, message []byte, commitments []*SigningCommitment, ) (*PiggybackSignatureShare, error)
BootstrapRound2 computes the signature share for the bootstrap session (session 0) using a session-bound message, and attaches the pre-generated next commitment. On success, the state transitions to ACTIVE.
func (*FROST) ComputeGroupCommitment ¶
func (f *FROST) ComputeGroupCommitment(message []byte, commitments []*SigningCommitment) (group.Point, error)
ComputeGroupCommitment computes the aggregate nonce commitment R from all signing commitments. This is useful for coordinators that need to compute the challenge externally (e.g., for circomlibjs compatibility).
The computation is: R = Σ (D_i + ρ_i * E_i) where ρ_i = H1(i, msg, commitments) is the binding factor for participant i.
The returned point R can be used to compute an external challenge:
c = poseidon([R.x, R.y, A.x, A.y, msg])
This is particularly useful when signers (e.g., Ledger devices) need to receive a pre-computed challenge rather than computing it internally.
func (*FROST) Finalize ¶
func (f *FROST) Finalize(p *Participant, allBroadcasts []*Round1Data) (*KeyShare, error)
Finalize completes the DKG protocol for participant p, computing their final key share. This should be called after all shares have been received and verified via FROST.Round2ReceiveShare.
The returned KeyShare contains the participant's secret key share and the group's combined public key, which is the same for all participants.
func (*FROST) NewParticipant ¶
NewParticipant creates a new participant for the DKG protocol.
The id parameter must be a unique integer from 1 to n (total participants). The random reader r is used to generate the participant's secret polynomial.
func (*FROST) NewRefreshParticipant ¶
NewRefreshParticipant creates a new participant for the refresh protocol.
The participant generates a zero-polynomial: coefficients[0] = 0 and coefficients[1..threshold-1] are random. Since the constant term is zero, the sum of all participants' zero-polynomials evaluated at any point contributes nothing to the reconstructed secret, preserving the group key.
The id parameter must be the participant's existing scalar identifier (from their [KeyShare.ID]). The random reader r is used to generate the random polynomial coefficients.
func (*FROST) NewReshareNewMember ¶
func (f *FROST) NewReshareNewMember(id group.Scalar) (*ReshareNewMember, error)
NewReshareNewMember creates a reshare state for a new committee member. The caller must be the NEW FROST instance (new threshold/total).
func (*FROST) NewReshareOldMember ¶
func (f *FROST) NewReshareOldMember(r io.Reader, share *KeyShare, oldMemberIDs []group.Scalar, newThreshold int) (*ReshareOldMember, error)
NewReshareOldMember creates a reshare state for an old committee member. The caller must be the OLD FROST instance (old threshold/total).
The oldMemberIDs slice lists all old members participating in the reshare; at least f.threshold must cooperate. The newThreshold parameter specifies the threshold for the new committee.
The method computes the Lagrange coefficient for this member within the cooperating set, weights the secret key share, and generates a fresh polynomial of degree newThreshold-1 whose constant term is lambda_i * s_i.
func (*FROST) PiggybackAggregate ¶
func (f *FROST) PiggybackAggregate( message []byte, sessionIndex uint64, currentCommitments []*SigningCommitment, shares []*PiggybackSignatureShare, publicKeys map[string]group.Point, groupKey group.Point, ) (*Signature, *PiggybackSessionCollector, error)
PiggybackAggregate aggregates piggybacked signature shares with per-share verification (mandatory per review C3), extracts the final signature, and collects next-session commitments.
The currentCommitments parameter contains the commitments for the current session (collected from the previous session's piggyback or from bootstrap). The shares contain both the signature shares for the current session and the NextCommitment for the next session.
publicKeys maps signer ID bytes to their public key, required for per-share verification.
func (*FROST) PiggybackSign ¶
func (f *FROST) PiggybackSign( r io.Reader, share *KeyShare, state *PiggybackNonceState, message []byte, commitments []*SigningCommitment, ) (*PiggybackSignatureShare, error)
PiggybackSign performs a 1-round signing session using the pending nonce from the previous session. The flow is:
- Pre-validate commitments (retryable on failure -- state unchanged).
- Pre-generate next nonce (retryable on failure -- state unchanged).
- Clone and consume the pending nonce (point of no return).
- Call SignRound2 with session-bound message.
- On SignRound2 failure: transition to FAILED state.
- On success: increment SessionIndex and return the piggybacked share.
func (*FROST) PiggybackVerify ¶
func (f *FROST) PiggybackVerify(message []byte, sessionIndex uint64, sig *Signature, groupKey group.Point) bool
PiggybackVerify is a convenience wrapper that verifies a signature produced by the piggyback protocol. It prepends the session index to the message before calling Verify, matching the session-bound message used during signing.
func (*FROST) RefreshFinalize ¶
func (f *FROST) RefreshFinalize(rp *RefreshParticipant, existingShare *KeyShare, allBroadcasts []*RefreshRound1Data) (*KeyShare, error)
RefreshFinalize completes the refresh protocol for participant rp, computing their new key share. This should be called after all deltas have been received and verified via FROST.RefreshRound2ReceiveDelta.
The returned KeyShare contains the participant's new secret key share and the SAME group public key as before. Old shares become invalid after refresh.
All n participants must participate in the refresh protocol (not just threshold). The allBroadcasts slice must contain broadcasts from every participant including rp itself.
func (*FROST) RefreshRound1PrivateSend ¶
func (f *FROST) RefreshRound1PrivateSend(rp *RefreshParticipant, recipientID group.Scalar) (*RefreshRound1PrivateData, error)
RefreshRound1PrivateSend computes and returns the private delta that participant rp must send to the specified recipient. The delta is the evaluation of rp's zero-polynomial at the recipient's ID.
This data must be transmitted over a secure, authenticated channel.
func (*FROST) RefreshRound2ReceiveDelta ¶
func (f *FROST) RefreshRound2ReceiveDelta(rp *RefreshParticipant, data *RefreshRound1PrivateData, senderCommitments []group.Point) error
RefreshRound2ReceiveDelta verifies a received delta against the sender's public commitments and stores it if valid. Returns an error if the delta fails verification, indicating a potentially malicious sender.
The verification uses Feldman's VSS scheme: it checks that delta * G == sum(Commitment[i] * recipientID^i).
Session binding: each refresh run generates fresh random polynomial coefficients and commitments. A delta replayed from a previous run will fail Feldman VSS verification against the current run's commitments.
func (*FROST) ReshareFinalize ¶
func (f *FROST) ReshareFinalize(rnm *ReshareNewMember, allBroadcasts []*ReshareRound1Data, expectedGroupKey group.Point) (*KeyShare, error)
ReshareFinalize completes the reshare protocol for new member rnm. The caller must be the NEW FROST instance.
It verifies that the sum of all old members' Commitments[0] equals the expectedGroupKey (group key preservation), then computes the new secret share as the sum of all received partial shares.
func (*FROST) ReshareNewMemberReceiveShare ¶
func (f *FROST) ReshareNewMemberReceiveShare(rnm *ReshareNewMember, data *ReshareRound1PrivateData, senderCommitments []group.Point) error
ReshareNewMemberReceiveShare verifies a received share from an old member using Feldman VSS and stores it. The caller must be the NEW FROST instance.
The verification checks: share * G == sum(Commitment[i] * recipientID^i).
Session binding: each reshare run generates fresh random polynomial coefficients and commitments. A share replayed from a previous run will fail Feldman VSS verification against the current run's commitments.
func (*FROST) ReshareOldMemberSendShare ¶
func (f *FROST) ReshareOldMemberSendShare(rom *ReshareOldMember, newMemberID group.Scalar) (*ReshareRound1PrivateData, error)
ReshareOldMemberSendShare computes the private share that old member rom must send to the specified new committee member. The caller must be the OLD FROST instance.
func (*FROST) Round1PrivateSend ¶
func (f *FROST) Round1PrivateSend(p *Participant, recipientID int) *Round1PrivateData
Round1PrivateSend computes and returns the private share that participant p must send to the specified recipient. This data must be transmitted over a secure, authenticated channel.
func (*FROST) Round2ReceiveShare ¶
func (f *FROST) Round2ReceiveShare(p *Participant, data *Round1PrivateData, senderCommitments []group.Point) error
Round2ReceiveShare verifies a received share against the sender's public commitments and stores it if valid. Returns an error if the share fails verification, indicating a potentially malicious sender.
The verification uses Feldman's VSS scheme: it checks that share * G == sum(Commitment[i] * recipientID^i).
func (*FROST) SignRound1 ¶
func (f *FROST) SignRound1(r io.Reader, share *KeyShare) (*SigningNonce, *SigningCommitment, error)
SignRound1 generates fresh nonces and a public commitment for a signing session. The returned SigningNonce must be kept secret and passed to FROST.SignRound2. The returned SigningCommitment must be broadcast to all other signers.
Each call to SignRound1 generates new random nonces. Nonces must never be reused across signing sessions.
func (*FROST) SignRound2 ¶
func (f *FROST) SignRound2( share *KeyShare, nonce *SigningNonce, message []byte, commitments []*SigningCommitment, ) (*SignatureShare, error)
SignRound2 computes this participant's signature share for the given message. It requires the participant's key share, their secret nonce from round 1, the message to sign, and all signing commitments from participating signers.
The commitments slice must include commitments from all signers participating in this signing session (at least threshold signers).
The nonce is consumed regardless of success or failure. After calling SignRound2, a new FROST.SignRound1 call is required for retry.
func (*FROST) Verify ¶
Verify checks whether a FROST signature is valid for the given message and group public key. Returns true if the signature is valid.
This performs standard Schnorr signature verification: z*G == R + c*Y, where c = H2(R, Y, message).
func (*FROST) VerifyShare ¶
func (f *FROST) VerifyShare( share *SignatureShare, publicKey group.Point, message []byte, commitments []*SigningCommitment, groupKey group.Point, ) (bool, error)
VerifyShare verifies an individual signature share against the signer's public key. This can be used by a coordinator to detect malicious signers before aggregation.
The verification equation is: z_i * G == R_i + c * lambda_i * PK_i where R_i = D_i + rho_i * E_i is the signer's effective commitment.
type Hasher ¶
type Hasher interface {
// H1 computes the binding factor for a signer.
// Inputs: message, encoded commitment list, signer ID.
H1(g group.Group, msg, encCommitList, signerID []byte) group.Scalar
// H2 computes the Schnorr challenge.
// Inputs: R point, public key Y, message.
H2(g group.Group, R, Y, msg []byte) group.Scalar
// H3 computes a nonce from seed and additional data.
// Inputs: seed, rho (binding factor), message.
H3(g group.Group, seed, rho, msg []byte) group.Scalar
// H4 hashes a message for signing.
// Currently unused by the core FROST protocol but defined as part of the
// FROST specification (RFC 9591, Section 4.7) for pre-hashing messages.
// Retained for specification completeness and future use.
H4(g group.Group, msg []byte) []byte
// H5 hashes the commitment list.
// Currently unused by the core FROST protocol but defined as part of the
// FROST specification (RFC 9591, Section 4.7) for commitment list hashing.
// Retained for specification completeness and future use.
H5(g group.Group, encCommitList []byte) []byte
}
Hasher defines the hash operations required by FROST. Different implementations can provide different hash functions and domain separation schemes.
type KeyShare ¶
type KeyShare struct {
ID group.Scalar
// This value must be kept private.
SecretKey group.Scalar
PublicKey group.Point
// participants and is used to verify signatures.
GroupKey group.Point
}
KeyShare represents a participant's share of the distributed secret key. KeyShares are produced by the DKG protocol via FROST.Finalize and are used for signing operations.
func (*KeyShare) Zero ¶
func (ks *KeyShare) Zero()
Zero securely erases secret material in the KeyShare. It zeroes both the secret key and the participant ID, since the ID is a Shamir evaluation point that could aid share reconstruction. Public fields (PublicKey, GroupKey) are left intact so callers can still reference the group key after cleanup.
type Participant ¶
type Participant struct {
// contains filtered or unexported fields
}
Participant holds the state for a single participant during the DKG protocol. Create instances using FROST.NewParticipant.
func (*Participant) Round1Broadcast ¶
func (p *Participant) Round1Broadcast() *Round1Data
Round1Broadcast returns the public data that this participant must broadcast to all other participants. This includes commitments to the participant's secret polynomial.
type PiggybackNonceState ¶
type PiggybackNonceState struct {
// ID is the participant's identifier.
ID group.Scalar
// PendingNonce holds the secret nonce values pre-generated during
// the previous session. Nil during bootstrap or after failure.
PendingNonce *SigningNonce
// PendingCommitment holds the public commitment broadcast during
// the previous session. Nil during bootstrap or after failure.
PendingCommitment *SigningCommitment
// SessionIndex is a monotonically increasing counter tracking
// the nonce generation epoch. Cryptographically bound into the
// binding factor hash to prevent cross-session replay.
SessionIndex uint64
// Active indicates whether this state has a valid pending nonce
// ready for consumption. Set to true after bootstrap or after
// a successful piggyback round. Set to false on failure.
Active bool
// contains filtered or unexported fields
}
PiggybackNonceState tracks the pending nonce commitment that was piggybacked in the previous signing session. Each signer maintains one instance per signing group.
func NewPiggybackState ¶
func NewPiggybackState(share *KeyShare) *PiggybackNonceState
NewPiggybackState creates a fresh piggyback state for a signer. The state starts in the INACTIVE state; call BootstrapRound1 to begin.
func (*PiggybackNonceState) ReBootstrap ¶
func (state *PiggybackNonceState) ReBootstrap()
ReBootstrap discards the current piggyback state and resets to INACTIVE. All pending nonce material is securely erased. After calling ReBootstrap, the signer must run a new bootstrap round (BootstrapRound1 + BootstrapRound2) before signing again.
Re-bootstrap by any signer requires all signers in the active set to re-bootstrap, since the commitment set changes.
func (*PiggybackNonceState) Zero ¶
func (state *PiggybackNonceState) Zero()
Zero securely erases all secret material in the piggyback state. After calling Zero, the state is unusable and must not be reused.
type PiggybackSessionCollector ¶
type PiggybackSessionCollector struct {
// NextCommitments maps signer ID (as byte key) to the commitment
// piggybacked for the next session.
NextCommitments map[string]*SigningCommitment
// ExpectedSigners is the number of signers expected.
ExpectedSigners int
// SessionIndex is the epoch for which these commitments are valid.
SessionIndex uint64
}
PiggybackSessionCollector aggregates piggybacked commitments received from all signers for the next session.
func CollectPiggybackCommitments ¶
func CollectPiggybackCommitments( shares []*PiggybackSignatureShare, expectedSessionIndex uint64, ) (*PiggybackSessionCollector, error)
CollectPiggybackCommitments extracts next-session commitments from piggybacked signature shares. It validates:
- NextCommitment is non-nil
- NextCommitment.ID matches the SignatureShare.ID
- SessionIndex matches the expected value
- No identity-point commitments
- No duplicate signer IDs
func (*PiggybackSessionCollector) Commitments ¶
func (c *PiggybackSessionCollector) Commitments() []*SigningCommitment
Commitments returns the collected commitments sorted by signer ID bytes. Deterministic ordering is required for consistent binding factor computation.
type PiggybackSignatureShare ¶
type PiggybackSignatureShare struct {
// current session.
SignatureShare
// session. Other signers must store this and use it as the
// round-1 commitment in the next session.
NextCommitment *SigningCommitment
// signing session that produced this share. Recipients use this to detect
// stale or replayed messages and to match commitments to sessions.
SessionIndex uint64
}
PiggybackSignatureShare extends SignatureShare with the next session's nonce commitment, piggybacked onto the round-2 message.
type PoseidonHasher ¶
type PoseidonHasher struct {
// contains filtered or unexported fields
}
PoseidonHasher implements Hasher using the Poseidon hash function. This is optimized for zkSNARK circuits, providing efficient in-circuit verification.
Poseidon operates over the BN254 scalar field, which is the same field used by Baby Jubjub. Inputs are encoded as field elements for optimal zkSNARK constraint count.
For inputs exceeding Poseidon's native 16-element limit, a sponge construction is used: the first 16 elements are hashed, then subsequent batches of up to 15 elements are hashed with the previous output chained as the first input element. This removes any signer count limitation.
Domain separation is achieved using unique initial field element values for each hash function (H1-H5).
KNOWN LIMITATION (BJJ subgroup bias): For BJJ subgroup (~251-bit order), simple modular reduction of a ~254-bit Poseidon output introduces statistical bias of ~7/8 statistical distance from uniform. For applications requiring full 128-bit uniformity with BJJ, use SHA256Hasher or Blake2bHasher instead.
H1-H3 panic on hash errors. This represents an invariant violation (nil inputs or corrupted state), not a recoverable runtime condition.
func NewPoseidonHasher ¶
func NewPoseidonHasher() *PoseidonHasher
NewPoseidonHasher creates a PoseidonHasher with pre-computed domain separators.
func (*PoseidonHasher) H1 ¶
H1 implements Hasher.H1 (binding factor computation). Hashes: domain_H1 || len(msgElems) || msg || len(commitElems) || encCommitList || signerID as field elements.
Each variable-length input (msg, encCommitList) is preceded by its element count encoded as a field element. This length-prefixed encoding prevents concatenation ambiguity: two different (msg, encCommitList) pairs that produce the same concatenated field elements will have different length prefixes. signerID is always exactly 32 bytes (one field element) so no prefix is needed.
func (*PoseidonHasher) H2 ¶
H2 implements Hasher.H2 (Schnorr challenge). Hashes: domain_H2 || len(R_elems) || R || len(Y_elems) || Y || len(msg_elems) || msg as field elements.
Each variable-length input is preceded by its element count to prevent concatenation ambiguity: different point encodings (compressed vs uncompressed) produce different element counts, and variable-length messages are delimited.
func (*PoseidonHasher) H3 ¶
H3 implements Hasher.H3 (nonce generation). Hashes: domain_H3 || len(seed_elems) || seed || len(rho_elems) || rho || len(msg_elems) || msg as field elements.
Length prefixes are included for defense-in-depth, even though seed and rho are typically fixed-length (32 bytes each). This prevents boundary ambiguity if callers pass non-standard lengths.
type RailgunHasher ¶
type RailgunHasher struct {
// contains filtered or unexported fields
}
RailgunHasher implements Hasher with exact compatibility for circomlibjs eddsa.verifyPoseidon.
The circomlibjs library computes the challenge as:
c = poseidon([R.x, R.y, pk.x, pk.y, msg])
Where R and pk are Baby JubJub points (as uncompressed X,Y coordinates) and msg is a single field element (not chunked).
This hasher produces signatures that can be verified by:
import { eddsa } from '@railgun-community/circomlibjs';
eddsa.verifyPoseidon(msg, signature, pubkey);
func NewRailgunHasher ¶
func NewRailgunHasher() *RailgunHasher
NewRailgunHasher creates a hasher compatible with circomlibjs eddsa.verifyPoseidon.
func (*RailgunHasher) H1 ¶
H1 implements Hasher.H1 (binding factor computation). Delegates to PoseidonHasher since this is internal to FROST.
func (*RailgunHasher) H2 ¶
H2 implements Hasher.H2 (Schnorr/EdDSA challenge). Computes: c = poseidon([R.x, R.y, A.x, A.y, msg]) Where A = Y / 8 (the circomlibjs-compatible public key derived from FROST group key Y).
This matches circomlibjs eddsa.verifyPoseidon which uses:
- verification: S * Base8 = R + (c * 8) * A
- challenge: c = poseidon([R.x, R.y, A.x, A.y, msg])
Points can be provided in either compressed (32 bytes) or uncompressed (64 bytes) format. Compressed points are automatically decompressed.
func (*RailgunHasher) H3 ¶
H3 implements Hasher.H3 (nonce generation). Delegates to PoseidonHasher since this is internal to FROST.
type RefreshParticipant ¶
type RefreshParticipant struct {
// contains filtered or unexported fields
}
RefreshParticipant holds the state for a participant during the refresh protocol (proactive share rotation). Each participant generates a zero-polynomial (constant term = 0) so that the sum of all participants' polynomials preserves the group secret while changing individual shares.
Create instances using FROST.NewRefreshParticipant.
func (*RefreshParticipant) Round1Broadcast ¶
func (rp *RefreshParticipant) Round1Broadcast() *RefreshRound1Data
Round1Broadcast returns the public data that this refresh participant must broadcast to all other participants.
func (*RefreshParticipant) Zero ¶
func (rp *RefreshParticipant) Zero()
Zero securely erases all secret material held by the refresh participant. This is called automatically by FROST.RefreshFinalize but can also be called explicitly to clean up after an aborted refresh.
type RefreshRound1Data ¶
type RefreshRound1Data struct {
// ID is the unique identifier of the broadcasting participant.
ID group.Scalar
// Commitments are commitments to the zero-polynomial coefficients.
// Commitments[0] must be the identity point (since coefficients[0] = 0).
Commitments []group.Point
}
RefreshRound1Data is the public data broadcast by a participant during the refresh protocol. Recipients use the commitments to verify received deltas via Feldman VSS.
type RefreshRound1PrivateData ¶
type RefreshRound1PrivateData struct {
// FromID is the sender's participant identifier.
FromID group.Scalar
// ToID is the intended recipient's participant identifier.
ToID group.Scalar
// Delta is the sender's zero-polynomial evaluated at the recipient's ID.
Delta group.Scalar
}
RefreshRound1PrivateData is the private delta sent from one participant to a specific recipient during the refresh protocol. This data must be sent over a secure, authenticated channel.
type ReshareNewMember ¶
type ReshareNewMember struct {
// contains filtered or unexported fields
}
ReshareNewMember holds the state for a new committee member during reshare. It accumulates verified shares from old committee members and finalizes into a new KeyShare once all shares are collected.
func (*ReshareNewMember) Zero ¶
func (rnm *ReshareNewMember) Zero()
Zero securely erases all secret material held by the reshare new member. Call this after FROST.ReshareFinalize to clean up accumulated shares.
type ReshareOldMember ¶
type ReshareOldMember struct {
// contains filtered or unexported fields
}
ReshareOldMember holds the state for an old committee member during reshare. The member's secret share is Lagrange-weighted and split into a new polynomial of degree newThreshold-1, so the sum of all old members' constant terms reconstructs the original group secret.
func (*ReshareOldMember) Round1Broadcast ¶
func (rom *ReshareOldMember) Round1Broadcast() *ReshareRound1Data
Round1Broadcast returns the public data that this old member must broadcast to all new committee members.
func (*ReshareOldMember) Zero ¶
func (rom *ReshareOldMember) Zero()
Zero securely erases all secret material from the old member's reshare state. Callers MUST call this after all shares have been sent to new members. Unlike FROST.RefreshFinalize which auto-zeroes, the reshare protocol requires explicit cleanup because old and new members are separate roles.
type ReshareRound1Data ¶
type ReshareRound1Data struct {
}
ReshareRound1Data is the public data broadcast by an old committee member. All new members use this to verify received shares via Feldman VSS and to confirm group key preservation.
type ReshareRound1PrivateData ¶
type ReshareRound1PrivateData struct {
}
ReshareRound1PrivateData is the private share sent from an old committee member to a new committee member. This data must be transmitted over a secure, authenticated channel.
type Round1Data ¶
type Round1Data struct {
// ID is the unique identifier of the broadcasting participant.
ID group.Scalar
// Commitments are Pedersen commitments to the polynomial coefficients.
// Commitments[i] = coefficients[i] * G, where G is the group generator.
Commitments []group.Point
}
Round1Data contains the public data broadcast by a participant during round 1 of the DKG protocol. This includes commitments to the participant's secret polynomial coefficients.
type Round1PrivateData ¶
type Round1PrivateData struct {
// FromID is the sender's participant identifier.
FromID group.Scalar
// ToID is the intended recipient's participant identifier.
ToID group.Scalar
// This value must be kept confidential during transmission.
Share group.Scalar
}
Round1PrivateData contains the private share sent from one participant to another during round 1 of the DKG protocol. This data must be sent over a secure, authenticated channel.
type SHA256Hasher ¶
type SHA256Hasher struct{}
SHA256Hasher implements Hasher using SHA-256. This is the default hasher for general use.
Domain separation prefixes:
- H1: "rho" (binding factor)
- H2: "chal" (Schnorr challenge)
- H3: "nonce" (nonce derivation)
- H4: "msg" (message pre-hash)
- H5: "com" (commitment list hash)
Each hash output is expanded to 64 bytes (two SHA-256 invocations with counter suffixes 0x00 and 0x01) for uniform reduction modulo the group order (bias < 2^-128).
type Secp256k1Hasher ¶
type Secp256k1Hasher struct {
// Prefix is the domain separation prefix.
// Default: "FROST-secp256k1-SHA256-v1"
Prefix string
}
Secp256k1Hasher implements Hasher using SHA-256 with domain separation for secp256k1 FROST signatures.
This hasher follows the FROST-secp256k1-SHA256 specification with domain separation prefix for each hash function.
Domain separation format: prefix + tag + inputs
func NewSecp256k1Hasher ¶
func NewSecp256k1Hasher() *Secp256k1Hasher
NewSecp256k1Hasher creates a Secp256k1Hasher with the standard FROST prefix.
func (*Secp256k1Hasher) H1 ¶
H1 implements Hasher.H1 (binding factor computation). Hashes: prefix || "rho" || msg || encCommitList || signerID
func (*Secp256k1Hasher) H2 ¶
H2 implements Hasher.H2 (Schnorr challenge). Hashes: prefix || "chal" || R || Y || msg
func (*Secp256k1Hasher) H3 ¶
H3 implements Hasher.H3 (nonce generation). Hashes: prefix || "nonce" || seed || rho || msg
type Signature ¶
type Signature struct {
// R is the commitment point (nonce point).
R group.Point
// Z is the response scalar.
Z group.Scalar
}
Signature represents a Schnorr signature produced by the FROST protocol. It can be verified against the group public key using FROST.Verify.
type SignatureShare ¶
SignatureShare is a participant's contribution to the final signature, produced during round 2 of signing.
type SigningCommitment ¶
type SigningCommitment struct {
// ID is the participant's identifier.
ID group.Scalar
// HidingPoint is the public commitment to the hiding nonce (D * G).
HidingPoint group.Point
// BindingPoint is the public commitment to the binding nonce (E * G).
BindingPoint group.Point
}
SigningCommitment is the public commitment broadcast by a participant during round 1 of signing.
type SigningNonce ¶
type SigningNonce struct {
// ID is the participant's identifier.
ID group.Scalar
// D is the hiding nonce (secret).
D group.Scalar
// E is the binding nonce (secret).
E group.Scalar
}
SigningNonce holds the secret nonce values generated by a participant during round 1 of signing. These values must be kept secret and never reused.