mirror of
https://github.com/luxfi/corona.git
synced 2026-09-24 00:25:54 +00:00
Final public-BFT audit (per AUDIT-2026-05.md §10) flagged the v0.7.3 asymmetry where keyera.Bootstrap / keyera.BootstrapWithSuite still routed through the legacy single-dealer Shamir share-out while keyera.Reanchor (v0.7.4) had already flipped to Pedersen-DKG. Callers picking the "obvious" name silently inherited the dealer caveat at chain genesis. v0.7.5 flips both Bootstrap defaults to route through BootstrapPedersen (Pedersen-DKG over R_q + Path (a) noise flooding) so no party ever holds the master secret s at any point in the ceremony. The legacy trusted-dealer ceremony path is reachable only via the explicit BootstrapTrustedDealer / BootstrapTrustedDealerWithSuite / ReanchorTrustedDealer / ReanchorTrustedDealerWithSuite names. The shared trusted-dealer body lives in the unexported bootstrapTrustedDealerImpl, so the public-facing aliases (and the Reanchor trusted-dealer alias) cannot drift from each other. Structural invariant: BootstrapPedersen requires n >= 2 && t < n. Deployments that need every validator to sign (t == n) must select BootstrapTrustedDealer explicitly; the unqualified Bootstrap will fail with ErrBootstrapPedersenShape on t == n, surfacing the trust-model decision at the call site rather than silently routing to the dealer path. Tests updated: keyera_test.go, hashsuite_immut_test.go, bootstrap_pedersen_test.go, reanchor_pedersen_test.go — drop t == n from the public-BFT cases (now use t = n - 1) and propagate the 3-tuple return from the new Bootstrap signature. Cross-runtime KAT byte-equality: scripts/regen-kats.sh updated for the post-Corona purge (LUXCPP path now crypto/corona, not the stale crypto/pulsar); reshare_oracle / activation_oracle / dkg2_oracle accept both CORONA_*_KAT_PATH and PULSAR_*_KAT_PATH env overrides for compatibility with the existing script. Manifest regenerated and verified. gpu/gpu_test.go: skip on non-GPU builds rather than fail (RegisterRing returns "GPU unavailable" without a CGO Metal/CUDA backend; the rest of the audit suite is exercised by the other package tests).
227 lines
7.5 KiB
Go
227 lines
7.5 KiB
Go
// Copyright (C) 2025-2026, Lux Industries Inc. All rights reserved.
|
|
// See the file LICENSE for licensing terms.
|
|
|
|
// Package keyera — HashSuite immutability tests (Gate 3 of the
|
|
// Mar-3-2026 PQ Consensus Architecture Freeze).
|
|
//
|
|
// These tests pin the era-pinning property of the HashSuite (per
|
|
// proofs/pulsar/hash-suite-separation.tex Remark on era-pinning):
|
|
//
|
|
// A. Bootstrap pins suite. The HashSuiteID recorded on a freshly
|
|
// bootstrapped era equals exactly the ID of the supplied
|
|
// HashSuite, and the field is read-only on the returned KeyEra.
|
|
// B. Reshare cannot change suite. The Reshare API does not accept a
|
|
// HashSuite parameter (verified at the type level by reading
|
|
// reshare API), and the propagated state's HashSuiteID equals the
|
|
// era's HashSuiteID byte-for-byte.
|
|
// C. Reanchor MAY change suite. ReanchorWithSuite opens a new era
|
|
// with a fresh GroupKey and a fresh HashSuiteID; the prior era's
|
|
// HashSuiteID is unchanged.
|
|
//
|
|
// Citations (canonical proof bucket):
|
|
//
|
|
// proofs/definitions/transcript-binding.tex
|
|
// Definition ref:pulsar-transcript
|
|
// proofs/pulsar/hash-suite-separation.tex
|
|
// Theorem ref:hash-suite-separation
|
|
package keyera
|
|
|
|
import (
|
|
"reflect"
|
|
"testing"
|
|
|
|
"github.com/luxfi/corona/hash"
|
|
)
|
|
|
|
// TestBootstrapPinsSuiteSHA3 — Gate 3A: Bootstrap with Corona-SHA3 →
|
|
// era.HashSuiteID == "Corona-SHA3".
|
|
func TestBootstrapPinsSuiteSHA3(t *testing.T) {
|
|
era, _, err := BootstrapWithSuite(
|
|
hash.NewCoronaSHA3(),
|
|
2,
|
|
[]string{"a", "b", "c"},
|
|
0, 0,
|
|
deterministicRand("hashsuite-immut-sha3"),
|
|
)
|
|
if err != nil {
|
|
t.Fatalf("BootstrapWithSuite: %v", err)
|
|
}
|
|
if era.HashSuiteID != hash.DefaultID {
|
|
t.Fatalf("era.HashSuiteID: want %q got %q", hash.DefaultID, era.HashSuiteID)
|
|
}
|
|
if got := era.State.HashSuiteID; got != hash.DefaultID {
|
|
t.Fatalf("state.HashSuiteID: want %q got %q", hash.DefaultID, got)
|
|
}
|
|
}
|
|
|
|
// TestBootstrapPinsSuiteBLAKE3 — Gate 3A: Bootstrap with Corona-BLAKE3
|
|
// → era.HashSuiteID == "Corona-BLAKE3".
|
|
func TestBootstrapPinsSuiteBLAKE3(t *testing.T) {
|
|
era, _, err := BootstrapWithSuite(
|
|
hash.NewCoronaBLAKE3(),
|
|
2,
|
|
[]string{"a", "b", "c"},
|
|
0, 0,
|
|
deterministicRand("hashsuite-immut-blake3"),
|
|
)
|
|
if err != nil {
|
|
t.Fatalf("BootstrapWithSuite: %v", err)
|
|
}
|
|
if era.HashSuiteID != hash.LegacyBLAKE3ID {
|
|
t.Fatalf("era.HashSuiteID: want %q got %q", hash.LegacyBLAKE3ID, era.HashSuiteID)
|
|
}
|
|
if got := era.State.HashSuiteID; got != hash.LegacyBLAKE3ID {
|
|
t.Fatalf("state.HashSuiteID: want %q got %q", hash.LegacyBLAKE3ID, got)
|
|
}
|
|
}
|
|
|
|
// TestBootstrapDefaultsToSHA3 confirms the no-suite Bootstrap entrypoint
|
|
// pins the production default, so legacy callers cannot accidentally
|
|
// open an era under no profile at all.
|
|
func TestBootstrapDefaultsToSHA3(t *testing.T) {
|
|
era, _, err := Bootstrap(2, []string{"a", "b", "c"}, 0, 0,
|
|
deterministicRand("hashsuite-default"))
|
|
if err != nil {
|
|
t.Fatalf("Bootstrap: %v", err)
|
|
}
|
|
if era.HashSuiteID != hash.DefaultID {
|
|
t.Fatalf("default suite: want %q got %q", hash.DefaultID, era.HashSuiteID)
|
|
}
|
|
}
|
|
|
|
// TestReshareCannotChangeSuiteSHA3 — Gate 3B: Reshare on a Corona-SHA3
|
|
// era yields a state with HashSuiteID == "Corona-SHA3" (unchanged).
|
|
func TestReshareCannotChangeSuiteSHA3(t *testing.T) {
|
|
era, _, err := BootstrapWithSuite(
|
|
hash.NewCoronaSHA3(),
|
|
2,
|
|
[]string{"v1", "v2", "v3"},
|
|
0, 0,
|
|
deterministicRand("reshare-cannot-change-sha3"),
|
|
)
|
|
if err != nil {
|
|
t.Fatalf("BootstrapWithSuite: %v", err)
|
|
}
|
|
priorID := era.HashSuiteID
|
|
|
|
next, err := era.Reshare([]string{"v1", "v2", "v3"}, 2, deterministicRand("reshare-1"))
|
|
if err != nil {
|
|
t.Fatalf("Reshare: %v", err)
|
|
}
|
|
if era.HashSuiteID != priorID {
|
|
t.Fatalf("era.HashSuiteID changed across Reshare: was %q now %q", priorID, era.HashSuiteID)
|
|
}
|
|
if next.HashSuiteID != priorID {
|
|
t.Fatalf("post-Reshare state.HashSuiteID: want %q got %q", priorID, next.HashSuiteID)
|
|
}
|
|
}
|
|
|
|
// TestReshareCannotChangeSuiteBLAKE3 — Gate 3B mirrored on the legacy
|
|
// profile. A BLAKE3-pinned era stays BLAKE3 across Reshare.
|
|
func TestReshareCannotChangeSuiteBLAKE3(t *testing.T) {
|
|
era, _, err := BootstrapWithSuite(
|
|
hash.NewCoronaBLAKE3(),
|
|
2,
|
|
[]string{"v1", "v2", "v3"},
|
|
0, 0,
|
|
deterministicRand("reshare-cannot-change-blake3"),
|
|
)
|
|
if err != nil {
|
|
t.Fatalf("BootstrapWithSuite: %v", err)
|
|
}
|
|
priorID := era.HashSuiteID
|
|
if priorID != hash.LegacyBLAKE3ID {
|
|
t.Fatalf("setup: era.HashSuiteID want %q got %q", hash.LegacyBLAKE3ID, priorID)
|
|
}
|
|
|
|
next, err := era.Reshare([]string{"v4", "v5", "v6"}, 2, deterministicRand("reshare-blake3"))
|
|
if err != nil {
|
|
t.Fatalf("Reshare: %v", err)
|
|
}
|
|
if era.HashSuiteID != priorID {
|
|
t.Fatalf("era.HashSuiteID changed across Reshare: was %q now %q", priorID, era.HashSuiteID)
|
|
}
|
|
if next.HashSuiteID != priorID {
|
|
t.Fatalf("post-Reshare state.HashSuiteID: want %q got %q", priorID, next.HashSuiteID)
|
|
}
|
|
}
|
|
|
|
// TestReshareAPIHasNoHashSuiteParameter is the type-level pin: the
|
|
// Reshare method's signature MUST NOT accept a hash.HashSuite, so it
|
|
// is impossible to change the suite at the call site. We use
|
|
// reflection on the method type to assert no parameter is assignable
|
|
// to hash.HashSuite.
|
|
func TestReshareAPIHasNoHashSuiteParameter(t *testing.T) {
|
|
era := &KeyEra{}
|
|
rt := reflect.ValueOf(era).MethodByName("Reshare").Type()
|
|
hashSuiteIface := reflect.TypeOf((*hash.HashSuite)(nil)).Elem()
|
|
for i := 0; i < rt.NumIn(); i++ {
|
|
in := rt.In(i)
|
|
// Only check exact match against the HashSuite interface or a
|
|
// type that explicitly implements it. (Reshare takes
|
|
// io.Reader; that is unrelated.)
|
|
if in == hashSuiteIface {
|
|
t.Fatalf("Reshare param %d is hash.HashSuite — reshare must not accept a suite", i)
|
|
}
|
|
if in.Kind() == reflect.Interface && in.Implements(hashSuiteIface) {
|
|
t.Fatalf("Reshare param %d implements hash.HashSuite — reshare must not accept a suite", i)
|
|
}
|
|
}
|
|
}
|
|
|
|
// TestReanchorMayChangeSuite — Gate 3C: ReanchorWithSuite from a
|
|
// Corona-SHA3 era to a Corona-BLAKE3 era yields era_2.HashSuiteID ==
|
|
// "Corona-BLAKE3", and era_1 is unchanged.
|
|
//
|
|
// The new committee uses t=2, n=3 because the v0.7.4 Reanchor routes
|
|
// through Pedersen-DKG which requires t < n (strictly less than) for
|
|
// identifiable-abort detection. The legacy t == n equivalence stays
|
|
// available under ReanchorTrustedDealer for HSM-ceremony scenarios.
|
|
func TestReanchorMayChangeSuite(t *testing.T) {
|
|
era1, _, err := BootstrapWithSuite(
|
|
hash.NewCoronaSHA3(),
|
|
2,
|
|
[]string{"a", "b", "c"},
|
|
0, 1,
|
|
deterministicRand("reanchor-era-1"),
|
|
)
|
|
if err != nil {
|
|
t.Fatalf("BootstrapWithSuite: %v", err)
|
|
}
|
|
if era1.HashSuiteID != hash.DefaultID {
|
|
t.Fatalf("era1.HashSuiteID: want %q got %q", hash.DefaultID, era1.HashSuiteID)
|
|
}
|
|
|
|
era2, transcript, err := ReanchorWithSuite(
|
|
era1,
|
|
hash.NewCoronaBLAKE3(),
|
|
2,
|
|
[]string{"d", "e", "f"},
|
|
0,
|
|
deterministicRand("reanchor-era-2"),
|
|
)
|
|
if err != nil {
|
|
t.Fatalf("ReanchorWithSuite: %v", err)
|
|
}
|
|
if transcript == nil {
|
|
t.Fatal("ReanchorWithSuite returned nil transcript")
|
|
}
|
|
if era2.HashSuiteID != hash.LegacyBLAKE3ID {
|
|
t.Fatalf("era2.HashSuiteID: want %q got %q", hash.LegacyBLAKE3ID, era2.HashSuiteID)
|
|
}
|
|
if era1.HashSuiteID != hash.DefaultID {
|
|
t.Fatalf("era1.HashSuiteID mutated by Reanchor: want %q got %q",
|
|
hash.DefaultID, era1.HashSuiteID)
|
|
}
|
|
if era1.GroupKey == era2.GroupKey {
|
|
t.Fatal("Reanchor returned the same GroupKey pointer; expected fresh key")
|
|
}
|
|
if era2.State.HashSuiteID != hash.LegacyBLAKE3ID {
|
|
t.Fatalf("era2.State.HashSuiteID: want %q got %q",
|
|
hash.LegacyBLAKE3ID, era2.State.HashSuiteID)
|
|
}
|
|
if era2.EraID != era1.EraID+1 {
|
|
t.Fatalf("era2.EraID: want %d got %d", era1.EraID+1, era2.EraID)
|
|
}
|
|
}
|