mirror of
https://github.com/luxfi/corona.git
synced 2026-09-24 00:25:54 +00:00
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
255 lines
8.7 KiB
Go
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.")
|
|
}
|