frost

package
v0.0.0-...-c62aa3f Latest Latest
Warning

This package is not in the latest version of its module.

Go to latest
Published: Mar 30, 2026 License: MIT Imports: 16 Imported by: 0

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:

  1. Each participant generates a random polynomial and broadcasts commitments to its coefficients using Participant.Round1Broadcast.
  2. Each participant sends private shares to all other participants using FROST.Round1PrivateSend.
  3. Each participant verifies received shares against the broadcasted commitments using FROST.Round2ReceiveShare.
  4. 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:

  1. Each signer generates nonces and commitments using FROST.SignRound1.
  2. Each signer computes their signature share using FROST.SignRound2.
  3. Signature shares are aggregated into a final signature using FROST.Aggregate.
  4. 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

View Source
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

View Source
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.

View Source
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.

func (*Blake2bHasher) H1

func (h *Blake2bHasher) H1(g group.Group, msg, encCommitList, signerID []byte) group.Scalar

H1 implements Hasher.H1 (binding factor computation).

func (*Blake2bHasher) H2

func (h *Blake2bHasher) H2(g group.Group, R, Y, msg []byte) group.Scalar

H2 implements Hasher.H2 (Schnorr challenge).

func (*Blake2bHasher) H3

func (h *Blake2bHasher) H3(g group.Group, seed, rho, msg []byte) group.Scalar

H3 implements Hasher.H3 (nonce generation).

func (*Blake2bHasher) H4

func (h *Blake2bHasher) H4(g group.Group, msg []byte) []byte

H4 implements Hasher.H4 (message hashing).

func (*Blake2bHasher) H5

func (h *Blake2bHasher) H5(g group.Group, encCommitList []byte) []byte

H5 implements Hasher.H5 (commitment list hashing).

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

func New(g group.Group, threshold, total int) (*FROST, error)

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

func NewWithHasher(g group.Group, threshold, total int, hasher Hasher) (*FROST, error)

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

func (f *FROST) NewParticipant(r io.Reader, id int) (*Participant, error)

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

func (f *FROST) NewRefreshParticipant(r io.Reader, id group.Scalar) (*RefreshParticipant, error)

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:

  1. Pre-validate commitments (retryable on failure -- state unchanged).
  2. Pre-generate next nonce (retryable on failure -- state unchanged).
  3. Clone and consume the pending nonce (point of no return).
  4. Call SignRound2 with session-bound message.
  5. On SignRound2 failure: transition to FAILED state.
  6. 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) Threshold

func (f *FROST) Threshold() int

Threshold returns the minimum number of signers required (t).

func (*FROST) Total

func (f *FROST) Total() int

Total returns the total number of participants (n).

func (*FROST) Verify

func (f *FROST) Verify(message []byte, sig *Signature, groupKey group.Point) bool

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 is the unique identifier for this participant (1 to n).
	ID group.Scalar

	// SecretKey is this participant's share of the group secret key.
	// This value must be kept private.
	SecretKey group.Scalar

	// PublicKey is the public key corresponding to this participant's secret share.
	PublicKey group.Point

	// GroupKey is the combined group public key. This is the same for all
	// 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 {
	// SignatureShare is the standard FROST signature share for the
	// current session.
	SignatureShare

	// NextCommitment is the nonce commitment for the NEXT signing
	// session. Other signers must store this and use it as the
	// round-1 commitment in the next session.
	NextCommitment *SigningCommitment

	// SessionIndex identifies the nonce epoch of the NextCommitment, NOT the
	// 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

func (h *PoseidonHasher) H1(g group.Group, msg, encCommitList, signerID []byte) group.Scalar

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

func (h *PoseidonHasher) H2(g group.Group, R, Y, msg []byte) group.Scalar

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

func (h *PoseidonHasher) H3(g group.Group, seed, rho, msg []byte) group.Scalar

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.

func (*PoseidonHasher) H4

func (h *PoseidonHasher) H4(g group.Group, msg []byte) []byte

