mirror of
https://github.com/luxfi/corona.git
synced 2026-07-27 02:50:34 +00:00
The Pedersen-VSS DKG share-dealing now runs entirely on github.com/luxfi/dkg
(proven byte-identical at cutover); corona's own copy is dead. Remove it and its
now-orphaned tooling, keeping only corona-specific code (Ringtail signing, β
rounding, Path (a) finalize — all in sign/, utils/, keyera/, untouched).
Deleted:
- dkg2/ (the whole package: Round1/Round2/commits/complaint/GPU dispatch).
- cmd/dkg2_oracle/ (the dkg2 KAT generator; the DKG KAT now lives in
luxfi/dkg's own ring/vss vectors).
- keyera/cutover_gate_test.go (the transitional dkg2-vs-vss equality gate; its
job is done — permanent byte-stability is pinned by the golden KAT, and the
PIN-4 convention assertion is preserved dkg2-free in pin4_convention_test.go).
Updated:
- cmd/cross_runtime_oracle drops the dkg2 manifest leg (the luxcpp C++ dkg2
cross-runtime gate migrates to luxfi/dkg separately — flagged, not silently
broken).
- keyera/reanchor doc comments + threshold bench comment point at luxfi/dkg.
go build ./... + go test ./... -count=1 green (13 ok / 0 fail), go vet + gofmt
clean. The golden byte-stability KAT + Bootstrap→sign→verify + dishonest-dealer
abort all still pass on the vss path.
587 lines
22 KiB
Go
587 lines
22 KiB
Go
// Copyright (C) 2025-2026, Lux Industries Inc. All rights reserved.
|
|
// See the file LICENSE for licensing terms.
|
|
|
|
// Package keyera is the lifecycle wrapper for a Corona group lineage.
|
|
//
|
|
// One KeyEra is opened by Bootstrap (legacy trusted dealer) or by
|
|
// BootstrapPedersen (public-BFT-safe, recommended). Subsequent
|
|
// validator-set rotations call Reshare, which preserves the GroupKey
|
|
// (A, bTilde) and rotates only the share distribution; no trusted
|
|
// dealer is needed for resharing. Reanchor opens a new era with a
|
|
// fresh GroupKey for security-event response (rare, governance-gated)
|
|
// and routes through Pedersen-DKG by default. ReanchorTrustedDealer
|
|
// is retained as an explicit opt-in alias for HSM/TEE ceremonies.
|
|
//
|
|
// The single source of truth for the lifecycle is
|
|
// `~/work/lux/pulsar/DESIGN.md`.
|
|
//
|
|
// Invariants (enforced loudly):
|
|
//
|
|
// BLS lane: each validator has its OWN keypair.
|
|
// ML-DSA lane: each validator has its OWN keypair.
|
|
// Corona lane: each validator has a SHARE of one group key.
|
|
//
|
|
// Within a key era:
|
|
//
|
|
// - The same hidden signing secret s is preserved across epochs.
|
|
// - The same public matrix A is preserved.
|
|
// - The same public key bTilde is preserved (the GroupKey is byte-
|
|
// identical across resharing).
|
|
// - The error e is NOT reshared. It was used only at Bootstrap to
|
|
// form the LWE public key; dealer state is erased.
|
|
// - Only the share distribution of s rotates per epoch.
|
|
//
|
|
// Across key eras (Reanchor): A, s, e, bTilde all change.
|
|
package keyera
|
|
|
|
import (
|
|
"crypto/rand"
|
|
"errors"
|
|
"fmt"
|
|
"io"
|
|
"math/big"
|
|
|
|
"github.com/luxfi/corona/hash"
|
|
"github.com/luxfi/corona/primitives"
|
|
"github.com/luxfi/corona/reshare"
|
|
"github.com/luxfi/corona/sign"
|
|
"github.com/luxfi/corona/threshold"
|
|
"github.com/luxfi/corona/utils"
|
|
|
|
"github.com/luxfi/lattice/v7/ring"
|
|
"github.com/luxfi/lattice/v7/utils/sampling"
|
|
"github.com/luxfi/lattice/v7/utils/structs"
|
|
)
|
|
|
|
// Errors returned by the package.
|
|
var (
|
|
ErrUninitialized = errors.New("keyera: era is uninitialized")
|
|
ErrInvalidThreshold = errors.New("keyera: threshold must satisfy 1 <= t <= n")
|
|
ErrEmptyValidators = errors.New("keyera: validator set is empty")
|
|
ErrMissingShare = errors.New("keyera: share missing for validator")
|
|
)
|
|
|
|
// CoronaKeyEraID is a monotonically increasing identifier for a key era.
|
|
// Bumped only on Reanchor (rare governance event). All resharings
|
|
// within an era keep the same era ID.
|
|
type CoronaKeyEraID uint64
|
|
|
|
// CoronaGroupID identifies one Corona group for grouped Quasar setups
|
|
// where validator sets are partitioned into smaller groups, each with
|
|
// its own GroupKey lineage. For the single-group case it is zero.
|
|
type CoronaGroupID uint64
|
|
|
|
// KeyEra is one Corona group lineage. The GroupKey (A, bTilde) is set at
|
|
// Bootstrap and persists across every Reshare within the era. State is
|
|
// the current epoch's share distribution; it rotates each Reshare.
|
|
//
|
|
// HashSuiteID pins the hash profile this era was opened under. It is
|
|
// recorded at Bootstrap and remains immutable through every Reshare in
|
|
// the era (per pulsar/proofs/hash-suite-separation.tex Remark on
|
|
// era-pinning). Reanchor MAY change the suite for the new era. The
|
|
// field is read-only after Bootstrap returns; Reshare propagates it
|
|
// without parameterisation.
|
|
type KeyEra struct {
|
|
EraID CoronaKeyEraID
|
|
GroupID CoronaGroupID
|
|
GroupKey *threshold.GroupKey
|
|
GenesisEpoch uint64
|
|
HashSuiteID string
|
|
State *EpochShareState
|
|
}
|
|
|
|
// EpochShareState is the per-epoch share distribution for a key era.
|
|
// Replaces the legacy "EpochKeys" naming — distinguishes "share
|
|
// rotation" from "key rotation".
|
|
//
|
|
// Three lineage fields, kept distinct (do not collapse — they mean
|
|
// different things):
|
|
//
|
|
// - KeyEraID: Corona group-key lineage. Bumps only at Reanchor (fresh
|
|
// GroupKey).
|
|
// - Generation: LSS resharing version within this key era. Bumps
|
|
// every Refresh / Reshare under the same GroupKey. Aligns with
|
|
// LSS's Generation field; managed by threshold/protocols/lss when
|
|
// this state is driven through the LSS-Corona adapter.
|
|
// - RollbackFrom: nonzero only when this state descends from a
|
|
// Rollback (= the prior Generation that was reverted from). Zero
|
|
// on ordinary forward transitions.
|
|
//
|
|
// In production, threshold/protocols/lss owns Generation/RollbackFrom
|
|
// updates. In the in-process keyera reference path (used for tests +
|
|
// KAT replay), keyera updates them itself.
|
|
type EpochShareState struct {
|
|
// Lineage (changes only at Reanchor — fresh GroupKey).
|
|
KeyEraID uint64
|
|
|
|
// HashSuiteID is the pinned hash profile for this share state.
|
|
// Mirrored from the parent KeyEra at Bootstrap and propagated
|
|
// without modification through every Reshare. NEVER set this
|
|
// field on a state that descends from a Reshare; use the parent
|
|
// era's HashSuiteID. Reanchor opens a NEW era with a fresh value.
|
|
HashSuiteID string
|
|
|
|
// LSS lifecycle.
|
|
Generation uint64
|
|
RollbackFrom uint64
|
|
|
|
// Per-epoch state (rotates every Refresh / Reshare).
|
|
Epoch uint64
|
|
Validators []string
|
|
Threshold int
|
|
Shares map[string]*threshold.KeyShare
|
|
}
|
|
|
|
// Bootstrap opens a new key era WITHOUT a trusted dealer.
|
|
//
|
|
// TRUST MODEL — PUBLIC-BFT-SAFE (default).
|
|
// This function routes the keygen ceremony through the shared
|
|
// github.com/luxfi/dkg Pedersen-VSS (vss, parameterized by
|
|
// ring.Ringtail()) + corona's Path (a) noise flooding so no single
|
|
// party ever holds the master secret s at any point in the ceremony.
|
|
// Every validator runs the vss Round 1 independently; the per-party
|
|
// noise contributions aggregate into a Corona-Sign-shaped public key
|
|
// `bTilde = Round_Xi(A·s + e”)` where each party adds one Gaussian
|
|
// `e_j'` slice under σ” = κ·σ_E·√n. Identifiable abort: a
|
|
// misbehaving sender is named in a signed luxfi/dkg blame complaint
|
|
// carrying re-checkable Pedersen evidence; the chain commits the abort
|
|
// transcript and stays at the previous epoch.
|
|
//
|
|
// On success returns (era, transcript, nil). The transcript is the
|
|
// public, byte-stable record of the ceremony; honest validators that
|
|
// observe the same cohort messages compute identical bytes. The
|
|
// consensus layer commits to `transcript.TranscriptHash` to ratify
|
|
// the era. On identifiable abort returns (nil, nil, err wrapping
|
|
// ErrBootstrapPedersenAbort); extract via ExtractAbortEvidence.
|
|
//
|
|
// LEGACY ALTERNATIVE: use BootstrapTrustedDealer for HSM/TEE
|
|
// ceremonies where the operator explicitly chooses a non-distributed
|
|
// trust root (publicly observable HSM-bound ceremony with commit-
|
|
// and-reveal entropy from genesis validators, dealer state zeroed
|
|
// before ceremony close). The dealer's host platform is in the trust
|
|
// boundary for the duration of the call. See DEPLOYMENT-RUNBOOK.md
|
|
// §Bootstrap-Trust for the trust-model trade-off documentation.
|
|
//
|
|
// Use crypto/rand.Reader for the kernel's randomness when no specific
|
|
// ceremony source is provided. Tests pass a deterministic source for
|
|
// KAT replay.
|
|
//
|
|
// Bootstrap pins the production HashSuite (Corona-SHA3). Use
|
|
// BootstrapWithSuite to open an era under the legacy Corona-BLAKE3
|
|
// profile (for cross-suite KAT replay only — NOT for production).
|
|
func Bootstrap(t int, validators []string, groupID CoronaGroupID, eraID CoronaKeyEraID, entropy io.Reader) (*KeyEra, *BootstrapTranscript, error) {
|
|
return BootstrapPedersen(hash.Default(), t, validators, groupID, eraID, entropy)
|
|
}
|
|
|
|
// BootstrapWithSuite is the suite-explicit form of Bootstrap. It
|
|
// routes through BootstrapPedersen (public-BFT-safe, no trusted
|
|
// dealer) with the supplied HashSuite; pass nil to use the
|
|
// production default (Corona-SHA3). See the Bootstrap docstring for
|
|
// the trust-model disclosure.
|
|
//
|
|
// LEGACY ALTERNATIVE: use BootstrapTrustedDealerWithSuite for HSM/TEE
|
|
// ceremony scenarios where a non-distributed trust root is acceptable
|
|
// by policy.
|
|
func BootstrapWithSuite(suite hash.HashSuite, t int, validators []string, groupID CoronaGroupID, eraID CoronaKeyEraID, entropy io.Reader) (*KeyEra, *BootstrapTranscript, error) {
|
|
return BootstrapPedersen(suite, t, validators, groupID, eraID, entropy)
|
|
}
|
|
|
|
// bootstrapTrustedDealerImpl is the historical trusted-dealer
|
|
// implementation, retained as the body of BootstrapTrustedDealer /
|
|
// BootstrapTrustedDealerWithSuite. Single source of truth so the
|
|
// public-facing aliases cannot drift from each other. NO production
|
|
// code path should call this directly; use BootstrapTrustedDealer
|
|
// instead so the name signals the trust-model decision at the call
|
|
// site.
|
|
func bootstrapTrustedDealerImpl(suite hash.HashSuite, t int, validators []string, groupID CoronaGroupID, eraID CoronaKeyEraID, entropy io.Reader) (*KeyEra, error) {
|
|
if len(validators) == 0 {
|
|
return nil, ErrEmptyValidators
|
|
}
|
|
n := len(validators)
|
|
if t < 1 || t > n {
|
|
return nil, fmt.Errorf("%w: t=%d n=%d", ErrInvalidThreshold, t, n)
|
|
}
|
|
if entropy == nil {
|
|
entropy = rand.Reader
|
|
}
|
|
suite = hash.Resolve(suite)
|
|
suiteID := suite.ID()
|
|
|
|
gk, sStd, err := genGroupKey(entropy)
|
|
if err != nil {
|
|
return nil, fmt.Errorf("keyera: bootstrap genGroupKey: %w", err)
|
|
}
|
|
|
|
r := gk.Params.R
|
|
skShares := primitives.ShamirSecretSharingGeneral(r, []ring.Poly(sStd), t, n)
|
|
for _, sh := range skShares {
|
|
utils.ConvertVectorToNTT(r, sh)
|
|
}
|
|
|
|
seeds, macKeysByParty := derivePairwiseMaterial(n, entropy)
|
|
lagrange := computeFullCommitteeLagrange(r, n)
|
|
|
|
state := &EpochShareState{
|
|
KeyEraID: uint64(eraID),
|
|
HashSuiteID: suiteID,
|
|
Generation: 0,
|
|
RollbackFrom: 0,
|
|
Epoch: 0,
|
|
Validators: append([]string(nil), validators...),
|
|
Threshold: t,
|
|
Shares: make(map[string]*threshold.KeyShare, n),
|
|
}
|
|
for i, v := range validators {
|
|
lambda := r.NewPoly()
|
|
lambda.Copy(lagrange[i])
|
|
r.NTT(lambda, lambda)
|
|
r.MForm(lambda, lambda)
|
|
state.Shares[v] = &threshold.KeyShare{
|
|
Index: i,
|
|
SkShare: skShares[i],
|
|
Seeds: seeds,
|
|
MACKeys: macKeysByParty[i],
|
|
Lambda: lambda,
|
|
GroupKey: gk,
|
|
}
|
|
}
|
|
|
|
// The dealer's master secret is no longer needed. Zero the standard-
|
|
// form copy so it is at least overwritten in this stack frame; the
|
|
// NTT-Mont copy was destroyed by ConvertVectorToNTT in place.
|
|
for i := range sStd {
|
|
for k := range sStd[i].Coeffs {
|
|
coeffs := sStd[i].Coeffs[k]
|
|
for j := range coeffs {
|
|
coeffs[j] = 0
|
|
}
|
|
}
|
|
}
|
|
|
|
return &KeyEra{
|
|
EraID: eraID,
|
|
GroupID: groupID,
|
|
GroupKey: gk,
|
|
GenesisEpoch: 0,
|
|
HashSuiteID: suiteID,
|
|
State: state,
|
|
}, nil
|
|
}
|
|
|
|
// Reshare evolves the era to a new committee while preserving GroupKey.
|
|
//
|
|
// The bare Shamir kernel runs in-process; for distributed deployments
|
|
// the consensus layer wraps this in the full Verifiable Secret Resharing
|
|
// (VSR) exchange (commits, complaints, activation cert) defined in
|
|
// corona/reshare. This kernel exists to (a) drive the cryptographic core,
|
|
// (b) be reused as the trusted-collaborator path for single-process
|
|
// integration tests, and (c) provide a reference against which the
|
|
// distributed protocol can be byte-equality checked.
|
|
//
|
|
// rand defaults to crypto/rand.Reader. Pass a deterministic source for
|
|
// KAT replay.
|
|
func (era *KeyEra) Reshare(newValidators []string, newThreshold int, randSource io.Reader) (*EpochShareState, error) {
|
|
if era == nil || era.GroupKey == nil || era.State == nil {
|
|
return nil, ErrUninitialized
|
|
}
|
|
if len(newValidators) == 0 {
|
|
return nil, ErrEmptyValidators
|
|
}
|
|
K := len(newValidators)
|
|
if newThreshold < 1 || newThreshold > K {
|
|
return nil, fmt.Errorf("%w: t=%d n=%d", ErrInvalidThreshold, newThreshold, K)
|
|
}
|
|
if randSource == nil {
|
|
randSource = rand.Reader
|
|
}
|
|
|
|
r := era.GroupKey.Params.R
|
|
|
|
oldSharesByID := make(map[int]reshare.Share, len(era.State.Shares))
|
|
for i, v := range era.State.Validators {
|
|
ks, ok := era.State.Shares[v]
|
|
if !ok {
|
|
return nil, fmt.Errorf("%w: %s", ErrMissingShare, v)
|
|
}
|
|
stdShare := cloneVectorAsStandard(r, ks.SkShare)
|
|
oldSharesByID[i+1] = reshare.Share(stdShare)
|
|
}
|
|
|
|
newCommitteeIDs := make([]int, K)
|
|
for i := range newValidators {
|
|
newCommitteeIDs[i] = i + 1
|
|
}
|
|
|
|
newSharesByID, err := reshare.Reshare(
|
|
r,
|
|
oldSharesByID,
|
|
era.State.Threshold,
|
|
newCommitteeIDs,
|
|
newThreshold,
|
|
randSource,
|
|
)
|
|
if err != nil {
|
|
return nil, fmt.Errorf("keyera: reshare kernel: %w", err)
|
|
}
|
|
|
|
lagrange := computeFullCommitteeLagrange(r, K)
|
|
seeds, macKeys := derivePairwiseMaterial(K, randSource)
|
|
|
|
nextState := &EpochShareState{
|
|
// Lineage preserved across resharing (KeyEraID never changes
|
|
// inside an era).
|
|
KeyEraID: era.State.KeyEraID,
|
|
// HashSuiteID is era-pinned; propagated, NOT a parameter.
|
|
// Reshare cannot change the suite — see hash-suite-separation
|
|
// theorem (proofs/pulsar/hash-suite-separation.tex).
|
|
HashSuiteID: era.HashSuiteID,
|
|
// LSS Generation increments by 1 on every successful Reshare.
|
|
Generation: era.State.Generation + 1,
|
|
// Ordinary forward transition; not a Rollback.
|
|
RollbackFrom: 0,
|
|
Epoch: era.State.Epoch + 1,
|
|
Validators: append([]string(nil), newValidators...),
|
|
Threshold: newThreshold,
|
|
Shares: make(map[string]*threshold.KeyShare, K),
|
|
}
|
|
for idx, v := range newValidators {
|
|
partyID := idx + 1
|
|
skShareNTT := stdShareToNTT(r, newSharesByID[partyID])
|
|
lambda := r.NewPoly()
|
|
lambda.Copy(lagrange[idx])
|
|
r.NTT(lambda, lambda)
|
|
r.MForm(lambda, lambda)
|
|
nextState.Shares[v] = &threshold.KeyShare{
|
|
Index: idx,
|
|
SkShare: skShareNTT,
|
|
Seeds: seeds,
|
|
MACKeys: macKeys[idx],
|
|
Lambda: lambda,
|
|
GroupKey: era.GroupKey,
|
|
}
|
|
}
|
|
|
|
era.State = nextState
|
|
return nextState, nil
|
|
}
|
|
|
|
// Reanchor opens a new key era with a fresh GroupKey via Pedersen-DKG
|
|
// + Path (a) noise flooding. Use ONLY for security-event response —
|
|
// long-tail share leakage, suspected master-secret compromise, etc.
|
|
// The chain governance MUST authorize this; it is not a routine
|
|
// operation. For ordinary validator-set rotation use Reshare, which
|
|
// preserves the GroupKey.
|
|
//
|
|
// Reanchor inherits the prior era's HashSuiteID. To migrate to a
|
|
// different suite (e.g. moving from legacy Corona-BLAKE3 to production
|
|
// Corona-SHA3) call ReanchorWithSuite.
|
|
//
|
|
// TRUST MODEL — PUBLIC-BFT-SAFE.
|
|
// This function routes through the shared github.com/luxfi/dkg
|
|
// Pedersen-VSS (vss) so no party ever holds the master secret s for
|
|
// the new era at any point in the rotation. The dealer that previously
|
|
// held s for the prior era is NOT re-trusted; every validator runs the
|
|
// vss Round 1 independently and Path (a) noise flooding aggregates the
|
|
// result.
|
|
//
|
|
// LEGACY ALTERNATIVE: use ReanchorTrustedDealer, which runs the
|
|
// single-dealer Shamir share-out (byte-equivalent to the v0.7.3 and
|
|
// earlier Reanchor behavior). Retained for HSM / TEE / publicly-
|
|
// observable foundation ceremonies where a non-distributed trust root
|
|
// is acceptable by policy. See DEPLOYMENT-RUNBOOK.md §Bootstrap-Trust.
|
|
//
|
|
// Returns the new *KeyEra (EraID = prev.EraID+1, GenesisEpoch =
|
|
// prev.State.Epoch+1), the public *BootstrapTranscript for chain
|
|
// commit, and nil on success. On identifiable abort returns
|
|
// (nil, nil, err wrapping ErrBootstrapPedersenAbort); extract via
|
|
// ExtractAbortEvidence.
|
|
func Reanchor(prev *KeyEra, t int, validators []string, groupID CoronaGroupID, entropy io.Reader) (*KeyEra, *BootstrapTranscript, error) {
|
|
return ReanchorPedersen(prev, t, validators, groupID, entropy)
|
|
}
|
|
|
|
// ReanchorWithSuite opens a new key era with a fresh GroupKey under
|
|
// the supplied HashSuite via Pedersen-DKG + Path (a) noise flooding.
|
|
// Reanchor is the ONLY lifecycle entrypoint that may pin a hash
|
|
// profile different from the prior era's (Reshare cannot — that is
|
|
// enforced by Reshare not accepting a suite parameter). nil suite
|
|
// resolves to the production default (Corona-SHA3).
|
|
//
|
|
// See Reanchor for the trust-model disclosure; ReanchorWithSuite is
|
|
// the suite-explicit form and shares the same public-BFT-safe routing.
|
|
func ReanchorWithSuite(prev *KeyEra, suite hash.HashSuite, t int, validators []string, groupID CoronaGroupID, entropy io.Reader) (*KeyEra, *BootstrapTranscript, error) {
|
|
return ReanchorPedersenWithSuite(prev, suite, t, validators, groupID, entropy)
|
|
}
|
|
|
|
// ReanchorTrustedDealer is the legacy single-trusted-party reanchor
|
|
// retained for ceremony scenarios where a non-distributed trust root
|
|
// is acceptable (e.g. publicly observable HSM / TEE ceremony with
|
|
// commit-and-reveal entropy from the new committee). It is byte-
|
|
// equivalent to the historical Reanchor entrypoint (v0.7.3 and earlier).
|
|
//
|
|
// PREFER Reanchor (which routes through Pedersen-DKG) for any
|
|
// deployment where no single party is trusted to discard the new
|
|
// era's master secret. See DEPLOYMENT-RUNBOOK.md §Bootstrap-Trust
|
|
// for the trust-model trade-off documentation.
|
|
func ReanchorTrustedDealer(prev *KeyEra, t int, validators []string, groupID CoronaGroupID, entropy io.Reader) (*KeyEra, error) {
|
|
var suite hash.HashSuite
|
|
if prev != nil && prev.HashSuiteID == hash.LegacyBLAKE3ID {
|
|
suite = hash.NewCoronaBLAKE3()
|
|
} else {
|
|
suite = hash.Default()
|
|
}
|
|
return ReanchorTrustedDealerWithSuite(prev, suite, t, validators, groupID, entropy)
|
|
}
|
|
|
|
// ReanchorTrustedDealerWithSuite is the suite-explicit form of
|
|
// ReanchorTrustedDealer. See its docstring for the trust-model
|
|
// disclosure.
|
|
func ReanchorTrustedDealerWithSuite(prev *KeyEra, suite hash.HashSuite, t int, validators []string, groupID CoronaGroupID, entropy io.Reader) (*KeyEra, error) {
|
|
var nextEraID CoronaKeyEraID
|
|
var nextEpoch uint64
|
|
if prev != nil {
|
|
nextEraID = prev.EraID + 1
|
|
if prev.State != nil {
|
|
nextEpoch = prev.State.Epoch + 1
|
|
}
|
|
}
|
|
next, err := bootstrapTrustedDealerImpl(suite, t, validators, groupID, nextEraID, entropy)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
next.GenesisEpoch = nextEpoch
|
|
next.State.Epoch = nextEpoch
|
|
return next, nil
|
|
}
|
|
|
|
// genGroupKey samples (A, s, e), forms b = A*s + e, rounds to bTilde,
|
|
// and returns a populated *threshold.GroupKey along with the master
|
|
// secret s in standard (non-NTT) form. The caller is responsible for
|
|
// either sharing s and erasing it (Bootstrap) or zeroing it (Reanchor).
|
|
func genGroupKey(entropy io.Reader) (*threshold.GroupKey, structs.Vector[ring.Poly], error) {
|
|
params, err := threshold.NewParams()
|
|
if err != nil {
|
|
return nil, nil, err
|
|
}
|
|
r := params.R
|
|
rXi := params.RXi
|
|
|
|
seed := make([]byte, sign.KeySize)
|
|
if _, err := io.ReadFull(entropy, seed); err != nil {
|
|
return nil, nil, err
|
|
}
|
|
prng, err := sampling.NewKeyedPRNG(seed)
|
|
if err != nil {
|
|
return nil, nil, err
|
|
}
|
|
uniformSampler := ring.NewUniformSampler(prng, r)
|
|
gaussianParams := ring.DiscreteGaussian{Sigma: sign.SigmaE, Bound: sign.BoundE}
|
|
gaussianSampler := ring.NewGaussianSampler(prng, r, gaussianParams, false)
|
|
|
|
A := utils.SamplePolyMatrix(r, sign.M, sign.N, uniformSampler, true, true)
|
|
sStd := utils.SamplePolyVector(r, sign.N, gaussianSampler, false, false)
|
|
|
|
// Compute b = A*s + e. We need NTT-Mont s for the matrix multiply,
|
|
// but we want to keep a standard-form copy of s for the Shamir
|
|
// sharing step.
|
|
sNTT := make(structs.Vector[ring.Poly], len(sStd))
|
|
for i := range sStd {
|
|
sNTT[i] = r.NewPoly()
|
|
sNTT[i].Copy(sStd[i])
|
|
}
|
|
utils.ConvertVectorToNTT(r, sNTT)
|
|
|
|
e := utils.SamplePolyVector(r, sign.M, gaussianSampler, true, true)
|
|
b := utils.InitializeVector(r, sign.M)
|
|
utils.MatrixVectorMul(r, A, sNTT, b)
|
|
utils.VectorAdd(r, b, e, b)
|
|
utils.ConvertVectorFromNTT(r, b)
|
|
bTilde := utils.RoundVector(r, rXi, b, sign.Xi)
|
|
|
|
// Erase the dealer's e from this stack. Quasar never reshares e —
|
|
// it was only needed to form bTilde at genesis.
|
|
for i := range e {
|
|
for k := range e[i].Coeffs {
|
|
coeffs := e[i].Coeffs[k]
|
|
for j := range coeffs {
|
|
coeffs[j] = 0
|
|
}
|
|
}
|
|
}
|
|
|
|
return &threshold.GroupKey{
|
|
A: A,
|
|
BTilde: bTilde,
|
|
Params: params,
|
|
}, sStd, nil
|
|
}
|
|
|
|
// computeFullCommitteeLagrange returns Lagrange coefficients for the
|
|
// committee positions [0, 1, ..., n-1] (0-indexed evaluation points;
|
|
// primitives.ComputeLagrangeCoefficients adds 1 internally).
|
|
func computeFullCommitteeLagrange(r *ring.Ring, n int) []ring.Poly {
|
|
T := make([]int, n)
|
|
for i := range T {
|
|
T[i] = i
|
|
}
|
|
return primitives.ComputeLagrangeCoefficients(r, T, big.NewInt(int64(sign.Q)))
|
|
}
|
|
|
|
// derivePairwiseMaterial generates per-pair PRF seeds and MAC keys for
|
|
// a committee of size K.
|
|
//
|
|
// seeds[i][j] : sign.KeySize bytes, present for every (i, j).
|
|
// macKeys[i][j] : sign.KeySize bytes, present for i != j; symmetric.
|
|
//
|
|
// In a single-process simulation the material is freshly drawn from
|
|
// randSource. In a distributed deployment the consensus layer overrides
|
|
// this with authenticated pairwise KEX from corona/reshare/pairwise.go,
|
|
// ensuring both endpoints derive the same value without a shared
|
|
// trusted dealer.
|
|
func derivePairwiseMaterial(K int, randSource io.Reader) (map[int][][]byte, []map[int][]byte) {
|
|
seeds := make(map[int][][]byte, K)
|
|
macKeys := make([]map[int][]byte, K)
|
|
for i := 0; i < K; i++ {
|
|
seeds[i] = make([][]byte, K)
|
|
macKeys[i] = make(map[int][]byte, K-1)
|
|
}
|
|
for i := 0; i < K; i++ {
|
|
for j := 0; j < K; j++ {
|
|
buf := make([]byte, sign.KeySize)
|
|
_, _ = io.ReadFull(randSource, buf)
|
|
seeds[i][j] = buf
|
|
}
|
|
}
|
|
for i := 0; i < K; i++ {
|
|
for j := i + 1; j < K; j++ {
|
|
buf := make([]byte, sign.KeySize)
|
|
_, _ = io.ReadFull(randSource, buf)
|
|
macKeys[i][j] = buf
|
|
macKeys[j][i] = buf
|
|
}
|
|
}
|
|
return seeds, macKeys
|
|
}
|
|
|
|
// cloneVectorAsStandard copies an NTT-Mont vector into a fresh standard-
|
|
// form vector, leaving the input untouched. Used by Reshare to feed the
|
|
// reshare kernel without mutating the caller's KeyShare.
|
|
func cloneVectorAsStandard(r *ring.Ring, in structs.Vector[ring.Poly]) structs.Vector[ring.Poly] {
|
|
out := make(structs.Vector[ring.Poly], len(in))
|
|
for i := range in {
|
|
out[i] = r.NewPoly()
|
|
out[i].Copy(in[i])
|
|
r.IMForm(out[i], out[i])
|
|
r.INTT(out[i], out[i])
|
|
}
|
|
return out
|
|
}
|
|
|
|
// stdShareToNTT converts a standard-form share (Reshare output) into
|
|
// the NTT-Mont form required by sign.Party. The input is consumed; the
|
|
// caller must not reuse the standard-form vector after this returns.
|
|
func stdShareToNTT(r *ring.Ring, in reshare.Share) structs.Vector[ring.Poly] {
|
|
out := structs.Vector[ring.Poly](in)
|
|
utils.ConvertVectorToNTT(r, out)
|
|
return out
|
|
}
|