mirror of
https://github.com/luxfi/consensus.git
synced 2026-10-30 14:45:53 +00:00
Replaces every direct import of luxfi/corona/* and luxfi/pulsar/ref/go
in consensus with the canonical luxfi/threshold/protocols/{corona,pulsar}
alias surface. Pure import swap + go.mod bump; no behaviour change.
Files touched (16):
bench/pq_modes_bench_test.go
engine/pq/crypto.go
engine/pq/engine.go
pkg/wire/policies.go
protocol/quasar/cert_size_compare_test.go
protocol/quasar/corona_gob.go
protocol/quasar/dual_threshold_test.go
protocol/quasar/epoch.go
protocol/quasar/epoch_test.go
protocol/quasar/gpu_accel.go
protocol/quasar/grouped_threshold.go
protocol/quasar/quasar.go
protocol/quasar/types.go
protocol/quasar/wave_signer.go
protocol/quasar/wave_signer_test.go
protocol/quasar/witness.go
protocol/quasar/witness_pulsar_test.go
Import path swaps:
luxfi/corona/threshold -> luxfi/threshold/protocols/corona
luxfi/corona/keyera -> luxfi/threshold/protocols/corona/keyera
luxfi/corona/primitives -> luxfi/threshold/protocols/corona/primitives
luxfi/corona/reshare -> luxfi/threshold/protocols/corona/reshare
luxfi/corona/sign -> luxfi/threshold/protocols/corona/sign
luxfi/corona/gpu -> luxfi/threshold/protocols/corona/gpu
luxfi/pulsar/ref/go/pkg/pulsar -> luxfi/threshold/protocols/pulsar
The aliased package name "coronaThreshold" used by quasar / engine/pq /
pkg/wire identifiers is preserved at the call sites (it now refers to
the threshold/protocols/corona alias package). engine/pq/crypto.go is
the only file where the package was bare-imported as `threshold`; that
identifier was renamed to `corona` to match the new alias path.
go.mod:
- luxfi/threshold bumped to a branch pseudo-version that carries the
new alias subpackages; will pin to a stable tag once threshold's
companion commit lands and a release is cut.
- luxfi/corona and luxfi/pulsar drop to indirect (consensus now
reaches them only via threshold/protocols).
- luxfi/math v1.4.0 -> v1.4.1 and luxfi/metric v1.5.0 -> v1.5.7 are
transitive minimum-version bumps pulled by the new threshold
requirement; they are no-op for consensus call sites.
Comment-only references to "github.com/luxfi/corona/..." in bls.go,
round_digest.go, reshare_epoch.go, comprehensive_test.go,
engine_test.go, quasar_bench_test.go and config/pq_mode.go are
documentation pointers to where the underlying implementation lives;
they are accurate either way and are left unchanged (out of refactor
scope).
Layering shape after:
consensus -> threshold/protocols/{corona,pulsar} (stable alias)
|
v
luxfi/corona, luxfi/pulsar (primitive implementations)
Closes the 22-file layering violation flagged by the 2026-05-31
integration audit. Tests pass: ./engine/pq/..., ./pkg/wire/...,
./protocol/quasar/... (all key cases — TestEpochManager*,
TestRoundSigner*, TestVerifyPQFinality*, TestBLSThreshold*,
TestDualSigningFlow, TestCoronaCertSize* — green; the pre-existing
TestGroupedEpochManager_ParallelKeygen perf-bound flake on slow
machines was reproduced on unmodified main, so it is not a
regression).
200 lines
7.1 KiB
Go
200 lines
7.1 KiB
Go
// Copyright (C) 2025-2026, Lux Industries Inc. All rights reserved.
|
|
// See the file LICENSE for licensing terms.
|
|
|
|
package quasar
|
|
|
|
import (
|
|
"crypto/rand"
|
|
"errors"
|
|
"testing"
|
|
|
|
"github.com/luxfi/consensus/config"
|
|
"github.com/luxfi/crypto/mldsa"
|
|
"github.com/luxfi/threshold/protocols/pulsar"
|
|
)
|
|
|
|
// TestVerifyPQFinality_PulsarM_RealVerifyAccepts proves that
|
|
// verifyPQFinality dispatches to luxfi/pulsar-m.Verify and accepts a
|
|
// real Pulsar-M-65 signature produced over the canonical
|
|
// SigningDigest. Closes F109: the prior implementation accepted by
|
|
// bit-counting; this test passes only when an actual lattice
|
|
// signature math verification succeeds.
|
|
func TestVerifyPQFinality_PulsarM_RealVerifyAccepts(t *testing.T) {
|
|
params := pulsar.ParamsP65
|
|
priv, err := pulsar.GenerateKey(params, rand.Reader)
|
|
if err != nil {
|
|
t.Fatalf("pulsar.GenerateKey: %v", err)
|
|
}
|
|
|
|
w := NewVerkleWitness(1)
|
|
w.BindPQGroupKey(priv.Pub.Bytes)
|
|
|
|
// Build a structurally complete witness (the bit-count pre-filter
|
|
// requires at least 1 set bit; threshold=1).
|
|
witness := &WitnessProof{
|
|
Commitment: make([]byte, 32),
|
|
Path: make([]byte, 16),
|
|
OpeningProof: make([]byte, 48),
|
|
CoronaBits: []byte{0x01},
|
|
BlockHeight: 100,
|
|
StateRoot: make([]byte, 32),
|
|
Timestamp: 1700000000,
|
|
}
|
|
for i := range witness.StateRoot {
|
|
witness.StateRoot[i] = byte(i)
|
|
}
|
|
|
|
digest := witness.SigningDigest()
|
|
sig, err := pulsar.Sign(params, priv, digest[:], []byte(signingDigestCustomization), true, rand.Reader)
|
|
if err != nil {
|
|
t.Fatalf("pulsar.Sign: %v", err)
|
|
}
|
|
witness.PQSignature = sig.Bytes
|
|
|
|
if err := w.verifyPQFinality(witness, priv.Pub.Bytes, mldsa.MLDSA65); err != nil {
|
|
t.Fatalf("verifyPQFinality rejected a real Pulsar-M signature: %v", err)
|
|
}
|
|
}
|
|
|
|
// TestVerifyPQFinality_PulsarM_TamperedSignatureRejected proves a
|
|
// tampered signature is refused by the real lattice verifier — the
|
|
// load-bearing property that closes the F109 attack chain.
|
|
func TestVerifyPQFinality_PulsarM_TamperedSignatureRejected(t *testing.T) {
|
|
params := pulsar.ParamsP65
|
|
priv, err := pulsar.GenerateKey(params, rand.Reader)
|
|
if err != nil {
|
|
t.Fatalf("pulsar.GenerateKey: %v", err)
|
|
}
|
|
|
|
w := NewVerkleWitness(1)
|
|
w.BindPQGroupKey(priv.Pub.Bytes)
|
|
|
|
witness := &WitnessProof{
|
|
Commitment: make([]byte, 32),
|
|
Path: make([]byte, 16),
|
|
OpeningProof: make([]byte, 48),
|
|
CoronaBits: []byte{0x01},
|
|
BlockHeight: 100,
|
|
StateRoot: make([]byte, 32),
|
|
Timestamp: 1700000000,
|
|
}
|
|
digest := witness.SigningDigest()
|
|
sig, err := pulsar.Sign(params, priv, digest[:], []byte(signingDigestCustomization), true, rand.Reader)
|
|
if err != nil {
|
|
t.Fatalf("pulsar.Sign: %v", err)
|
|
}
|
|
|
|
// Flip a byte mid-signature. A real lattice verifier MUST refuse;
|
|
// a bit-count tautology would happily accept.
|
|
tampered := append([]byte(nil), sig.Bytes...)
|
|
tampered[len(tampered)/2] ^= 0xFF
|
|
witness.PQSignature = tampered
|
|
|
|
err = w.verifyPQFinality(witness, priv.Pub.Bytes, mldsa.MLDSA65)
|
|
if err == nil {
|
|
t.Fatal("verifyPQFinality accepted a tampered signature")
|
|
}
|
|
if !errors.Is(err, ErrPulsarVerifyFail) {
|
|
t.Fatalf("expected ErrPulsarVerifyFail, got: %v", err)
|
|
}
|
|
}
|
|
|
|
// TestVerifyPQFinality_PulsarM_BitCountAlone_DoesNotPass is the
|
|
// canonical F109 anti-regression. The earlier form of this test wired
|
|
// an all-0xFF "signature" into the witness, but those bytes failed at
|
|
// the FIPS 204 decoder *before* the lattice verify equation ran — so a
|
|
// regression that broke only the verify equation could slip past. The
|
|
// stronger form below builds a VALID Pulsar-M signature over message M1
|
|
// and then mutates the witness to M2 (a different StateRoot) so the
|
|
// signed digest no longer matches the witness's SigningDigest(). A
|
|
// real lattice verifier MUST refuse; a bit-count tautology accepts.
|
|
func TestVerifyPQFinality_PulsarM_BitCountAlone_DoesNotPass(t *testing.T) {
|
|
params := pulsar.ParamsP65
|
|
priv, err := pulsar.GenerateKey(params, rand.Reader)
|
|
if err != nil {
|
|
t.Fatalf("pulsar.GenerateKey: %v", err)
|
|
}
|
|
|
|
w := NewVerkleWitness(1)
|
|
w.BindPQGroupKey(priv.Pub.Bytes)
|
|
|
|
// M1 — the witness we actually sign.
|
|
witness := &WitnessProof{
|
|
Commitment: make([]byte, 32),
|
|
Path: make([]byte, 16),
|
|
OpeningProof: make([]byte, 48),
|
|
CoronaBits: []byte{0xFF}, // 8 bits set — bit-count would pass at any threshold ≤ 8
|
|
BlockHeight: 100,
|
|
StateRoot: make([]byte, 32),
|
|
Timestamp: 1700000000,
|
|
}
|
|
for i := range witness.StateRoot {
|
|
witness.StateRoot[i] = byte(i)
|
|
}
|
|
|
|
digestM1 := witness.SigningDigest()
|
|
sig, err := pulsar.Sign(params, priv, digestM1[:], []byte(signingDigestCustomization), true, rand.Reader)
|
|
if err != nil {
|
|
t.Fatalf("pulsar.Sign: %v", err)
|
|
}
|
|
witness.PQSignature = sig.Bytes
|
|
|
|
// Swap the message: mutate StateRoot so SigningDigest now yields
|
|
// M2 ≠ M1. The signature still decodes (it's a genuine Pulsar-M
|
|
// signature), the bit-count pre-filter still passes (CoronaBits
|
|
// untouched), but the lattice verify equation MUST reject.
|
|
witness.StateRoot[0] ^= 0x01
|
|
digestM2 := witness.SigningDigest()
|
|
if digestM1 == digestM2 {
|
|
t.Fatal("test bug: StateRoot mutation did not change SigningDigest")
|
|
}
|
|
|
|
err = w.verifyPQFinality(witness, priv.Pub.Bytes, mldsa.MLDSA65)
|
|
if err == nil {
|
|
t.Fatal("verifyPQFinality accepted a valid signature over a different message: F109 regressed")
|
|
}
|
|
if !errors.Is(err, ErrPulsarVerifyFail) {
|
|
t.Fatalf("expected ErrPulsarVerifyFail, got: %v", err)
|
|
}
|
|
}
|
|
|
|
// TestVerifyBLSAggregate_RefusedUnderStrictPQ proves verifyBLSAggregate
|
|
// hard-refuses any call made under a strict-PQ profile, regardless of
|
|
// signature validity. Closes F107.
|
|
func TestVerifyBLSAggregate_RefusedUnderStrictPQ(t *testing.T) {
|
|
w := NewVerkleWitness(1)
|
|
w.SetProfile(config.StrictPQ())
|
|
|
|
// 48 bytes of zeros — would fail curve-point check anyway, but the
|
|
// strict-PQ refusal must fire FIRST (typed sentinel).
|
|
err := w.verifyBLSAggregate(make([]byte, 48), []byte("validators"))
|
|
if !errors.Is(err, ErrBLSForbiddenUnderStrictPQ) {
|
|
t.Fatalf("expected ErrBLSForbiddenUnderStrictPQ, got: %v", err)
|
|
}
|
|
}
|
|
|
|
// TestVerifyBLSAggregate_AllowedUnderLegacyNilProfile proves the BLS
|
|
// aggregate path remains callable on the pre-locked-profile legacy
|
|
// caller path (nil profile). The function still refuses garbage curve
|
|
// points; it just doesn't refuse on profile grounds.
|
|
//
|
|
// ForkClassicalCompatUnsafe is NOT in this test: even the fork profile
|
|
// pins ForbidPairings=true on the proof axis, which transitively forbids
|
|
// BLS aggregate verification. The only path that admits a BLS aggregate
|
|
// today is the truly-legacy "no profile bound" caller path retained for
|
|
// pre-locked-profile compatibility — and even there only as a stepping
|
|
// stone toward Pulsar-M everywhere.
|
|
func TestVerifyBLSAggregate_AllowedUnderLegacyNilProfile(t *testing.T) {
|
|
w := NewVerkleWitness(1) // profile remains nil
|
|
|
|
// Garbage bytes — must still fail the curve-point check, but with
|
|
// a different error than the strict-PQ sentinel.
|
|
err := w.verifyBLSAggregate(make([]byte, 48), []byte("validators"))
|
|
if err == nil {
|
|
t.Fatal("verifyBLSAggregate should reject zero-point bytes even on legacy path")
|
|
}
|
|
if errors.Is(err, ErrBLSForbiddenUnderStrictPQ) {
|
|
t.Fatalf("nil-profile legacy path must NOT raise the strict-PQ refusal; got: %v", err)
|
|
}
|
|
}
|