Files
corona/reshare/full_integration_test.go
zeekay 8c010dd8ef corona: rename module path github.com/luxfi/pulsar → github.com/luxfi/corona
Repo forks from luxfi/pulsar (R-LWE production threshold for Lux
consensus) to luxfi/corona under the family rename. Same Ring-LWE
code, same hash family (Pulsar-SHA3 / Pulsar-BLAKE3 -- wire-format
customization tags stay stable per MED-2). Only the Go module path
and internal imports change.

Lux family alignment:
  - Pulsar  = M-LWE / FIPS 204 / NIST MPTC N1 (was luxfi/pulsar-m)
  - Corona  = R-LWE / NIST MPTC S1            (this repo)
  - Corona = academic upstream + Lux DKG expansions
2026-05-11 14:55:26 -07:00

255 lines
8.7 KiB
Go

// Copyright (C) 2025-2026, Lux Industries Inc. All rights reserved.
// See the file LICENSE for licensing terms.
package reshare
import (
"crypto/ed25519"
"crypto/rand"
"math/big"
"testing"
"github.com/luxfi/corona/primitives"
"github.com/luxfi/corona/sign"
"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"
"golang.org/x/crypto/curve25519"
)
// TestFullReshareThenKeyShareThenSign — production-relevance integration
// test that exercises Correction 3:
//
// 1. Genesis: create (A, s, e, b̃) and standard-Shamir shares for an
// OLD committee.
// 2. The OLD committee runs sign once with bTilde to confirm the
// genesis is sound.
// 3. Resharing: rotate the share distribution onto a NEW committee
// via Reshare. NO trusted dealer.
// 4. The NEW committee establishes pairwise KEX → DeriveSeeds /
// DeriveMACKeys and constructs full PartyKeyShare instances via
// PartyKeyShareFromShare (Lambda + Seeds + MACKeys + GroupKey
// pointer all populated).
// 5. The NEW committee drives sign.SignRound1/Round2/Finalize,
// and the resulting signature MUST verify against the
// UNCHANGED bTilde from step 1.
//
// This is the test that proves the brief's Correction 3 is satisfied:
// resharing only SkShare is not enough — Lambda, Seeds, MACKeys, and
// GroupKey must regenerate too, and ALL of them must be sufficient
// to drive a verifying signature.
func TestFullReshareThenKeyShareThenSign(t *testing.T) {
r, err := ring.NewRing(1<<sign.LogN, []uint64{sign.Q})
if err != nil {
t.Fatal(err)
}
rXi, _ := ring.NewRing(1<<sign.LogN, []uint64{sign.QXi})
rNu, _ := ring.NewRing(1<<sign.LogN, []uint64{sign.QNu})
// ─── Genesis ────────────────────────────────────────────────
const tOld, nOld = 2, 3
const nVec, mPk = sign.N, sign.M
prng, _ := sampling.NewKeyedPRNG([]byte("full-integration-genesis"))
uniformSampler := ring.NewUniformSampler(prng, r)
gaussian := ring.DiscreteGaussian{Sigma: sign.SigmaE, Bound: sign.BoundE}
gaussianSampler := ring.NewGaussianSampler(prng, r, gaussian, false)
A := utils.SamplePolyMatrix(r, mPk, nVec, uniformSampler, true, true)
s := utils.SamplePolyVector(r, nVec, gaussianSampler, false, false)
primSharesMap := primitives.ShamirSecretSharingGeneral(r, s, tOld, nOld)
oldSharesStd := make(map[int]Share, nOld)
for partyIdx, vec := range primSharesMap {
oldSharesStd[partyIdx+1] = structs.Vector[ring.Poly](vec)
}
// b = A·s + e in NTT-Mont, then bTilde rounded.
utils.ConvertVectorToNTT(r, s)
e := utils.SamplePolyVector(r, mPk, gaussianSampler, true, true)
b := utils.InitializeVector(r, mPk)
utils.MatrixVectorMul(r, A, s, b)
utils.VectorAdd(r, b, e, b)
utils.ConvertVectorFromNTT(r, b)
bTilde := utils.RoundVector(r, rXi, b, sign.Xi)
// Persistent group key — UNCHANGED across resharing.
groupKey := &PartyGroupKey{A: A, BTilde: bTilde}
// ─── Reshare ────────────────────────────────────────────────
// New committee: 5 parties with threshold 3.
const tNew = 3
newCommittee := []int{17, 18, 19, 20, 21}
newSharesStd, err := Reshare(
r, oldSharesStd, tOld, newCommittee, tNew,
newFakeRand([]byte("full-integration-reshare")),
)
if err != nil {
t.Fatalf("Reshare: %v", err)
}
// ─── Pairwise KEX (production-style) ────────────────────────
// Each new validator generates an Ed25519 wire identity + ephemeral
// X25519 pair. The transcript hash binds everything together.
K := tNew // The active signing set we drive sign.Party with.
wirePub := make([]ed25519.PublicKey, K)
wirePriv := make([]ed25519.PrivateKey, K)
for i := 0; i < K; i++ {
pub, priv, err := ed25519.GenerateKey(nil)
if err != nil {
t.Fatal(err)
}
wirePub[i] = pub
wirePriv[i] = priv
}
ephPriv := make([][]byte, K)
ephPub := make([][]byte, K)
for i := 0; i < K; i++ {
ephPriv[i] = make([]byte, 32)
if _, err := rand.Read(ephPriv[i]); err != nil {
t.Fatal(err)
}
ephPub[i], _ = curve25519.X25519(ephPriv[i], curve25519.Basepoint)
}
// Build a transcript binding for the resharing invocation.
wireKeys := make([][]byte, K)
for i := 0; i < K; i++ {
wireKeys[i] = wirePub[i]
}
tIn := TranscriptInputs{
ChainID: []byte("lux-mainnet-test"),
GroupID: []byte("quasar-pq-test"),
OldEpochID: 1, NewEpochID: 2,
OldSetHash: ValidatorSetHash(wireKeys, nil), NewSetHash: ValidatorSetHash(wireKeys, nil),
ThresholdOld: tOld, ThresholdNew: tNew,
Variant: "reshare",
}
tHash := tIn.Hash(nil)
// Each party signs its ephemeral.
ephSig := make([][]byte, K)
for i := 0; i < K; i++ {
ephSig[i] = SignEphemeral(wirePriv[i], ephPub[i], tHash)
}
// Each pair runs AuthenticatedKex.
authKex := make(map[[2]int][]byte, K*(K-1)/2)
for i := 0; i < K; i++ {
for j := i + 1; j < K; j++ {
ak, err := AuthenticatedKex(ephPriv[i], ephPub[j], ephSig[j], wirePub[j], tHash, nil)
if err != nil {
t.Fatalf("AuthenticatedKex(%d, %d): %v", i, j, err)
}
authKex[[2]int{i, j}] = ak
}
}
// Self-seeds — derived from the validator's own wire key.
selfSeeds := make(map[int][]byte, K)
for i := 0; i < K; i++ {
// In production this is a hot-key-derived 32-byte seed; in the
// test we use the wire pubkey.
buf := make([]byte, 32)
copy(buf, wirePub[i])
selfSeeds[i] = buf
}
pairwiseSeeds, err := DeriveSeeds(K, authKex, selfSeeds,
tIn.ChainID, tIn.GroupID, 0, tIn.NewEpochID, nil, sign.KeySize)
if err != nil {
t.Fatal(err)
}
pairwiseMACs, err := DeriveMACKeys(K, authKex,
tIn.ChainID, tIn.GroupID, 0, tIn.NewEpochID, nil, sign.KeySize)
if err != nil {
t.Fatal(err)
}
// ─── PartyKeyShareFromShare for each new party ──────────────
// We pick the lowest-tNew IDs of newCommittee as the active
// signing set.
activeNewIDs := newCommittee[:tNew] // {17, 18, 19}
signSetForLagrange := make([]int, tNew)
for i, id := range activeNewIDs {
signSetForLagrange[i] = id - 1
}
lagrangeNew := primitives.ComputeLagrangeCoefficients(r, signSetForLagrange, big.NewInt(int64(sign.Q)))
pkShares := make([]*PartyKeyShare, K)
for idx, realID := range activeNewIDs {
// Reuse PartyKeyShareFromShare to populate the bulk of the
// fields. Pass the active-signers committee (indices 0..K-1
// are the protocol indices, not party IDs).
_ = idx
pks, err := PartyKeyShareFromShare(
r, newSharesStd[realID],
realID, // real 1-indexed ID
activeNewIDs,
pairwiseSeeds, pairwiseMACs,
groupKey,
)
if err != nil {
t.Fatalf("PartyKeyShareFromShare for %d: %v", realID, err)
}
pkShares[pks.Index] = pks
}
// ─── Drive the 2-round Sign protocol ────────────────────────
// sign.Party expects skShare in NTT-Mont form. Convert per party.
sign.K = K
sign.Threshold = K
signers := make([]*sign.Party, K)
signSetProtocol := make([]int, K)
for i := 0; i < K; i++ {
signSetProtocol[i] = i
}
for idx := 0; idx < K; idx++ {
party := sign.NewParty(idx, r, rXi, rNu, uniformSampler)
// Convert the reshared SkShare to NTT-Mont form (sign.Party
// expects this form).
skShareNTT := cloneVector(r, pkShares[idx].SkShare)
utils.ConvertVectorToNTT(r, skShareNTT)
party.SkShare = skShareNTT
party.Seed = pkShares[idx].Seeds
party.MACKeys = pkShares[idx].MACKeys
// Lambda: use the on-the-fly (sign-set-specific) coefficient,
// not the new-committee coefficient. This is the standard
// signing-protocol convention. The PartyKeyShareFromShare
// helper computes Lambda for the FULL new committee; for
// signing we need it for the ACTIVE SIGNING SUBSET.
lambda := r.NewPoly()
lambda.Copy(lagrangeNew[idx])
r.NTT(lambda, lambda)
r.MForm(lambda, lambda)
party.Lambda = lambda
signers[idx] = party
}
if !runRoundsAndVerify(t, "FULL-INTEGRATION", r, rXi, rNu, A, bTilde, signers, signSetProtocol) {
t.Fatal("full-integration signature did not verify against UNCHANGED bTilde")
}
// ─── Forced erasure ──────────────────────────────────────────
// After activation, every old share MUST be erased.
for _, sh := range oldSharesStd {
EraseShare(sh)
}
// Confirm the old shares are now zero.
for id, sh := range oldSharesStd {
if !allZeroShare(sh) {
t.Fatalf("old share for party %d not zeroed after EraseShare", id)
}
}
t.Log("PASS: full-integration test — Reshare + PartyKeyShareFromShare " +
"produces complete KeyShare instances; the new committee signs and " +
"verifies against the UNCHANGED bTilde; old shares erased.")
}