feat(keyera): drive the dealerless DKG through luxfi/dkg vss

Rewire BootstrapPedersen's no-reconstruct share-dealing onto the shared
github.com/luxfi/dkg library (vss + blame + ring.Ringtail()), dropping the
in-keyera dependence on corona/dkg2. corona keeps ONLY its scheme-specific
finalize: Path (a) noise flooding + Round_Xi, which turns the per-party secret
shares s_j into the LWE-shaped Ringtail public key bTilde = Round_Xi(A·s+e'').
The vss Pedersen aggregate T = A·s1+B·u is NOT corona's group key (its B·u term
is not a small LWE error), so vss is used purely for the no-reconstruct share
distribution + identifiable blame.

Mechanism:
  - BootstrapPedersen draws per-party sampling seeds from the ceremony entropy
    (same sequencing as before) and threads each into vss.Party.Round1, so the
    χ draws — hence the Pedersen commits and the secret shares — are byte-
    identical to the prior dkg2 path (cutover gates B/C/E).
  - DealPedersen runs Round 1 + opens every sealed envelope; FinishBootstrapPedersen
    runs Round 3 (Pedersen verify + aggregate) then Path (a) + KeyShare assembly.
    The two-phase split is the test seam for identifiable abort (tamper an opened
    share, observe ErrBootstrapPedersenAbort + re-checkable blame).
  - Commit digests are recomputed under the corona HashSuite over the lattigo
    wire bytes of vss's commits — byte-identical to dkg2's CommitDigest — so the
    noise seed and transcript hash are preserved.
  - AbortEvidence now carries luxfi/dkg blame.Complaint (accused-by-NodeID,
    third-party re-checkable via vss.RecheckBadDelivery); the in-process channel
    identities are derived deterministically and affect only the (immediately-
    opened) envelope ciphertext, never any byte-stable output.

