Files
corona/keyera/bootstrap_pedersen_test.go
zeekay 2cecc08c63 refactor(corona): delete the dkg2 package — superseded by luxfi/dkg vss
The Pedersen-VSS DKG share-dealing now runs entirely on github.com/luxfi/dkg
(proven byte-identical at cutover); corona's own copy is dead. Remove it and its
now-orphaned tooling, keeping only corona-specific code (Ringtail signing, β
rounding, Path (a) finalize — all in sign/, utils/, keyera/, untouched).

Deleted:
  - dkg2/ (the whole package: Round1/Round2/commits/complaint/GPU dispatch).
  - cmd/dkg2_oracle/ (the dkg2 KAT generator; the DKG KAT now lives in
    luxfi/dkg's own ring/vss vectors).
  - keyera/cutover_gate_test.go (the transitional dkg2-vs-vss equality gate; its
    job is done — permanent byte-stability is pinned by the golden KAT, and the
    PIN-4 convention assertion is preserved dkg2-free in pin4_convention_test.go).

Updated:
  - cmd/cross_runtime_oracle drops the dkg2 manifest leg (the luxcpp C++ dkg2
    cross-runtime gate migrates to luxfi/dkg separately — flagged, not silently
    broken).
  - keyera/reanchor doc comments + threshold bench comment point at luxfi/dkg.

go build ./... + go test ./... -count=1 green (13 ok / 0 fail), go vet + gofmt
clean. The golden byte-stability KAT + Bootstrap→sign→verify + dishonest-dealer
abort all still pass on the vss path.
2026-06-27 22:03:55 -07:00

426 lines
14 KiB
Go

// Copyright (C) 2025-2026, Lux Industries Inc. All rights reserved.
// See the file LICENSE for licensing terms.
package keyera
import (
"errors"
"reflect"
"testing"
"github.com/luxfi/corona/hash"
"github.com/luxfi/corona/sign"
"github.com/luxfi/corona/threshold"
dkgblame "github.com/luxfi/dkg/blame"
dkgring "github.com/luxfi/dkg/ring"
dkgvss "github.com/luxfi/dkg/vss"
)
// TestBootstrapPedersen_RoundTrip — 5 parties run Pedersen DKG with
// threshold t=3. The orchestrator returns a KeyEra with shares for
// every validator and a BootstrapTranscript suitable for chain commit.
//
// Acceptance criteria:
// - era.GroupKey is non-nil, A is the canonical dkg2 A
// (nothing-up-my-sleeve).
// - era.State.Shares has exactly n entries.
// - transcript.TranscriptHash is non-zero and stable across two runs
// with the same entropy stream.
// - Every party's SkShare has the expected (sign.N) dimension.
// - The era's threshold pins t.
func TestBootstrapPedersen_RoundTrip(t *testing.T) {
const thr, n = 3, 5
validators := []string{"v1", "v2", "v3", "v4", "v5"}
era, transcript, err := BootstrapPedersen(
hash.NewCoronaSHA3(),
thr, validators, 0, 0,
deterministicRand("bootstrap-pedersen-roundtrip"),
)
if err != nil {
t.Fatalf("BootstrapPedersen: %v", err)
}
if era == nil || era.GroupKey == nil {
t.Fatal("nil era / GroupKey")
}
if transcript == nil {
t.Fatal("nil transcript")
}
if got := len(era.State.Shares); got != n {
t.Fatalf("share count: want %d got %d", n, got)
}
if got := era.State.Threshold; got != thr {
t.Fatalf("threshold: want %d got %d", thr, got)
}
if era.HashSuiteID != hash.DefaultID {
t.Fatalf("suite: want %q got %q", hash.DefaultID, era.HashSuiteID)
}
if transcript.TranscriptHash == [32]byte{} {
t.Fatal("transcript hash is zero")
}
if len(transcript.BetaSerialized) != n {
t.Fatalf("transcript beta count: want %d got %d", n, len(transcript.BetaSerialized))
}
if len(transcript.Round1Digests) != n {
t.Fatalf("transcript digest count: want %d got %d", n, len(transcript.Round1Digests))
}
// Every share has the right dimension.
for v, ks := range era.State.Shares {
if len(ks.SkShare) != sign.N {
t.Fatalf("validator %s: SkShare dim %d, want %d", v, len(ks.SkShare), sign.N)
}
if ks.GroupKey != era.GroupKey {
t.Fatalf("validator %s: GroupKey pointer mismatch", v)
}
if ks.Lambda.Coeffs == nil {
t.Fatalf("validator %s: Lambda unset", v)
}
}
// Determinism: running again with the same entropy yields the same
// transcript hash, the same bTilde bytes, and the same digest set.
era2, transcript2, err := BootstrapPedersen(
hash.NewCoronaSHA3(),
thr, validators, 0, 0,
deterministicRand("bootstrap-pedersen-roundtrip"),
)
if err != nil {
t.Fatalf("BootstrapPedersen replay: %v", err)
}
if transcript.TranscriptHash != transcript2.TranscriptHash {
t.Fatal("transcript hash non-deterministic")
}
if !reflect.DeepEqual(transcript.Round1Digests, transcript2.Round1Digests) {
t.Fatal("Round1Digests non-deterministic")
}
if !reflect.DeepEqual(transcript.BTildeBytes, transcript2.BTildeBytes) {
t.Fatal("bTildeBytes non-deterministic")
}
// Different entropy → different transcript hash.
_, transcript3, err := BootstrapPedersen(
hash.NewCoronaSHA3(),
thr, validators, 0, 0,
deterministicRand("bootstrap-pedersen-different"),
)
if err != nil {
t.Fatalf("BootstrapPedersen alt: %v", err)
}
if transcript.TranscriptHash == transcript3.TranscriptHash {
t.Fatal("transcript hash collided across entropy streams")
}
// Sanity: the second era has a fresh GroupKey pointer (independent
// objects).
if era == era2 {
t.Fatal("expected distinct *KeyEra values across separate calls")
}
}
// TestBootstrapPedersen_DishonestDealer — one party broadcasts an
// inconsistent contribution (tampered share-to-0). The orchestrator
// returns ErrBootstrapPedersenAbort with an AbortEvidence naming the
// offending sender. The era and transcript are nil — the chain stays
// at the previous epoch.
func TestBootstrapPedersen_DishonestDealer(t *testing.T) {
const thr, n = 3, 5
validators := []string{"v1", "v2", "v3", "v4", "v5"}
suite := hash.NewCoronaSHA3()
// Run the share-dealing half (vss Round 1 + envelope opening) so we can
// tamper one opened share before the verify/aggregate round.
rng := deterministicRand("bootstrap-pedersen-dishonest")
seeds := make([][]byte, n)
for i := 0; i < n; i++ {
seeds[i] = make([]byte, sign.KeySize)
if _, err := rng.Read(seeds[i]); err != nil {
t.Fatalf("rng.Read[%d]: %v", i, err)
}
}
contribs, err := DealPedersen(suite, thr, validators, seeds)
if err != nil {
t.Fatalf("DealPedersen: %v", err)
}
// Tamper sender 2's share as recipient 0 opened it. Round 3 from the
// perspective of recipient 0 (processed first) must name sender 2 as the
// bad actor via the Pedersen identity.
contribs.inputs[0][2].Share[0].Coeffs[0][0] ^= 0x42
era, transcript, err := FinishBootstrapPedersen(suite, thr, validators, 0, 0, contribs)
if era != nil || transcript != nil {
t.Fatal("expected (nil, nil) on abort")
}
if err == nil {
t.Fatal("expected ErrBootstrapPedersenAbort, got nil")
}
if !errors.Is(err, ErrBootstrapPedersenAbort) {
t.Fatalf("expected ErrBootstrapPedersenAbort wrapping, got %v", err)
}
ev := ExtractAbortEvidence(err)
if ev == nil {
t.Fatal("AbortEvidence nil after ExtractAbortEvidence")
}
if _, ok := ev.Disqualified[2]; !ok {
t.Fatalf("expected sender 2 disqualified, disqualified=%v", ev.Disqualified)
}
if len(ev.Complaints) == 0 {
t.Fatal("expected at least one Complaint in evidence")
}
if ev.Complaints[0].Reason != dkgblame.ReasonBadDelivery {
t.Fatalf("complaint.Reason: want bad-delivery got %v", ev.Complaints[0].Reason)
}
if ev.Complaints[0].Accused != pedersenNodeID(2) {
t.Fatalf("complaint.Accused: want NodeID of dealer 2, got %x", ev.Complaints[0].Accused)
}
// Third-party re-check: the complaint's evidence confirms the share is
// malformed without trusting the accuser.
prof, err := dkgring.Ringtail()
if err != nil {
t.Fatalf("ring.Ringtail: %v", err)
}
justified, err := dkgvss.RecheckBadDelivery(prof, ev.Complaints[0])
if err != nil {
t.Fatalf("RecheckBadDelivery: %v", err)
}
if !justified {
t.Fatal("RecheckBadDelivery says the accusation is unjustified — tamper not detected")
}
// The transcript hash on the evidence is non-zero — chain can
// commit the abort transcript and stay at previous epoch.
if ev.TranscriptHash == [32]byte{} {
t.Fatal("AbortEvidence.TranscriptHash is zero")
}
}
// TestBootstrapPedersen_FollowedBySign — full integration. Run a
// Pedersen bootstrap with no trusted dealer, then drive the standard
// Corona threshold-sign 2-round protocol under the produced GroupKey.
// The resulting signature must verify under threshold.Verify.
//
// This is the binding contract: the noise-flooded GroupKey (A, bTilde)
// produced by Path (a) is structurally identical to a trusted-dealer
// Corona setup, so the existing Sign/Verify path accepts it unchanged.
func TestBootstrapPedersen_FollowedBySign(t *testing.T) {
const thr, n = 4, 5
validators := []string{"v1", "v2", "v3", "v4", "v5"}
era, _, err := BootstrapPedersen(
hash.NewCoronaSHA3(),
thr, validators, 0, 0,
deterministicRand("bootstrap-pedersen-sign"),
)
if err != nil {
t.Fatalf("BootstrapPedersen: %v", err)
}
if !signAndVerify(t, era, validators) {
t.Fatal("noise-flooded GroupKey did not accept threshold-signed message")
}
}
// TestBootstrapPedersen_NoMasterSecretInMemory — defense-in-depth
// statement: at no point during the bootstrap ceremony does any party
// (here represented by any kernel-resident dkg2.DKGSession) hold the
// reconstructed master secret s.
//
// We assert this structurally: the dkg2.DKGSession exposes no field or
// method that returns the master secret; the orchestrator never calls
// any reconstruction primitive; and the per-party SkShare values are
// independent shares (every pair differs).
//
// This is necessarily a structural argument, not a runtime one — the Go
// runtime cannot prove the absence of a value across all paths. The
// public-BFT-safe property is established by review + this structural
// check, and ultimately by the Pedersen-DKG security proof (hiding
// under MLWE on the wide concatenation [A|B]) cited in
// papers/lp-073-pulsar §07.
func TestBootstrapPedersen_NoMasterSecretInMemory(t *testing.T) {
const thr, n = 3, 5
validators := []string{"v1", "v2", "v3", "v4", "v5"}
era, _, err := BootstrapPedersen(
hash.NewCoronaSHA3(),
thr, validators, 0, 0,
deterministicRand("bootstrap-pedersen-no-master"),
)
if err != nil {
t.Fatalf("BootstrapPedersen: %v", err)
}
// Structural assertion 1: the vss.Party exposes no "master secret"
// accessor (no-reconstruct: a party retains only its own aggregated share).
dkgType := reflect.TypeOf((*dkgvss.Party)(nil)).Elem()
for i := 0; i < dkgType.NumField(); i++ {
f := dkgType.Field(i)
// Reject any field whose lower-cased name contains the substring
// "mastersecret" or "masters".
lower := lowercase(f.Name)
if contains(lower, "mastersecret") || contains(lower, "fullsecret") {
t.Fatalf("dkgvss.Party has a forbidden field %q — master-secret state leaks", f.Name)
}
}
// Structural assertion 2: every distinct pair of party SkShares
// disagrees on at least one coefficient. If two shares were equal
// to the reconstructed master secret (e.g. via a misbehaving
// orchestrator that wrote s into every party's share slot), they
// would collide on every coefficient and we would catch it here.
for vA, ksA := range era.State.Shares {
for vB, ksB := range era.State.Shares {
if vA >= vB { // canonicalise to one direction
continue
}
if sharesEqual(ksA, ksB) {
t.Fatalf("validators %s and %s share identical secret bytes — orchestrator leaked master secret", vA, vB)
}
}
}
// Structural assertion 3: every party's Lambda is distinct (Lagrange
// coefficients at distinct evaluation points cannot collide for
// degree ≥ 1 polynomials over Z_q with n ≥ 2).
lambdaSeen := make(map[uint64]string)
for vName, ks := range era.State.Shares {
key := ks.Lambda.Coeffs[0][0]
if prev, ok := lambdaSeen[key]; ok && prev != vName {
t.Fatalf("validators %s and %s share identical Lambda[0][0] — Lagrange weights degenerate", prev, vName)
}
lambdaSeen[key] = vName
}
}
// TestBootstrapPedersen_ParameterValidation — input bounds checking.
func TestBootstrapPedersen_ParameterValidation(t *testing.T) {
cases := []struct {
name string
t, n int
validators []string
want error
}{
{"empty validators", 1, 0, nil, ErrEmptyValidators},
{"t < 1", 0, 3, []string{"a", "b", "c"}, ErrInvalidThreshold},
{"t > n", 4, 3, []string{"a", "b", "c"}, ErrInvalidThreshold},
{"t == n", 3, 3, []string{"a", "b", "c"}, ErrBootstrapPedersenShape},
{"n == 1", 1, 1, []string{"a"}, ErrBootstrapPedersenShape},
}
for _, c := range cases {
t.Run(c.name, func(t *testing.T) {
_, _, err := BootstrapPedersen(
hash.NewCoronaSHA3(),
c.t, c.validators, 0, 0,
deterministicRand("validation"),
)
if !errors.Is(err, c.want) {
t.Fatalf("expected %v, got %v", c.want, err)
}
})
}
}
// TestBootstrapPedersen_DefaultSuite — nil suite resolves to the
// production default (Corona-SHA3) and produces a working era.
func TestBootstrapPedersen_DefaultSuite(t *testing.T) {
const thr, n = 2, 3
validators := []string{"a", "b", "c"}
era, _, err := BootstrapPedersen(
nil, // → Corona-SHA3
thr, validators, 0, 0,
deterministicRand("default-suite"),
)
if err != nil {
t.Fatalf("BootstrapPedersen(nil suite): %v", err)
}
if era.HashSuiteID != hash.DefaultID {
t.Fatalf("HashSuiteID: want %q got %q", hash.DefaultID, era.HashSuiteID)
}
}
// TestBootstrapTrustedDealer_StandalonePath — the BootstrapTrustedDealer
// alias is the only entrypoint that still routes through the legacy
// single-dealer Shamir share-out (for HSM/TEE ceremony scenarios). Since
// v0.7.5 the unqualified Bootstrap defaults to Pedersen-DKG, so the two
// paths are NOT byte-equivalent any more. This test pins that the
// trusted-dealer alias remains usable (same n, same validator set,
// same threshold) for the t == n case which the Pedersen path
// structurally rejects.
func TestBootstrapTrustedDealer_StandalonePath(t *testing.T) {
const thr, n = 3, 3
validators := []string{"x", "y", "z"}
era, err := BootstrapTrustedDealer(thr, validators, 0, 0, deterministicRand("trusted-dealer"))
if err != nil {
t.Fatalf("BootstrapTrustedDealer: %v", err)
}
if era.GroupKey == nil {
t.Fatal("BootstrapTrustedDealer returned nil GroupKey")
}
if got := len(era.State.Validators); got != n {
t.Fatalf("validator count: want %d got %d", n, got)
}
if got := era.State.Threshold; got != thr {
t.Fatalf("threshold: want %d got %d", thr, got)
}
// The unqualified Bootstrap (Pedersen-DKG path) refuses t == n
// because identifiable-abort detection requires at least one honest
// non-sender per round. Confirm the dispatch.
if _, _, err := Bootstrap(thr, validators, 0, 0, deterministicRand("public-bft")); err == nil {
t.Fatal("Bootstrap with t == n should fail under Pedersen-DKG (t < n required)")
}
}
// ---------------------- helpers ----------------------
// sharesEqual returns true iff two KeyShares carry byte-identical
// SkShare coefficients on every polynomial slot.
func sharesEqual(a, b *threshold.KeyShare) bool {
if len(a.SkShare) != len(b.SkShare) {
return false
}
for i := range a.SkShare {
if a.SkShare[i].N() != b.SkShare[i].N() {
return false
}
for level := range a.SkShare[i].Coeffs {
if len(a.SkShare[i].Coeffs[level]) != len(b.SkShare[i].Coeffs[level]) {
return false
}
for k := range a.SkShare[i].Coeffs[level] {
if a.SkShare[i].Coeffs[level][k] != b.SkShare[i].Coeffs[level][k] {
return false
}
}
}
}
return true
}
// contains is a trivial substring test that avoids pulling in strings.
func contains(haystack, needle string) bool {
if len(needle) == 0 {
return true
}
for i := 0; i+len(needle) <= len(haystack); i++ {
if haystack[i:i+len(needle)] == needle {
return true
}
}
return false
}
// lowercase returns a lowercase copy of s. Avoid strings.ToLower to keep
// the import surface clean.
func lowercase(s string) string {
b := make([]byte, len(s))
for i := 0; i < len(s); i++ {
c := s[i]
if c >= 'A' && c <= 'Z' {
c += 32
}
b[i] = c
}
return string(b)
}