Files
corona/keyera/hashsuite_immut_test.go
zeekay f04e70924d keyera: v0.7.5 — public-BFT Bootstrap default closes last trusted-dealer dispatch
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).
2026-05-21 19:04:55 -07:00

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)
}
}