Documentation
¶
Overview ¶
Package reshare implements the two proactive secret-sharing primitives Lens needs to evolve a key era's share distribution without changing the persistent group public key.
Refresh — same-committee zero-polynomial proactive update (Herzberg-Jakobsson-Jarecki-Krawczyk-Yung 1997, "PSS"). The committee is fixed; every party samples a random degree-(t-1) polynomial z_i(X) with z_i(0) = 0 and contributes z_i(α_j) to each peer j. Each party updates s'_j = s_j + Σ_i z_i(α_j). The master secret is unchanged because Σ_i z_i(0) = 0. Use case: periodic share-randomization within a stable validator set, to defeat a mobile adversary that compromises < t parties per epoch and accumulates shares across epochs.
Reshare — validator-set resharing. The OLD committee O and NEW committee N are different (potentially disjoint, potentially different threshold). The new committee has no old shares and so cannot apply zero-polynomial deltas. Instead, a qualified subset Q ⊆ O with |Q| ≥ t_old executes a one-shot Shamir-from- Lagrange transformation: each i ∈ Q samples g_i(X) of degree t_new-1 with g_i(0) = s_i (its OWN old share as the constant term), and privately delivers g_i(β_j) to each new party j ∈ N. Each new party j computes
s'_j = Σ_{i ∈ Q} λ^Q_i · g_i(β_j)
where λ^Q_i are the Lagrange coefficients for Q evaluated at 0. Define G(X) = Σ_{i ∈ Q} λ^Q_i · g_i(X). Then deg(G) = t_new − 1, and G(0) = s by Lagrange interpolation over Q at X = 0. Use case: validator-set rotation at Quasar epoch boundaries.
Both primitives leave the public key X = s·G unchanged. The genesis values (G, H, X) are persistent for the entire key era. Only the share distribution changes. This is the property that lets Quasar avoid running a full DKG on every validator-set rotation.
Security model (kernel) ¶
The kernel APIs Refresh / Reshare implement the arithmetic correctness of the two primitives, against a HONEST-BUT-CURIOUS adversary that may statically corrupt up to t-1 parties.
VSR — production deployment ¶
The kernel is embedded in a full Verifiable Secret Resharing protocol with the following components, implemented in sibling files:
- commit.go — Pedersen commits (g^c · h^r) to f_i (Refresh) and g_i (Reshare); recipients verify each share against the committed polynomial.
- transcript.go — Domain-separated transcript binding the entire resharing exchange.
- complaint.go — Complaint format, signed evidence, complaint quorum logic, deterministic disqualification.
- keyshare.go — Wraps reshared scalar shares into complete threshold.KeyShare instances by regenerating Lambda + Seeds + MACKeys + VerificationShare.
- pairwise.go — Authenticated pairwise X25519+Ed25519 KEX → KDF for Seeds / MACKeys under domain-separated tags.
- activation.go — Post-reshare activation cert: the new committee threshold-signs the resharing transcript hash under the unchanged GroupKey. The chain accepts the new epoch only when the activation verifies.
Index ¶
- Constants
- Variables
- func AuthenticatedKex(privIEph []byte, pubJEph []byte, sigJEph []byte, jStaticKey ed25519.PublicKey, ...) ([]byte, error)
- func CanonicalPair(i, j int) [2]int
- func CommitDigest(commits []primitives.Point, suite hash.HashSuite) [32]byte
- func CommitToPoly(c primitives.Curve, secretCoeffs, blindCoeffs []primitives.Scalar) ([]primitives.Point, error)
- func ComplaintHash(c *Complaint) [32]byte
- func ComputeDisqualifiedSet(complaints []*Complaint, thresholdOld int) map[int]struct{}
- func DeriveMACKeys(K int, authKex map[[2]int][]byte, chainID, groupID []byte, ...) (map[[2]int][]byte, error)
- func DeriveSeeds(K int, authKex map[[2]int][]byte, selfSeeds map[int][]byte, ...) (map[[2]int][]byte, error)
- func DisqualificationThreshold(thresholdOld int) int
- func EraseScalar(s primitives.Scalar)
- func FilterQualifiedQuorum(originalQuorum []int, disqualified map[int]struct{}, tOld int) ([]int, error)
- func KDFOutput(suite hash.HashSuite, tag string, authKex []byte, chainID, groupID []byte, ...) []byte
- func Refresh(c primitives.Curve, shares map[int]primitives.Scalar, threshold int, ...) (map[int]primitives.Scalar, error)
- func Reshare(c primitives.Curve, oldShares map[int]primitives.Scalar, tOld int, ...) (map[int]primitives.Scalar, error)
- func SignEphemeral(priv ed25519.PrivateKey, pubEph []byte, transcriptHash [32]byte) []byte
- func ValidatorSetHash(publicKeys [][]byte, suite hash.HashSuite) [32]byte
- func Verify(c primitives.Curve, shares map[int]primitives.Scalar, t int) (primitives.Scalar, error)
- func VerifyActivation(cert *ActivationCert, localTranscriptHash [32]byte, localExchangeHash [32]byte, ...) error
- func VerifyShareAgainstCommits(c primitives.Curve, share, blind primitives.Scalar, commits []primitives.Point, ...) error
- func X25519Pair(privA, pubB []byte) ([]byte, error)
- type ActivationCert
- type ActivationMessage
- type Complaint
- type ComplaintReason
- type PairwiseKeyMaterial
- type ReshareTranscript
- type TranscriptInputs
Constants ¶
const TranscriptPersonalization = "lens.reshare.transcript.v1"
TranscriptPersonalization is the legacy BLAKE3 personalization string. Kept as an exported symbol because some external consumers (cross-language KAT loaders) reference it; the canonical transcript binding is now produced via the HashSuite layer.
Variables ¶
var ( ErrActivationFailed = errors.New("reshare: activation cert failed to verify under group public key") ErrTranscriptMismatch = errors.New("reshare: activation transcript hash does not match local view") )
Errors returned by the activation circuit-breaker.
var ( ErrCommitMismatch = errors.New("reshare: commitment verification failed") ErrCommitWrongLength = errors.New("reshare: commit vector has wrong length") ErrInconsistentDigests = errors.New("reshare: cross-recipient commit digest mismatch") )
Errors specific to commitment verification.
var ( ErrInvalidThresholdOld = errors.New("reshare: t_old must be >= 1") ErrInvalidThresholdNew = errors.New("reshare: t_new must be >= 1") ErrTOldExceedsOldSet = errors.New("reshare: t_old exceeds size of old committee") ErrTNewExceedsNewSet = errors.New("reshare: t_new exceeds size of new committee") ErrEmptyNewSet = errors.New("reshare: empty new committee") ErrZeroPartyID = errors.New("reshare: party IDs must be 1-indexed (no 0)") ErrDuplicateNewID = errors.New("reshare: duplicate ID in new committee") ErrTOldShortfall = errors.New("reshare: fewer than t_old shares supplied; cannot reconstruct s") )
Errors returned by the package.
var ErrInsufficientQuorum = errors.New("reshare: qualified quorum below t_old after disqualification")
ErrInsufficientQuorum signals that too many resharing parties were disqualified for the protocol to recover.
Functions ¶
func AuthenticatedKex ¶
func AuthenticatedKex( privIEph []byte, pubJEph []byte, sigJEph []byte, jStaticKey ed25519.PublicKey, transcriptHash [32]byte, suite hash.HashSuite, ) ([]byte, error)
AuthenticatedKex runs the pairwise authenticated key exchange and returns the auth_kex_ij value. The signed ephemeral protects against active man-in-the-middle.
suite=nil resolves to the production default (Lens-SHA3).
func CanonicalPair ¶
CanonicalPair returns the (i, j) tuple in canonical (smaller-first) order. Used as a map key for pairwise material.
func CommitDigest ¶
func CommitDigest(commits []primitives.Point, suite hash.HashSuite) [32]byte
CommitDigest returns the canonical 32-byte digest over a commit vector under the supplied HashSuite. suite=nil resolves to the production default (Lens-SHA3).
func CommitToPoly ¶
func CommitToPoly( c primitives.Curve, secretCoeffs, blindCoeffs []primitives.Scalar, ) ([]primitives.Point, error)
CommitToPoly produces the t Pedersen commitments to the secret-poly coefficients c_k together with the matching blinding-poly coefficients r_k.
func ComplaintHash ¶
ComplaintHash returns BLAKE3 over the complaint's canonical bytes (signature included).
func ComputeDisqualifiedSet ¶
ComputeDisqualifiedSet takes a slice of validated complaints and returns the set of sender IDs that meet the disqualification threshold. Every honest party that processes the same complaint set returns the same disqualified set.
Complaints are deduplicated by (sender, complainer) tuple.
func DeriveMACKeys ¶
func DeriveMACKeys( K int, authKex map[[2]int][]byte, chainID, groupID []byte, eraID, generation uint64, suite hash.HashSuite, outLen int, ) (map[[2]int][]byte, error)
DeriveMACKeys mirrors DeriveSeeds with the "lens.reshare.mac-key.v1" tag and only off-diagonal entries.
func DeriveSeeds ¶
func DeriveSeeds( K int, authKex map[[2]int][]byte, selfSeeds map[int][]byte, chainID, groupID []byte, eraID, generation uint64, suite hash.HashSuite, outLen int, ) (map[[2]int][]byte, error)
DeriveSeeds populates the per-pair PRF seed map for a committee of size K. suite=nil resolves to the production default.
func DisqualificationThreshold ¶
DisqualificationThreshold returns the minimum number of distinct, validly-signed complaints needed to disqualify a sender.
Default: t_old - 1. Rationale: any single Byzantine validator can emit one false complaint to slow the protocol, but to disqualify an honest sender the adversary needs t_old - 1 collaborators — exceeding the static-corruption threshold of t_old - 1 by exactly one.
func EraseScalar ¶
func EraseScalar(s primitives.Scalar)
EraseScalar overwrites the scalar's binary representation with zero bytes. After activation, every old share MUST be erased — failure to do so undermines the proactive-security guarantee.
func FilterQualifiedQuorum ¶
func FilterQualifiedQuorum( originalQuorum []int, disqualified map[int]struct{}, tOld int, ) ([]int, error)
FilterQualifiedQuorum returns the survivor set Q' = Q \ disqualified. Returns ErrInsufficientQuorum if |Q'| < tOld.
func KDFOutput ¶
func KDFOutput( suite hash.HashSuite, tag string, authKex []byte, chainID, groupID []byte, eraID, generation uint64, partyI, partyJ int, outLen int, ) []byte
KDFOutput derives a fixed-length output from keying material under the supplied HashSuite's pairwise KDF. suite=nil resolves to the production default (Lens-SHA3).
The tag is folded into the chainID label with a `|` separator so two callers with distinct tags but the same remaining inputs always produce distinct bytes.
func Refresh ¶
func Refresh( c primitives.Curve, shares map[int]primitives.Scalar, threshold int, randSource io.Reader, ) (map[int]primitives.Scalar, error)
Refresh runs the HJKY97 same-committee proactive update on a fixed committee. The set of party IDs and the threshold are unchanged: only the share values rotate to fresh independent randomness while preserving Σ_j λ^T_j · s_j = s for every threshold subset T.
Algorithm (per HJKY97 §3, "zero-polynomial" form):
Each party i samples a random degree-(t-1) polynomial z_i(X) over the curve's scalar field with z_i(0) = 0 (i.e. no constant term).
Each party i privately delivers z_i(α_j) to every party j in the committee, where α_j is the Shamir evaluation point of party j (here α_j = j, the 1-indexed party ID).
Each party j updates its share:
s'_j = s_j + Σ_i z_i(α_j) (mod q)
func Reshare ¶
func Reshare( c primitives.Curve, oldShares map[int]primitives.Scalar, tOld int, newSet []int, tNew int, randSource io.Reader, ) (map[int]primitives.Scalar, error)
Reshare runs the proactive secret-resharing protocol described in the package doc on the supplied curve. Inputs:
- c — the curve s lives in (must match the GroupKey's curve).
- oldShares — partyID → secret-share scalar (1-indexed evaluation point). The map MUST contain at least t_old entries.
- tOld — old reconstruction threshold.
- newSet — new committee (slice of distinct 1-indexed party IDs).
- tNew — new reconstruction threshold (≤ |newSet|).
- randSource — randomness source for fresh polynomial coefficients (nil → crypto/rand.Reader).
Returns the new share map keyed by new partyID. The output share at partyID j satisfies the (t_new, |newSet|)-Shamir relation against the SAME master secret s as the input shares.
func SignEphemeral ¶
func SignEphemeral( priv ed25519.PrivateKey, pubEph []byte, transcriptHash [32]byte, ) []byte
SignEphemeral produces the signature j ships with its ephemeral public key.
func ValidatorSetHash ¶
ValidatorSetHash returns the canonical hash of a validator set. suite=nil resolves to the production default (Lens-SHA3).
func Verify ¶
func Verify(c primitives.Curve, shares map[int]primitives.Scalar, t int) (primitives.Scalar, error)
Verify is a debugging helper: it Lagrange-interpolates the input shares at X=0 (using the smallest-ID t-subset) and returns the reconstructed master secret.
IMPORTANT: This recovers s, so it MUST NOT be used in production — only in tests and KAT verification. Calling it gives the caller the secret.
func VerifyActivation ¶
func VerifyActivation( cert *ActivationCert, localTranscriptHash [32]byte, localExchangeHash [32]byte, suite hash.HashSuite, verify func(message []byte, signature []byte) bool, ) error
VerifyActivation runs the chain-level activation check. suite=nil resolves to the production default (Lens-SHA3).
func VerifyShareAgainstCommits ¶
func VerifyShareAgainstCommits( c primitives.Curve, share, blind primitives.Scalar, commits []primitives.Point, x primitives.Scalar, ) error
VerifyShareAgainstCommits checks the recipient-side equation
share·G + blind·H ?= Σ_{k=0..t-1} x^k · C_k
func X25519Pair ¶
X25519Pair runs an X25519 key exchange and returns the 32-byte shared secret.
Types ¶
type ActivationCert ¶
type ActivationCert struct {
Message ActivationMessage
Signature []byte
}
ActivationCert wraps the threshold signature emitted by the new committee over an ActivationMessage.
type ActivationMessage ¶
type ActivationMessage struct {
Transcript TranscriptInputs
}
ActivationMessage is the canonical bytes-to-be-signed for the post-reshare circuit-breaker.
func (*ActivationMessage) SignableBytes ¶
func (a *ActivationMessage) SignableBytes(suite hash.HashSuite) []byte
SignableBytes returns the canonical bytes the new committee threshold-signs to produce an activation cert.
type Complaint ¶
type Complaint struct {
TranscriptHash [32]byte
SenderID int
ComplainerID int
Reason ComplaintReason
Evidence []byte
Signature []byte
ComplainerKey ed25519.PublicKey
}
Complaint is a signed assertion that sender PartyID misbehaved during the resharing protocol.
func (*Complaint) Bytes ¶
Bytes returns the canonical signed payload for a complaint. The Signature field is excluded (it is computed OVER these bytes).
func (*Complaint) Sign ¶
func (c *Complaint) Sign(priv ed25519.PrivateKey)
Sign produces a complaint signature using the provided Ed25519 private key.
type ComplaintReason ¶
type ComplaintReason uint8
ComplaintReason enumerates the failure modes that justify a complaint.
const ( // ComplaintBadDelivery — share_{i→j} fails the commitment check. ComplaintBadDelivery ComplaintReason = 1 // ComplaintEquivocation — sender i shipped different commits to // different recipients (detected via Round 1.5 digest cross-check). ComplaintEquivocation ComplaintReason = 2 // ComplaintMissing — sender i failed to deliver share_{i→j} by // the round-1 deadline. ComplaintMissing ComplaintReason = 3 // ComplaintMalformedCommit — sender i's commit vector has the // wrong length, has nil entries, or fails internal sanity checks. ComplaintMalformedCommit ComplaintReason = 4 )
func (ComplaintReason) String ¶
func (r ComplaintReason) String() string
String returns a human-readable name for the reason.
type PairwiseKeyMaterial ¶
PairwiseKeyMaterial holds the X25519 keys + signed ephemerals that together produce auth_kex_ij. One instance per pair (canonicalized by the smaller party ID first).
type ReshareTranscript ¶
type ReshareTranscript struct {
}
ReshareTranscript is the structured digest of the resharing exchange.
type TranscriptInputs ¶
type TranscriptInputs struct {
ChainID []byte
NetworkID []byte
GroupID []byte
KeyEraID uint64
OldGeneration uint64
NewGeneration uint64
OldEpochID uint64
NewEpochID uint64
OldSetHash [32]byte
NewSetHash [32]byte
ThresholdOld uint32
ThresholdNew uint32
GroupPublicKeyHash [32]byte
NebulaRoot [32]byte
HashSuiteID string
ImplementationVersion string
Variant string // "refresh" or "reshare"
CurveName string // "ed25519" / "secp256k1" / "ristretto255"
}
TranscriptInputs holds the public binding fields for one resharing invocation. All fields are mandatory; the transcript hash is well- defined only when every byte below is fixed.