H4 implements Hasher.H4 (message hashing). Returns the Poseidon hash of: domain_H4 || msg as a 32-byte big-endian value.

func (*PoseidonHasher) H5

func (h *PoseidonHasher) H5(g group.Group, encCommitList []byte) []byte

H5 implements Hasher.H5 (commitment list hashing). Returns the Poseidon hash of: domain_H5 || encCommitList as a 32-byte big-endian value.

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

func (h *RailgunHasher) H1(g group.Group, msg, encCommitList, signerID []byte) group.Scalar

H1 implements Hasher.H1 (binding factor computation). Delegates to PoseidonHasher since this is internal to FROST.

func (*RailgunHasher) H2

func (h *RailgunHasher) H2(g group.Group, R, Y, msg []byte) group.Scalar

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

func (h *RailgunHasher) H3(g group.Group, seed, rho, msg []byte) group.Scalar

H3 implements Hasher.H3 (nonce generation). Delegates to PoseidonHasher since this is internal to FROST.

func (*RailgunHasher) H4

func (h *RailgunHasher) H4(g group.Group, msg []byte) []byte

H4 implements Hasher.H4 (message hashing). Delegates to PoseidonHasher since this is internal to FROST.

func (*RailgunHasher) H5

func (h *RailgunHasher) H5(g group.Group, encCommitList []byte) []byte

H5 implements Hasher.H5 (commitment list hashing). 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 {
	ID          group.Scalar
	Commitments []group.Point
}

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 {
	FromID group.Scalar
	ToID   group.Scalar
	Share  group.Scalar
}

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

	// Share is the sender's polynomial evaluated at the recipient's ID.
	// 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).

func (*SHA256Hasher) H1

func (h *SHA256Hasher) H1(g group.Group, msg, encCommitList, signerID []byte) group.Scalar

H1 implements Hasher.H1.

func (*SHA256Hasher) H2

func (h *SHA256Hasher) H2(g group.Group, R, Y, msg []byte) group.Scalar

H2 implements Hasher.H2.

func (*SHA256Hasher) H3

func (h *SHA256Hasher) H3(g group.Group, seed, rho, msg []byte) group.Scalar

H3 implements Hasher.H3.

func (*SHA256Hasher) H4

func (h *SHA256Hasher) H4(g group.Group, msg []byte) []byte

H4 implements Hasher.H4.

func (*SHA256Hasher) H5

func (h *SHA256Hasher) H5(g group.Group, encCommitList []byte) []byte

H5 implements Hasher.H5.

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

func (h *Secp256k1Hasher) H1(g group.Group, msg, encCommitList, signerID []byte) group.Scalar

H1 implements Hasher.H1 (binding factor computation). Hashes: prefix || "rho" || msg || encCommitList || signerID

func (*Secp256k1Hasher) H2

func (h *Secp256k1Hasher) H2(g group.Group, R, Y, msg []byte) group.Scalar

H2 implements Hasher.H2 (Schnorr challenge). Hashes: prefix || "chal" || R || Y || msg

func (*Secp256k1Hasher) H3

func (h *Secp256k1Hasher) H3(g group.Group, seed, rho, msg []byte) group.Scalar

H3 implements Hasher.H3 (nonce generation). Hashes: prefix || "nonce" || seed || rho || msg

func (*Secp256k1Hasher) H4

func (h *Secp256k1Hasher) H4(g group.Group, msg []byte) []byte

H4 implements Hasher.H4 (message hashing). Returns: SHA-256(prefix || "msg" || msg)

func (*Secp256k1Hasher) H5

func (h *Secp256k1Hasher) H5(g group.Group, encCommitList []byte) []byte

H5 implements Hasher.H5 (commitment list hashing). Returns: SHA-256(prefix || "com" || encCommitList)

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

type SignatureShare struct {
	// ID is the participant's identifier.
	ID group.Scalar

	// Z is the signature share value.
	Z group.Scalar
}

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.

Jump to

Keyboard shortcuts

? : This menu
/ : Search site
f or F : Jump to
y or Y : Canonical URL