Proof: the frozen golden KAT (bTilde / transcript / shares digests for 5/3 and
3/2) PASSES against the vss path — the group key, transcript, and shares are
preserved byte-for-byte. Determinism, Bootstrap→sign→verify, dishonest-dealer
abort (+ RecheckBadDelivery), reanchor round-trip/abort, and the no-master-secret
structural checks all green. Public API (Bootstrap/KeyEra/EpochShareState/
BootstrapTranscript/GroupKey/KeyShare) unchanged.
This commit is contained in:
2026-06-27 21:55:51 -07:00
parent 1a52f8ddcc
commit 8dab6d3997
4 changed files with 445 additions and 292 deletions
+384 -222
View File
@@ -4,26 +4,48 @@
package keyera
import (
"bytes"
"crypto/rand"
"encoding/binary"
"errors"
"fmt"
"io"
"math"
"math/big"
"github.com/luxfi/corona/dkg2"
"github.com/luxfi/corona/hash"
"github.com/luxfi/corona/primitives"
"github.com/luxfi/corona/sign"
"github.com/luxfi/corona/threshold"
"github.com/luxfi/corona/utils"
"github.com/luxfi/lattice/v7/ring"
dkgblame "github.com/luxfi/dkg/blame"
dkgchannel "github.com/luxfi/dkg/channel"
dkgring "github.com/luxfi/dkg/ring"
dkgvss "github.com/luxfi/dkg/vss"
lring "github.com/luxfi/lattice/v7/ring"
"github.com/luxfi/lattice/v7/utils/sampling"
"github.com/luxfi/lattice/v7/utils/structs"
"github.com/zeebo/blake3"
)
// bootstrap_pedersen.go — dealerless key-era bootstrap.
//
// The no-reconstruct Pedersen-VSS share-dealing (commit & deal, equivocation
// gate, Pedersen verification, identifiable abort) is provided by the shared
// github.com/luxfi/dkg library, parameterized by ring.Ringtail(). corona keeps
// ONLY its scheme-specific finalize: Path (a) noise flooding + Round_Xi, which
// turns the per-party secret shares s_j into the LWE-shaped Ringtail public key
// bTilde = Round_Xi(A·s + e″). The Pedersen aggregate T = A·s1+B·u that vss
// produces is NOT used as the group key (its B·u term is not a small LWE error);
// vss is used purely for the no-reconstruct share distribution + blame.
//
// Byte-stability (golden_bootstrap_kat_test.go): the per-party sampling seed is
// threaded into vss's Round1 so the χ draws — hence the Pedersen commits and the
// secret shares — are byte-identical to the prior dkg2 path; the commit digests
// are recomputed under the corona HashSuite over the same commit bytes. So the
// group key, shares, and transcript are PRESERVED, not re-baselined.
// BootstrapTranscript is the public, byte-stable record produced by a
// BootstrapPedersen run. Every honest validator that observes the same
// cohort messages computes identical transcript bytes; the chain commits
@@ -59,9 +81,14 @@ type BootstrapTranscript struct {
// AbortEvidence captures the signed Complaint set that aborted a
// BootstrapPedersen run. The chain stays at the previous era and the
// returned error wraps ErrBootstrapPedersenAbort.
//
// Complaints are the shared library's scheme-agnostic blame records (a bad
// share is named by the accused validator's NodeID, re-checkable by any third
// party via vss.RecheckBadDelivery). Disqualified maps the committee index of
// each named dealer.
type AbortEvidence struct {
TranscriptHash [32]byte
Complaints []*dkg2.Complaint
Complaints []*dkgblame.Complaint
Disqualified map[int]struct{}
}
@@ -72,7 +99,7 @@ var (
ErrBootstrapPedersenAbort = errors.New("keyera: bootstrap-pedersen aborted with identifiable evidence")
// ErrBootstrapPedersenShape is returned when input parameters violate
// the (1 ≤ t ≤ n) and (n ≥ 2) constraints required by dkg2.
// the (1 ≤ t ≤ n) and (n ≥ 2) constraints required by the DKG.
ErrBootstrapPedersenShape = errors.New("keyera: bootstrap-pedersen parameter shape")
)
@@ -88,57 +115,88 @@ const noiseFloodTag = "CORONA-BOOTSTRAP-PEDERSEN-NOISEFLOOD-v1"
// transcript so two suites can never collide on a single era.
const transcriptTag = "CORONA-BOOTSTRAP-PEDERSEN-TRANSCRIPT-v1"
// PedersenContributions is the raw output of all parties' Round 1 plus
// the per-recipient share/blind tables, suitable for direct injection
// into the FinishBootstrapPedersen orchestrator. Tests use this to
// simulate identifiable-abort scenarios where a single party is
// dishonest: they tamper one entry and then call FinishBootstrapPedersen
// to drive the abort path.
// tagCommitDigest is the customization tag bound into the Round 1.5 commit
// digest under the active HashSuite. The suite ID is bound into the digest
// input as well so two suites can never produce a colliding digest for the
// same commit vector. The string is the historical corona/dkg2 tag, kept
// verbatim so the digest — hence the noise seed and the transcript hash —
// is byte-identical across the move to luxfi/dkg.
const tagCommitDigest = "CORONA-DKG2-COMMIT-DIGEST-v1"
// identityDomainTag derives the per-party long-term DKG channel identities
// deterministically. The identities authenticate the sealed share envelopes;
// they do NOT influence the public commits, the secret shares, or the group
// key (those are functions of the per-party sampling seed only), so a fixed
// derivation keeps the whole in-process run replayable without perturbing any
// byte-stable output.
const identityDomainTag = "CORONA-BOOTSTRAP-PEDERSEN-IDENTITY-v1"
// kemStreamTag derives the per-party KEM-encapsulation randomness stream that
// vss.Round1 consumes after the 32-byte sampling seed. Like the identities,
// this affects only the (immediately-opened, then-discarded) envelope
// ciphertext, never any byte-stable output.
const kemStreamTag = "CORONA-BOOTSTRAP-PEDERSEN-KEMSTREAM-v1"
// pedersenSession bundles the in-process vss machinery for one bootstrap: the
// Ringtail profile, the n party sessions, their deterministic identities, the
// committee NodeIDs, and the era/committee context. corona drives every party
// itself (in-process reference path); production wraps each party over an
// authenticated network at the consensus layer.
type pedersenSession struct {
profile *dkgring.Profile
parties []*dkgvss.Party
ids []*dkgchannel.IdentityKey
nodes []dkgchannel.NodeID
context [32]byte
cr *lring.Ring // corona ring (== profile.Ring.Base()) for Path (a)
crXi *lring.Ring // post-rounding ring for Round_Xi
}
// PedersenContributions is the raw share-dealing output of all parties' Round 1
// plus, per recipient, the opened (share, blind, commits) it verifies in
// Round 3. It feeds FinishBootstrapPedersen. Tests use it to simulate an
// identifiable-abort: they tamper one opened share and call
// FinishBootstrapPedersen to drive the blame path.
//
// In production the per-party contributions arrive over the network and
// the consensus layer assembles them; the kernel exists for tests +
// integration suite.
// In production the per-party Round-1 broadcasts + sealed envelopes arrive over
// the network and the consensus layer assembles them; this kernel exists for
// tests + the single-process integration suite.
type PedersenContributions struct {
// Round1 outputs indexed by sender party ID 0..n-1.
Round1 []*dkg2.Round1Output
// dkg2 ring (matches Round1's sessions). The caller may build a fresh
// dkg2.NewParams() and pass its .R.
Sessions []*dkg2.DKGSession
session *pedersenSession
// commits[i] is dealer i's broadcast Pedersen commit vector (plain-NTT).
commits [][]dkgring.Vector
// digests[i] is the corona-HashSuite commit digest of dealer i's commits.
digests [][32]byte
// inputs[j][i] is recipient j's opened contribution from dealer i.
inputs []map[int]*dkgvss.DealerInput
}
// BootstrapPedersen opens a new key era WITHOUT a trusted dealer.
//
// The construction follows Path (a) of papers/lp-073-pulsar §07:
//
// 1. Each party runs dkg2.Round1 → broadcasts Pedersen commits, sends
// (share_{i→j}, blind_{i→j}) privately to every recipient j.
// 1. Each party runs the shared Pedersen-VSS Round 1 → broadcasts Pedersen
// commits, KEM-seals (share_{i→j}, blind_{i→j}) privately to recipient j.
// 2. Each party verifies all incoming Pedersen pairs (constant-time
// comparison; identifiable abort on mismatch).
// 3. Each party runs dkg2.Round2 → obtains its share s_j of the master
// secret s. NO PARTY ever holds s in memory.
// 4. Each party samples a fresh Gaussian e_j' ~ D(σ”) locally with
// σ” = κ · σ_E · √n (the slack reservation of LP-073 §5) and
// 3. Each party aggregates its share s_j of the master secret s. NO PARTY
// ever holds s in memory (no-reconstruct: vss never sums the contributions).
// 4. Each party samples a fresh Gaussian e_j' ~ D(σ″) locally with
// σ″ = κ · σ_E · √n (the slack reservation of LP-073 §5) and
// broadcasts β_j = A · NTT(λ_j · s_j) + e_j'. The published β_j is
// LWE-protected by e_j' so no party learns more than its own share.
// 5. All parties aggregate b = Σ_j β_j = A · s + e” in NTT-Mont; the
// 5. All parties aggregate b = Σ_j β_j = A · s + e″ in NTT-Mont; the
// Corona-Sign-shaped public key is bTilde = Round_Xi(b).
//
// On success returns:
// On success returns (era, transcript, nil). On identifiable abort returns
// (nil, nil, ErrBootstrapPedersenAbort) with the AbortEvidence wrapped into
// the error (extract via ExtractAbortEvidence).
//
// - era — fully populated *KeyEra with the noise-flooded GroupKey;
// - transcript — the public BootstrapTranscript suitable for chain commit;
// - nil error.
//
// On identifiable abort returns (nil, nil, ErrBootstrapPedersenAbort) and
// the AbortEvidence is wrapped into the returned error via errors.As.
//
// In production each party drives its own dkg2.DKGSession over an
// authenticated network. This in-process kernel drives every party
// itself; it exists to (a) be the trusted-collaborator path for the
// single-process integration tests and (b) provide a reference against
// which the distributed protocol can be byte-equality checked. The
// distributed wrapper at the consensus layer reuses every primitive
// imported here.
// In production each party drives its own vss session over an authenticated
// network. This in-process kernel drives every party itself; it exists to (a)
// be the trusted-collaborator path for the single-process integration tests
// and (b) provide a reference against which the distributed protocol can be
// byte-equality checked.
//
// Pass nil for the production suite default (Corona-SHA3).
func BootstrapPedersen(suite hash.HashSuite, t int, validators []string, groupID CoronaGroupID, eraID CoronaKeyEraID, entropy io.Reader) (*KeyEra, *BootstrapTranscript, error) {
@@ -149,14 +207,14 @@ func BootstrapPedersen(suite hash.HashSuite, t int, validators []string, groupID
if t < 1 || t > n {
return nil, nil, fmt.Errorf("%w: t=%d n=%d", ErrInvalidThreshold, t, n)
}
// dkg2 requires n ≥ 2 and 1 ≤ t < n (strictly less than for honest-
// abort detection). For t == n we fall back to the trusted-dealer
// path because dkg2 cannot guard a corruption-free quorum below 1.
// The Pedersen DKG requires n ≥ 2 and 1 ≤ t < n (strictly less than for
// honest-abort detection). For t == n callers must use the trusted-dealer
// path because the DKG cannot guard a corruption-free quorum below 1.
if n < 2 {
return nil, nil, fmt.Errorf("%w: n=%d (dkg2 requires n >= 2)", ErrBootstrapPedersenShape, n)
return nil, nil, fmt.Errorf("%w: n=%d (dkg requires n >= 2)", ErrBootstrapPedersenShape, n)
}
if t >= n {
return nil, nil, fmt.Errorf("%w: t=%d n=%d (dkg2 requires t < n)", ErrBootstrapPedersenShape, t, n)
return nil, nil, fmt.Errorf("%w: t=%d n=%d (dkg requires t < n)", ErrBootstrapPedersenShape, t, n)
}
if entropy == nil {
entropy = rand.Reader
@@ -164,24 +222,11 @@ func BootstrapPedersen(suite hash.HashSuite, t int, validators []string, groupID
suite = hash.Resolve(suite)
suiteID := suite.ID()
// Initialize dkg2 ring + matrices. The matrices A, B are
// nothing-up-my-sleeve; every party derives them deterministically.
dkgParams, err := dkg2.NewParams()
if err != nil {
return nil, nil, fmt.Errorf("keyera: bootstrap-pedersen dkg2 params: %w", err)
}
// Create one session per party. Each session needs an independent
// random seed for its Round1WithSeed input; we draw all of them up
// front so the entropy stream is sequenced canonically.
sessions := make([]*dkg2.DKGSession, n)
// Draw the per-party sampling seeds from the ceremony entropy, canonically
// sequenced (matches the prior dkg2 path's entropy consumption exactly, so
// the χ draws — hence commits, shares, and bTilde — are byte-stable).
round1Seeds := make([][]byte, n)
for i := 0; i < n; i++ {
sess, err := dkg2.NewDKGSession(dkgParams, i, n, t, suite)
if err != nil {
return nil, nil, fmt.Errorf("keyera: bootstrap-pedersen NewDKGSession[%d]: %w", i, err)
}
sessions[i] = sess
seed := make([]byte, sign.KeySize)
if _, err := io.ReadFull(entropy, seed); err != nil {
return nil, nil, fmt.Errorf("keyera: bootstrap-pedersen entropy[%d]: %w", i, err)
@@ -189,86 +234,171 @@ func BootstrapPedersen(suite hash.HashSuite, t int, validators []string, groupID
round1Seeds[i] = seed
}
// Round 1 — produce per-party Pedersen commits + shares/blinds.
round1 := make([]*dkg2.Round1Output, n)
contribs, err := dealPedersen(suite, t, n, round1Seeds)
if err != nil {
return nil, nil, err
}
return finishPedersenContribs(suite, suiteID, t, n, validators, groupID, eraID, contribs)
}
// newPedersenSession builds the in-process committee: the Ringtail profile, n
// deterministic channel identities, NodeIDs, the identity directory, the
// corona rings, and one vss.Party per committee position.
func newPedersenSession(t, n int) (*pedersenSession, error) {
profile, err := dkgring.Ringtail()
if err != nil {
return nil, fmt.Errorf("keyera: bootstrap-pedersen ring.Ringtail: %w", err)
}
crXi, _ := lring.NewRing(1<<sign.LogN, []uint64{sign.QXi}) // non-prime modulus; error deliberately ignored (RoundVector container only)
ids := make([]*dkgchannel.IdentityKey, n)
nodes := make([]dkgchannel.NodeID, n)
entries := make(map[dkgchannel.NodeID]*dkgchannel.IdentityPublicKey, n)
for i := 0; i < n; i++ {
out, err := sessions[i].Round1WithSeed(round1Seeds[i])
id, err := dkgchannel.GenerateIdentity(derivedStream(identityDomainTag, uint32(i)))
if err != nil {
return nil, nil, fmt.Errorf("keyera: bootstrap-pedersen Round1[%d]: %w", i, err)
return nil, fmt.Errorf("keyera: bootstrap-pedersen identity[%d]: %w", i, err)
}
ids[i] = id
nodes[i] = pedersenNodeID(i)
entries[nodes[i]] = id.PublicKey()
}
dir, err := dkgchannel.NewIdentityDirectory(entries)
if err != nil {
return nil, fmt.Errorf("keyera: bootstrap-pedersen directory: %w", err)
}
// The era/committee context binds every envelope to this ceremony. It is
// public and deterministic from (t, n); the consensus layer overrides it
// with a chain transcript root in the distributed deployment.
var context [32]byte
ctx := blake3.New()
_, _ = ctx.Write([]byte("CORONA-BOOTSTRAP-PEDERSEN-CONTEXT-v1"))
_, _ = ctx.Write(u32be(uint32(t)))
_, _ = ctx.Write(u32be(uint32(n)))
copy(context[:], ctx.Sum(nil))
parties := make([]*dkgvss.Party, n)
for i := 0; i < n; i++ {
p, err := dkgvss.NewParty(profile, nodes[i], ids[i], i, n, t, nodes, dir, context)
if err != nil {
return nil, fmt.Errorf("keyera: bootstrap-pedersen NewParty[%d]: %w", i, err)
}
parties[i] = p
}
return &pedersenSession{
profile: profile,
parties: parties,
ids: ids,
nodes: nodes,
context: context,
cr: profile.Ring.Base(),
crXi: crXi,
}, nil
}
// dealPedersen drives the shared Pedersen-VSS Round 1 for every party (with the
// per-party sampling seed threaded so the χ draws are byte-identical to the
// prior dkg2 path), computes the corona commit digests, then opens every sealed
// envelope so each recipient holds its (share, blind, commits) from each dealer.
// The honest-path Pedersen verification + aggregation happens in
// finishPedersenContribs (so a test can tamper an opened share first).
func dealPedersen(suite hash.HashSuite, t, n int, round1Seeds [][]byte) (*PedersenContributions, error) {
if len(round1Seeds) != n {
return nil, fmt.Errorf("%w: seeds=%d n=%d", ErrBootstrapPedersenShape, len(round1Seeds), n)
}
sess, err := newPedersenSession(t, n)
if err != nil {
return nil, err
}
// Round 1: each party samples χ, builds Pedersen commits, seals shares.
round1 := make([]*dkgvss.Round1Out, n)
for i := 0; i < n; i++ {
out, err := sess.parties[i].Round1(seededRound1Rng(round1Seeds[i], i))
if err != nil {
return nil, fmt.Errorf("keyera: bootstrap-pedersen Round1[%d]: %w", i, err)
}
round1[i] = out
}
return finishBootstrapPedersen(suite, suiteID, t, n, validators, groupID, eraID, dkgParams, sessions, round1)
}
// finishBootstrapPedersen drives Rounds 1.5/2 and the Path (a) noise-
// flooding on a pre-computed set of Round 1 outputs. Tests use this
// entrypoint (via FinishBootstrapPedersen) to inject deliberately
// dishonest contributions and exercise the identifiable-abort path.
//
// The in-process orchestrator owns the dkg2 sessions and the round1
// outputs; in production each party drives its own. The transcript bytes
// are independent of which side runs the loop because every input is
// deterministic given the round1 outputs and the validator list.
func finishBootstrapPedersen(suite hash.HashSuite, suiteID string, t, n int, validators []string, groupID CoronaGroupID, eraID CoronaKeyEraID, dkgParams *dkg2.Params, sessions []*dkg2.DKGSession, round1 []*dkg2.Round1Output) (*KeyEra, *BootstrapTranscript, error) {
r := dkgParams.R
// Round 1.5 — each recipient computes its sender digests under the
// active suite. In a distributed deployment each digest is broadcast
// and cross-checked. The kernel runs in-process so digests are
// deterministic and pinned into the transcript.
// Commit digests under the corona HashSuite (Round 1.5 equivocation gate +
// transcript binding). Computed over the lattigo wire bytes of each dealer's
// commits — byte-identical to the prior dkg2 CommitDigest.
commits := make([][]dkgring.Vector, n)
digests := make([][32]byte, n)
for i := 0; i < n; i++ {
d, err := round1[i].CommitDigest(suite)
commits[i] = round1[i].Commits
d, err := coronaCommitDigest(suite, round1[i].Commits)
if err != nil {
return nil, nil, fmt.Errorf("keyera: bootstrap-pedersen CommitDigest[%d]: %w", i, err)
return nil, fmt.Errorf("keyera: bootstrap-pedersen CommitDigest[%d]: %w", i, err)
}
digests[i] = d
}
// Round 2 — each recipient verifies every share/blind pair against
// the commits, then aggregates to its own (s_j, u_j, b_ped). On any
// identifiable abort we collect signed complaints and return.
//
// s shares per recipient (standard form, dimension sign.N).
sShares := make([]structs.Vector[ring.Poly], n)
// Open every sealed envelope: recipient j recovers (share, blind) from each
// dealer i. The opened values are byte-identical to the dealer's Horner
// evaluation f_i(j+1) / g_i(j+1).
inputs := make([]map[int]*dkgvss.DealerInput, n)
for j := 0; j < n; j++ {
shares := map[int]structs.Vector[ring.Poly]{}
blinds := map[int]structs.Vector[ring.Poly]{}
commits := map[int][]structs.Vector[ring.Poly]{}
row := make(map[int]*dkgvss.DealerInput, n)
for i := 0; i < n; i++ {
shares[i] = round1[i].Shares[j]
blinds[i] = round1[i].Blinds[j]
commits[i] = round1[i].Commits
}
sj, _, _, badID, err := sessions[j].Round2Identify(shares, blinds, commits)
if err != nil {
abortEv, abortErr := buildAbortEvidence(suite, digests, suiteID, n, t,
validators, j, badID, shares, blinds, commits)
if abortErr != nil {
return nil, nil, fmt.Errorf("keyera: bootstrap-pedersen Round2[%d] + abort-evidence: %v / %w", j, abortErr, err)
share, blind, err := sess.parties[j].OpenDealerShare(i, round1[i].Envelopes[j])
if err != nil {
return nil, fmt.Errorf("keyera: bootstrap-pedersen open[%d<-%d]: %w", j, i, err)
}
return nil, nil, fmt.Errorf("%w: %w", ErrBootstrapPedersenAbort,
abortEvidenceWrap(abortEv, err))
row[i] = &dkgvss.DealerInput{Commits: round1[i].Commits, Share: share, Blind: blind}
}
sShares[j] = sj
inputs[j] = row
}
// Path (a) noise flooding. Compute Lagrange weights at X=0 for the
// full committee T = {0, ..., n-1}; each party's contribution scales
// its share by λ_j so the aggregate equals s. Each party then adds a
// fresh Gaussian e_j' under σ'' = κ · σ_E · √n.
//
// The deterministic noise seed for e_j' is derived from the active
// HashSuite over the canonical transcript prefix so a KAT replay can
// reproduce the bootstrap byte-for-byte from a single entropy source.
A := sessions[0].APublic()
lagrange := computeFullCommitteeLagrange(r, n)
return &PedersenContributions{
session: sess,
commits: commits,
digests: digests,
inputs: inputs,
}, nil
}
// Build aggregated b in NTT-Mont form by summing per-party β_j.
// We also serialize each β_j for the transcript.
bSum := utils.InitializeVector(r, sign.M)
// finishPedersenContribs runs Round 3 (Pedersen verify + aggregate) for every
// recipient over the opened contributions, then the Path (a) noise flooding and
// KeyShare assembly. On the first dealer whose contribution fails verification
// it returns ErrBootstrapPedersenAbort with re-checkable blame evidence.
func finishPedersenContribs(suite hash.HashSuite, suiteID string, t, n int, validators []string, groupID CoronaGroupID, eraID CoronaKeyEraID, contribs *PedersenContributions) (*KeyEra, *BootstrapTranscript, error) {
sess := contribs.session
cr := sess.cr
digests := contribs.digests
// Round 3 — each recipient verifies every dealer's (share, blind) against
// the commits and aggregates its own secret share s_j. On any identifiable
// abort we mint a signed complaint and return.
sShares := make([]structs.Vector[lring.Poly], n)
for j := 0; j < n; j++ {
res, fault, err := sess.parties[j].Round3(contribs.inputs[j])
if err != nil {
if fault == nil {
return nil, nil, fmt.Errorf("keyera: bootstrap-pedersen Round3[%d]: %w", j, err)
}
abortEv, abortErr := buildAbortEvidence(sess, suite, suiteID, digests, n, t, validators, fault)
if abortErr != nil {
return nil, nil, fmt.Errorf("keyera: bootstrap-pedersen Round3[%d] + abort-evidence: %v / %w", j, abortErr, err)
}
return nil, nil, fmt.Errorf("%w: %w", ErrBootstrapPedersenAbort, abortEvidenceWrap(abortEv, err))
}
sShares[j] = res.Share
}
// Path (a) noise flooding. Compute Lagrange weights at X=0 for the full
// committee T = {0, ..., n-1}; each party scales its share by λ_j so the
// aggregate equals s. Each party then adds a fresh Gaussian e_j' under
// σ″ = κ · σ_E · √n. The deterministic noise seed is derived from the
// active HashSuite over the canonical transcript prefix so a KAT replay can
// reproduce the bootstrap byte-for-byte from a single entropy source.
A := sess.profile.A
lagrange := computeFullCommitteeLagrange(cr, n)
bSum := utils.InitializeVector(cr, sign.M)
betaSerialized := make([][]byte, n)
noiseSigma, noiseBound := pathANoiseParameters(n)
@@ -282,35 +412,35 @@ func finishBootstrapPedersen(suite hash.HashSuite, suiteID string, t, n int, val
for j := 0; j < n; j++ {
// λ_j in NTT-Mont form for the multiplication.
lambda := r.NewPoly()
lambda := cr.NewPoly()
lambda.Copy(lagrange[j])
r.NTT(lambda, lambda)
r.MForm(lambda, lambda)
cr.NTT(lambda, lambda)
cr.MForm(lambda, lambda)
// NTT-Mont share s_j scaled by λ_j: λ_j · NTT-Mont(s_j).
// sShares[j] is in standard coefficient form; convert to NTT-Mont.
sNTT := make(structs.Vector[ring.Poly], sign.N)
sNTT := make(structs.Vector[lring.Poly], sign.N)
for vi := 0; vi < sign.N; vi++ {
sNTT[vi] = *sShares[j][vi].CopyNew()
r.NTT(sNTT[vi], sNTT[vi])
r.MForm(sNTT[vi], sNTT[vi])
cr.NTT(sNTT[vi], sNTT[vi])
cr.MForm(sNTT[vi], sNTT[vi])
}
// (λ_j · s_j) coordinate-wise.
scaled := make(structs.Vector[ring.Poly], sign.N)
scaled := make(structs.Vector[lring.Poly], sign.N)
for vi := 0; vi < sign.N; vi++ {
scaled[vi] = r.NewPoly()
r.MulCoeffsMontgomery(sNTT[vi], lambda, scaled[vi])
scaled[vi] = cr.NewPoly()
cr.MulCoeffsMontgomery(sNTT[vi], lambda, scaled[vi])
}
// β_j = A · (λ_j · s_j) + e_j' where e_j' ~ D(σ'').
Aprod := utils.InitializeVector(r, sign.M)
utils.MatrixVectorMul(r, A, scaled, Aprod)
// β_j = A · (λ_j · s_j) + e_j' where e_j' ~ D(σ″).
Aprod := utils.InitializeVector(cr, sign.M)
utils.MatrixVectorMul(cr, A, scaled, Aprod)
ePrime := samplePathANoise(r, suite, noiseSeedTranscript, j, noiseSigma, noiseBound)
ePrime := samplePathANoise(cr, suite, noiseSeedTranscript, j, noiseSigma, noiseBound)
// ePrime is in NTT-Mont; β_j = Aprod + ePrime.
beta := utils.InitializeVector(r, sign.M)
utils.VectorAdd(r, Aprod, ePrime, beta)
beta := utils.InitializeVector(cr, sign.M)
utils.VectorAdd(cr, Aprod, ePrime, beta)
// Serialize β_j for the transcript.
buf, serr := serializeVector(beta)
@@ -320,17 +450,17 @@ func finishBootstrapPedersen(suite hash.HashSuite, suiteID string, t, n int, val
betaSerialized[j] = buf
// Accumulate.
utils.VectorAdd(r, bSum, beta, bSum)
utils.VectorAdd(cr, bSum, beta, bSum)
}
// b = Σ_j β_j = A·s + e'' in NTT-Mont. Convert to standard form,
// then round to bTilde under the QXi ring.
utils.ConvertVectorFromNTT(r, bSum)
bTilde := utils.RoundVector(r, dkgParams.RXi, bSum, sign.Xi)
// b = Σ_j β_j = A·s + e″ in NTT-Mont. Convert to standard form, then round
// to bTilde under the QXi ring.
utils.ConvertVectorFromNTT(cr, bSum)
bTilde := utils.RoundVector(cr, sess.crXi, bSum, sign.Xi)
// Build Corona threshold.GroupKey. The matrix A is the dkg2 A
// (nothing-up-my-sleeve), so the era is reproducible from the
// transcript alone.
// Build Corona threshold.GroupKey. The matrix A is the Ringtail A
// (nothing-up-my-sleeve, byte-identical to the prior dkg2 A), so the era is
// reproducible from the transcript alone.
thParams, err := threshold.NewParams()
if err != nil {
return nil, nil, fmt.Errorf("keyera: bootstrap-pedersen threshold params: %w", err)
@@ -341,11 +471,11 @@ func finishBootstrapPedersen(suite hash.HashSuite, suiteID string, t, n int, val
Params: thParams,
}
// Pairwise seeds and MAC keys. In a distributed deployment these come
// from authenticated pairwise KEX (reshare/pairwise.go); the kernel
// derives them deterministically from the transcript so KATs can
// replay. The bytes are independent of any secret share material —
// they form a public symmetric mask that the cohort agrees on.
// Pairwise seeds and MAC keys. In a distributed deployment these come from
// authenticated pairwise KEX (reshare/pairwise.go); the kernel derives them
// deterministically from the transcript so KATs can replay. The bytes are
// independent of any secret share material — they form a public symmetric
// mask the cohort agrees on (protection comes from each party's SkShare).
seedTranscript := suite.TranscriptHash(
[]byte("CORONA-BOOTSTRAP-PEDERSEN-PAIRWISE-v1"),
noiseSeedTranscript[:],
@@ -366,9 +496,9 @@ func finishBootstrapPedersen(suite hash.HashSuite, suiteID string, t, n int, val
}
for j, v := range validators {
// sShares[j] is in standard form; convert to NTT-Mont for the
// signing-time multiplication path (matches threshold.GenerateKeysTrustedDealer
// signing-time multiplication path (matches the trusted-dealer
// convention).
skNTT := make(structs.Vector[ring.Poly], sign.N)
skNTT := make(structs.Vector[lring.Poly], sign.N)
for vi := 0; vi < sign.N; vi++ {
skNTT[vi] = *sShares[j][vi].CopyNew()
}
@@ -417,21 +547,38 @@ func finishBootstrapPedersen(suite hash.HashSuite, suiteID string, t, n int, val
}, transcript, nil
}
// FinishBootstrapPedersen is the kernel entrypoint that takes a fully
// pre-computed set of Round1 outputs (one per party) and drives the
// remainder of the protocol: Round 1.5 digest computation, Round 2
// DealPedersen runs the share-dealing half of BootstrapPedersen (Round 1 +
// envelope opening) over the supplied per-party sampling seeds and returns the
// opened contributions. Tests use this to tamper one opened share before
// calling FinishBootstrapPedersen, exercising the identifiable-abort path.
//
// seeds must have exactly n = len(validators) entries of sign.KeySize bytes.
func DealPedersen(suite hash.HashSuite, t int, validators []string, seeds [][]byte) (*PedersenContributions, 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 n < 2 || t >= n {
return nil, fmt.Errorf("%w: t=%d n=%d", ErrBootstrapPedersenShape, t, n)
}
if len(seeds) != n {
return nil, fmt.Errorf("%w: seeds=%d n=%d", ErrBootstrapPedersenShape, len(seeds), n)
}
return dealPedersen(hash.Resolve(suite), t, n, seeds)
}
// FinishBootstrapPedersen takes a fully assembled set of opened contributions
// (produced by DealPedersen) and drives the remainder of the protocol: Round 3
// verification, Path (a) noise flooding, and KeyShare assembly.
//
// The caller is responsible for producing the dkg2.Round1Output array;
// in production this is the per-party network broadcast. In tests, the
// caller may deliberately tamper a Round1 output to exercise the
// identifiable-abort path.
//
// The dkgParams and sessions arrays MUST correspond to the supplied
// round1 outputs (same n, same t, same A, B matrices).
// In production the contributions arrive over the network; in tests the caller
// may tamper one opened share to exercise the identifiable-abort path.
//
// suite=nil resolves to the production default (Corona-SHA3).
func FinishBootstrapPedersen(suite hash.HashSuite, t int, validators []string, groupID CoronaGroupID, eraID CoronaKeyEraID, dkgParams *dkg2.Params, sessions []*dkg2.DKGSession, round1 []*dkg2.Round1Output) (*KeyEra, *BootstrapTranscript, error) {
func FinishBootstrapPedersen(suite hash.HashSuite, t int, validators []string, groupID CoronaGroupID, eraID CoronaKeyEraID, contribs *PedersenContributions) (*KeyEra, *BootstrapTranscript, error) {
if len(validators) == 0 {
return nil, nil, ErrEmptyValidators
}
@@ -442,11 +589,11 @@ func FinishBootstrapPedersen(suite hash.HashSuite, t int, validators []string, g
if n < 2 || t >= n {
return nil, nil, fmt.Errorf("%w: t=%d n=%d", ErrBootstrapPedersenShape, t, n)
}
if len(sessions) != n || len(round1) != n {
return nil, nil, fmt.Errorf("%w: sessions=%d round1=%d, expected %d", ErrBootstrapPedersenShape, len(sessions), len(round1), n)
if contribs == nil || contribs.session == nil || len(contribs.inputs) != n {
return nil, nil, fmt.Errorf("%w: malformed contributions", ErrBootstrapPedersenShape)
}
suite = hash.Resolve(suite)
return finishBootstrapPedersen(suite, suite.ID(), t, n, validators, groupID, eraID, dkgParams, sessions, round1)
return finishPedersenContribs(suite, suite.ID(), t, n, validators, groupID, eraID, contribs)
}
// BootstrapTrustedDealer is the legacy single-trusted-party bootstrap
@@ -482,7 +629,7 @@ func BootstrapTrustedDealerWithSuite(suite hash.HashSuite, t int, validators []s
// pathANoiseParameters returns the (σ, bound) pair for the Path (a)
// noise-flooding sub-protocol over a committee of n parties. The slack
// reservation σ” = κ · σ_E · √n is the LP-073 §5 bound: it is large
// reservation σ″ = κ · σ_E · √n is the LP-073 §5 bound: it is large
// enough that the published β_j = A · (λ_j · s_j) + e_j' leaks no more
// information about s_j than a fresh LWE sample (the standard MLWE
// noise-flooding argument under DDH/MLWE).
@@ -499,10 +646,10 @@ func pathANoiseParameters(n int) (sigma, bound float64) {
//
// The Gaussian PRNG is seeded by HashSuite(noise-tag || party-index) so
// every honest party that re-derives the bootstrap transcript can verify
// the broadcast β_j byte-for-byte. In the production distributed
// protocol the seed comes from each party's own private RNG; here we
// derive deterministically for KAT-replay.
func samplePathANoise(r *ring.Ring, suite hash.HashSuite, transcript [32]byte, partyID int, sigma, bound float64) structs.Vector[ring.Poly] {
// the broadcast β_j byte-for-byte. In the production distributed protocol
// the seed comes from each party's own private RNG; here we derive
// deterministically for KAT-replay.
func samplePathANoise(r *lring.Ring, suite hash.HashSuite, transcript [32]byte, partyID int, sigma, bound float64) structs.Vector[lring.Poly] {
var partyBuf [4]byte
binary.BigEndian.PutUint32(partyBuf[:], uint32(partyID))
seed := suite.TranscriptHash(
@@ -511,8 +658,8 @@ func samplePathANoise(r *ring.Ring, suite hash.HashSuite, transcript [32]byte, p
partyBuf[:],
)
prng, _ := sampling.NewKeyedPRNG(seed[:])
gauss := ring.NewGaussianSampler(prng, r,
ring.DiscreteGaussian{Sigma: sigma, Bound: bound}, false)
gauss := lring.NewGaussianSampler(prng, r,
lring.DiscreteGaussian{Sigma: sigma, Bound: bound}, false)
return utils.SamplePolyVector(r, sign.M, gauss, true, true)
}
@@ -551,35 +698,27 @@ func derivePairwisePedersen(n int, transcriptSeed [32]byte) (map[int][][]byte, [
return seeds, macKeys
}
// buildAbortEvidence packages a single recipient's Round2Identify
// failure into a signed-complaint-shaped AbortEvidence record. The
// senderID, when ≥ 0, names the misbehaving sender that recipient j
// observed.
//
// The kernel does not sign the complaints (signing requires the wire-
// identity ed25519 key, which lives at the consensus layer); it
// constructs unsigned complaints that the consensus layer signs and
// broadcasts. Tests inject an in-memory wire key via the AbortEvidence
// callers, which is fine because the bootstrap is in-process.
func buildAbortEvidence(suite hash.HashSuite, digests [][32]byte, suiteID string, n, t int, validators []string, complainerID, senderID int, shares, blinds map[int]structs.Vector[ring.Poly], commits map[int][]structs.Vector[ring.Poly]) (*AbortEvidence, error) {
if senderID < 0 || senderID >= n {
// Wrap missing-input failures as ComplaintMissing — the recipient
// can still drive disqualification without naming a specific
// commit/share pair.
return &AbortEvidence{
TranscriptHash: suite.TranscriptHash(
[]byte(transcriptTag),
[]byte(suiteID),
framedJoinValidators(validators),
framedJoinUint32([]uint32{uint32(t), uint32(n)}),
framedJoin(digests),
),
Complaints: nil,
Disqualified: map[int]struct{}{
complainerID: {},
},
}, nil
// coronaCommitDigest computes the Round 1.5 commit digest of a dealer's commit
// vector under the corona HashSuite. The digest is taken over the lattigo wire
// bytes of the commits, byte-identical to the prior dkg2 Round1Output.CommitDigest
// (same tag, same suite-ID binding, same serialization), so the noise seed and
// transcript hash are preserved across the move to luxfi/dkg.
func coronaCommitDigest(suite hash.HashSuite, commits []dkgring.Vector) ([32]byte, error) {
var buf bytes.Buffer
for _, v := range commits {
if _, err := v.WriteTo(&buf); err != nil {
return [32]byte{}, err
}
}
return suite.TranscriptHash([]byte(tagCommitDigest), []byte(suite.ID()), buf.Bytes()), nil
}
// buildAbortEvidence packages a recipient's Round-3 VerifyFault into a signed
// blame complaint naming the offending dealer by NodeID, plus the disqualified
// committee index. The accuser is the recipient that observed the fault; it
// signs with its deterministic in-process identity. Any third party re-checks
// the accusation with vss.RecheckBadDelivery — no trust in the accuser.
func buildAbortEvidence(sess *pedersenSession, suite hash.HashSuite, suiteID string, digests [][32]byte, n, t int, validators []string, fault *dkgvss.VerifyFault) (*AbortEvidence, error) {
transcriptHash := suite.TranscriptHash(
[]byte(transcriptTag),
[]byte(suiteID),
@@ -587,20 +726,19 @@ func buildAbortEvidence(suite hash.HashSuite, digests [][32]byte, suiteID string
framedJoinUint32([]uint32{uint32(t), uint32(n)}),
framedJoin(digests),
)
c, err := dkg2.NewBadDeliveryComplaint(
transcriptHash,
senderID,
complainerID,
shares[senderID], blinds[senderID], commits[senderID],
accused := sess.nodes[fault.DealerIndex]
accuser := sess.nodes[fault.RecipientIndex]
c, err := dkgvss.NewBadDeliveryComplaint(
sess.profile, fault, transcriptHash, accused, accuser, sess.ids[fault.RecipientIndex],
)
if err != nil {
return nil, err
}
return &AbortEvidence{
TranscriptHash: transcriptHash,
Complaints: []*dkg2.Complaint{c},
Complaints: []*dkgblame.Complaint{c},
Disqualified: map[int]struct{}{
senderID: {},
fault.DealerIndex: {},
},
}, nil
}
@@ -679,9 +817,44 @@ func asBootstrapAbortErr(err error, out **bootstrapAbortErr) bool {
return false
}
// pedersenNodeID returns the deterministic committee NodeID for index i. The
// first two bytes encode the 1-based position; the rest are a fixed domain tag
// byte. NodeIDs are public committee labels, not secrets.
func pedersenNodeID(i int) dkgchannel.NodeID {
var id dkgchannel.NodeID
binary.BigEndian.PutUint16(id[:2], uint16(i+1))
id[2] = 0xC0
return id
}
// derivedStream returns a deterministic unbounded byte stream from a domain tag
// and a 32-bit selector (party index). Used to seed channel identities and KEM
// randomness reproducibly.
func derivedStream(tag string, sel uint32) io.Reader {
h := blake3.New()
_, _ = h.Write([]byte(tag))
_, _ = h.Write(u32be(sel))
return h.Digest()
}
// seededRound1Rng returns the reader vss.Round1 consumes: the exact 32-byte
// sampling seed first (so the χ draws — hence commits and shares — match the
// prior dkg2 path byte-for-byte), then a deterministic KEM-randomness tail
// (which affects only the immediately-opened envelope ciphertext).
func seededRound1Rng(seed []byte, partyIndex int) io.Reader {
return io.MultiReader(bytes.NewReader(seed), derivedStream(kemStreamTag, uint32(partyIndex)))
}
// u32be returns the 4-byte big-endian encoding of x.
func u32be(x uint32) []byte {
var b [4]byte
binary.BigEndian.PutUint32(b[:], x)
return b[:]
}
// serializeVector returns the canonical wire bytes of a vector via the
// lattigo WriteTo. Length-prefixed so concatenation is unambiguous.
func serializeVector(v structs.Vector[ring.Poly]) ([]byte, error) {
func serializeVector(v structs.Vector[lring.Poly]) ([]byte, error) {
var inner []byte
buf := bytesBuffer{}
if _, err := v.WriteTo(&buf); err != nil {
@@ -695,7 +868,7 @@ func serializeVector(v structs.Vector[ring.Poly]) ([]byte, error) {
}
// bTildeBytes returns the canonical wire bytes of the final bTilde.
func bTildeBytes(bTilde structs.Vector[ring.Poly]) []byte {
func bTildeBytes(bTilde structs.Vector[lring.Poly]) []byte {
buf := bytesBuffer{}
if _, err := bTilde.WriteTo(&buf); err != nil {
// WriteTo on a valid Vector cannot fail with a bytesBuffer.
@@ -764,14 +937,3 @@ func (b *bytesBuffer) Write(p []byte) (int, error) {
}
func (b *bytesBuffer) Bytes() []byte { return b.b }
// computeFullCommitteeLagrange (re-exported here for the path-(a) noise-
// flood path) computes Lagrange coefficients λ_j evaluated at X = 0 for
// the full committee {0, 1, ..., n-1} (1-indexed evaluation points
// 1..n). The result is in standard coefficient form; callers convert to
// NTT-Mont as needed.
//
// Defined in keyera.go alongside the trusted-dealer Bootstrap; redeclared
// here so the function reads as one cohesive Pedersen module.
var _ = primitives.ComputeLagrangeCoefficients
var _ = (*big.Int)(nil)
+39 -36
View File
@@ -8,10 +8,13 @@ import (
"reflect"
"testing"
"github.com/luxfi/corona/dkg2"
"github.com/luxfi/corona/hash"
"github.com/luxfi/corona/sign"
"github.com/luxfi/corona/threshold"
dkgblame "github.com/luxfi/dkg/blame"
dkgring "github.com/luxfi/dkg/ring"
dkgvss "github.com/luxfi/dkg/vss"
)
// TestBootstrapPedersen_RoundTrip — 5 parties run Pedersen DKG with
@@ -126,41 +129,27 @@ func TestBootstrapPedersen_DishonestDealer(t *testing.T) {
validators := []string{"v1", "v2", "v3", "v4", "v5"}
suite := hash.NewCoronaSHA3()
// Reproduce Round1 outputs via the dkg2 API so we can tamper one.
dkgParams, err := dkg2.NewParams()
if err != nil {
t.Fatalf("dkg2.NewParams: %v", err)
}
sessions := make([]*dkg2.DKGSession, n)
round1 := make([]*dkg2.Round1Output, n)
// Run the share-dealing half (vss Round 1 + envelope opening) so we can
// tamper one opened share before the verify/aggregate round.
rng := deterministicRand("bootstrap-pedersen-dishonest")
seeds := make([][]byte, n)
for i := 0; i < n; i++ {
sess, err := dkg2.NewDKGSession(dkgParams, i, n, thr, suite)
if err != nil {
t.Fatalf("NewDKGSession[%d]: %v", i, err)
}
sessions[i] = sess
seed := make([]byte, sign.KeySize)
if _, err := rng.Read(seed); err != nil {
seeds[i] = make([]byte, sign.KeySize)
if _, err := rng.Read(seeds[i]); err != nil {
t.Fatalf("rng.Read[%d]: %v", i, err)
}
out, err := sess.Round1WithSeed(seed)
if err != nil {
t.Fatalf("Round1WithSeed[%d]: %v", i, err)
}
round1[i] = out
}
contribs, err := DealPedersen(suite, thr, validators, seeds)
if err != nil {
t.Fatalf("DealPedersen: %v", err)
}
// Tamper sender 2's share-to-recipient-0. Round2Identify run from
// the perspective of recipient 0 must name sender 2 as the bad
// actor. The orchestrator runs every recipient in order, so
// recipient 0 detects the abort first.
round1[2].Shares[0][0].Coeffs[0][0] ^= 0x42
// Tamper sender 2's share as recipient 0 opened it. Round 3 from the
// perspective of recipient 0 (processed first) must name sender 2 as the
// bad actor via the Pedersen identity.
contribs.inputs[0][2].Share[0].Coeffs[0][0] ^= 0x42
era, transcript, err := FinishBootstrapPedersen(
suite, thr, validators, 0, 0,
dkgParams, sessions, round1,
)
era, transcript, err := FinishBootstrapPedersen(suite, thr, validators, 0, 0, contribs)
if era != nil || transcript != nil {
t.Fatal("expected (nil, nil) on abort")
}
@@ -181,12 +170,26 @@ func TestBootstrapPedersen_DishonestDealer(t *testing.T) {
if len(ev.Complaints) == 0 {
t.Fatal("expected at least one Complaint in evidence")
}
if ev.Complaints[0].SenderID != 2 {
t.Fatalf("complaint.SenderID: want 2 got %d", ev.Complaints[0].SenderID)
}
if ev.Complaints[0].Reason != dkg2.ComplaintBadDelivery {
if ev.Complaints[0].Reason != dkgblame.ReasonBadDelivery {
t.Fatalf("complaint.Reason: want bad-delivery got %v", ev.Complaints[0].Reason)
}
if ev.Complaints[0].Accused != pedersenNodeID(2) {
t.Fatalf("complaint.Accused: want NodeID of dealer 2, got %x", ev.Complaints[0].Accused)
}
// Third-party re-check: the complaint's evidence confirms the share is
// malformed without trusting the accuser.
prof, err := dkgring.Ringtail()
if err != nil {
t.Fatalf("ring.Ringtail: %v", err)
}
justified, err := dkgvss.RecheckBadDelivery(prof, ev.Complaints[0])
if err != nil {
t.Fatalf("RecheckBadDelivery: %v", err)
}
if !justified {
t.Fatal("RecheckBadDelivery says the accusation is unjustified — tamper not detected")
}
// The transcript hash on the evidence is non-zero — chain can
// commit the abort transcript and stay at previous epoch.
@@ -247,9 +250,9 @@ func TestBootstrapPedersen_NoMasterSecretInMemory(t *testing.T) {
t.Fatalf("BootstrapPedersen: %v", err)
}
// Structural assertion 1: dkg2.DKGSession exposes no "master secret"
// accessor.
dkgType := reflect.TypeOf((*dkg2.DKGSession)(nil)).Elem()
// Structural assertion 1: the vss.Party exposes no "master secret"
// accessor (no-reconstruct: a party retains only its own aggregated share).
dkgType := reflect.TypeOf((*dkgvss.Party)(nil)).Elem()
for i := 0; i < dkgType.NumField(); i++ {
f := dkgType.Field(i)
// Reject any field whose lower-cased name contains the substring
+3 -3
View File
@@ -108,12 +108,12 @@ func TestCutover_RingtailMatricesByteIdentical(t *testing.T) {
t.Logf("Ringtail() A,B byte-identical to dkg2.DeriveA/DeriveB over all %dx%d slots", len(coronaA), len(coronaA[0]))
}
// seededRound1Rng returns a reader that yields seed first (the 32 bytes vss
// seededGateRng returns a reader that yields seed first (the 32 bytes vss
// Party.Round1 consumes as its sampling seed) then a deterministic tail (KEM
// encapsulation randomness — does NOT affect the commits, which are computed
// before any sealing). This makes vss's χ draws identical to a corona/dkg2
// Round1WithSeed(seed) run.
func seededRound1Rng(seed []byte) io.Reader {
func seededGateRng(seed []byte) io.Reader {
tail := make([]byte, 1<<16)
// deterministic, distinct from seed; KEM randomness only.
for i := range tail {
@@ -191,7 +191,7 @@ func TestCutover_Round1CommitsByteIdentical(t *testing.T) {
t.Fatalf("ring.Ringtail: %v", err)
}
party := buildVSSParty0(t, prof, n, thr)
vssOut, err := party.Round1(seededRound1Rng(seed))
vssOut, err := party.Round1(seededGateRng(seed))
if err != nil {
t.Fatalf("vss Party.Round1: %v", err)
}
+19 -31
View File
@@ -8,9 +8,11 @@ import (
"reflect"
"testing"
"github.com/luxfi/corona/dkg2"
"github.com/luxfi/corona/hash"
"github.com/luxfi/corona/sign"
dkgblame "github.com/luxfi/dkg/blame"
dkgvss "github.com/luxfi/dkg/vss"
)
// TestReanchorPedersen_RoundTrip — open a Pedersen-DKG era, rotate to a
@@ -130,16 +132,16 @@ func TestReanchorPedersen_NoMasterSecret(t *testing.T) {
t.Fatalf("ReanchorPedersen: %v", err)
}
// Structural assertion 1: dkg2.DKGSession exposes no master-secret
// Structural assertion 1: the vss.Party exposes no master-secret
// accessor field. This is shared with BootstrapPedersen but worth
// re-asserting on the Reanchor path so a future regression on
// either is caught.
dkgType := reflect.TypeOf((*dkg2.DKGSession)(nil)).Elem()
dkgType := reflect.TypeOf((*dkgvss.Party)(nil)).Elem()
for i := 0; i < dkgType.NumField(); i++ {
f := dkgType.Field(i)
lower := lowercase(f.Name)
if contains(lower, "mastersecret") || contains(lower, "fullsecret") {
t.Fatalf("dkg2.DKGSession has a forbidden field %q — master-secret state leaks", f.Name)
t.Fatalf("dkgvss.Party has a forbidden field %q — master-secret state leaks", f.Name)
}
}
@@ -187,42 +189,28 @@ func TestReanchorPedersen_DishonestParty(t *testing.T) {
validators := []string{"v1", "v2", "v3", "v4", "v5"}
suite := hash.NewCoronaSHA3()
dkgParams, err := dkg2.NewParams()
if err != nil {
t.Fatalf("dkg2.NewParams: %v", err)
}
sessions := make([]*dkg2.DKGSession, n)
round1 := make([]*dkg2.Round1Output, n)
rng := deterministicRand("reanchor-pedersen-dishonest")
seeds := make([][]byte, n)
for i := 0; i < n; i++ {
sess, err := dkg2.NewDKGSession(dkgParams, i, n, thr, suite)
if err != nil {
t.Fatalf("NewDKGSession[%d]: %v", i, err)
}
sessions[i] = sess
seed := make([]byte, sign.KeySize)
if _, err := rng.Read(seed); err != nil {
seeds[i] = make([]byte, sign.KeySize)
if _, err := rng.Read(seeds[i]); err != nil {
t.Fatalf("rng.Read[%d]: %v", i, err)
}
out, err := sess.Round1WithSeed(seed)
if err != nil {
t.Fatalf("Round1WithSeed[%d]: %v", i, err)
}
round1[i] = out
}
contribs, err := DealPedersen(suite, thr, validators, seeds)
if err != nil {
t.Fatalf("DealPedersen: %v", err)
}
// Tamper sender 4's share-to-recipient-1. Round2Identify from
// Tamper sender 4's share as recipient 1 opened it. Round 3 from
// recipient 1's perspective must name sender 4 as the bad actor.
round1[4].Shares[1][0].Coeffs[0][0] ^= 0x42
contribs.inputs[1][4].Share[0].Coeffs[0][0] ^= 0x42
// The Reanchor abort path shares its semantics with the Bootstrap
// abort path: FinishBootstrapPedersen is the kernel entrypoint and
// the wrapping for era-id / epoch adjustments in ReanchorPedersen
// occurs strictly AFTER a successful return.
era, transcript, err := FinishBootstrapPedersen(
suite, thr, validators, 0, 1,
dkgParams, sessions, round1,
)
era, transcript, err := FinishBootstrapPedersen(suite, thr, validators, 0, 1, contribs)
if era != nil || transcript != nil {
t.Fatal("expected (nil, nil) on Reanchor abort")
}
@@ -242,10 +230,10 @@ func TestReanchorPedersen_DishonestParty(t *testing.T) {
if len(ev.Complaints) == 0 {
t.Fatal("expected at least one Complaint in evidence")
}
if ev.Complaints[0].SenderID != 4 {
t.Fatalf("complaint.SenderID: want 4 got %d", ev.Complaints[0].SenderID)
if ev.Complaints[0].Accused != pedersenNodeID(4) {
t.Fatalf("complaint.Accused: want NodeID of dealer 4, got %x", ev.Complaints[0].Accused)
}
if ev.Complaints[0].Reason != dkg2.ComplaintBadDelivery {
if ev.Complaints[0].Reason != dkgblame.ReasonBadDelivery {
t.Fatalf("complaint.Reason: want bad-delivery got %v", ev.Complaints[0].Reason)
}
if ev.TranscriptHash == [32]byte{} {