Files
warp/pulsar/pulsar_test.go
zeekay 443aa53f81 feat(warp)!: typed finality-evidence model (Beam/Pulsar/Corona/P3Q) + Corona relabel
Decomplect the PQ finality lanes into a typed, impossible-to-confuse model where
KIND chooses the verifier and SUITE only chooses parameterization (fail-closed on
mismatch). Kills the prior conflation where the corona-backed lattice lane was
mislabeled "Pulse/Pulsar-SHA3".

Four strict evidence kinds — each its own verifier + suite + key registry:
  beam-bls                BLS12-381 aggregate (fast classical)
  pulsar-threshold-mldsa  threshold ML-DSA, verified as a STANDARD FIPS-204
                          ML-DSA signature under a group public key; the TALUS
                          dealerless machinery is entirely offline (VerifyPulsar
                          imports only mldsa65 — enforced by an import-hygiene test)
  corona-ringtail         Ringtail/Module-LWE lattice threshold (the corona kernel)
  p3q-mldsa-rollup        succinct rollup of independent ML-DSA-65 sigs; strict
                          ONLY when its proof system is PQ (classical SNARK gated)
Plus the raw mldsa-cert-set as a NON-strict availability/audit artifact.

Subject-agnostic verifiers take (subject, evidence, keyEra): one dispatch serves
the warp cross-chain digest D and the consensus finality subject
  M = keccak256("QUASAR_FINALITY_V1" || chainID || height || round || blockID ||
      stateRoot || signerSetID || keyEraID || generation || pChainHeight || policyID)
folding generation + pChainHeight + policyID so a signature binds its era, its
validator snapshot, and its tier (tier-downgrade replay fails closed).

Policy tiers (BLS_FAST / HYBRID_PQ_CHECKPOINT / STRICT_QUASAR / RECOVERY) select
which lanes are required — orthogonal to verification and to keys. PulsarKeyEra
and CoronaKeyEra are symmetric but DISTINCT registry types; P3Q/cert-set resolve a
per-validator SignerSetAuthority — no resolver can alias another's key material.
Offline signers (pulsard, coronad, P3Q prover) are interfaces only, never imported
by the verify path.

Wire unchanged: D, the Message c14n, messageDST, the ZAP magic and kind bytes are
byte-identical (forge Go<->Solidity equality holds; golden envelope SHA unchanged).
The corona lane DST is renamed pulseDST->coronaDST (no deployed corona sigs;
envelope KAT regenerated).

22 tests green: non-confusable dispatch, suite/kind fail-closed, Pulsar import
hygiene, subject-identity across lanes, policyID downgrade, key-era distinctness,
P3Q-classical-root strict gate.
2026-06-27 13:47:13 -07:00

227 lines
7.8 KiB
Go

// Copyright (C) 2019-2026, Lux Industries Inc. All rights reserved.
// See the file LICENSE for licensing terms.
package pulsar
import (
"bytes"
"crypto/rand"
"errors"
"testing"
"github.com/luxfi/crypto/bls"
"github.com/luxfi/ids"
"github.com/luxfi/warp"
"github.com/stretchr/testify/require"
corona "github.com/luxfi/corona/threshold"
)
// runPulsarCeremony runs a full t-of-n threshold signing ceremony against
// the given message and returns the Signature plus the GroupKey.
func runPulsarCeremony(t *testing.T, n, threshold int, message string) (*corona.Signature, *corona.GroupKey) {
t.Helper()
shares, gk, err := corona.GenerateKeys(threshold, n, rand.Reader)
require.NoError(t, err)
require.Len(t, shares, n)
signers := make([]int, n)
for i := range signers {
signers[i] = i
}
prfKey := make([]byte, 32)
_, err = rand.Read(prfKey)
require.NoError(t, err)
parties := make([]*corona.Signer, n)
for i := range parties {
parties[i] = corona.NewSigner(shares[i])
}
r1 := make(map[int]*corona.Round1Data, n)
for i, p := range parties {
r1[i] = p.Round1(1, prfKey, signers)
}
r2 := make(map[int]*corona.Round2Data, n)
for i, p := range parties {
d, err := p.Round2(1, message, prfKey, signers, r1)
require.NoError(t, err)
r2[i] = d
}
sig, err := parties[0].Finalize(r2)
require.NoError(t, err)
require.True(t, corona.Verify(gk, message, sig))
return sig, gk
}
// envFixture builds a Envelope with the given Pulsar lineage.
func envFixture(t *testing.T, eraID, generation uint64) *warp.Envelope {
t.Helper()
message := &warp.Message{
NetworkID: 1,
SourceChainID: ids.ID{0xC1, 0xC2, 0xC3, 0xC4},
SourceKeyEraID: eraID,
SourceGeneration: generation,
HashSuiteID: warp.MessageHashProfileTag,
Payload: []byte("warp-pulsar-roundtrip"),
}
signers := warp.NewBitSet()
signers.Add(0)
signers.Add(2)
var sigBytes [bls.SignatureLen]byte
copy(sigBytes[:], bytes.Repeat([]byte{0xAB}, bls.SignatureLen))
env, err := warp.NewEnvelope(message, warp.NewBitSetSignature(signers, sigBytes), nil, nil)
require.NoError(t, err)
return env
}
type stubResolver struct {
gk *corona.GroupKey
suiteID string
err error
}
func (s *stubResolver) ResolveGroupKey(_ [32]byte, _ uint64, _ uint64) (*corona.GroupKey, string, error) {
return s.gk, s.suiteID, s.err
}
// coronaInputs extracts the (subject, CoronaEvidence) pair for an envelope's
// corona lane: subject = D, evidence = routing + serialized signature. This
// mirrors exactly what the warp relay path (verifyPQLanes) hands to the
// subject-agnostic Corona verifier.
func coronaInputs(env *warp.Envelope) ([]byte, warp.CoronaEvidence) {
id := env.Message.ID()
return id[:], warp.CoronaEvidence{
ChainID: env.Message.SourceChainID,
KeyEraID: env.Message.SourceKeyEraID,
Generation: env.Message.SourceGeneration,
Sig: env.CoronaSig,
}
}
func TestSerializeDeserializeCoronaSigRoundTrip(t *testing.T) {
sig, gk := runPulsarCeremony(t, 3, 2, "round-trip")
wire, err := SerializeCoronaSig(sig)
require.NoError(t, err)
require.NotEmpty(t, wire)
parsed, err := DeserializeCoronaSig(wire)
require.NoError(t, err)
require.True(t, corona.Verify(gk, "round-trip", parsed))
}
func TestRingtailVerifierAcceptsValidPulse(t *testing.T) {
env := envFixture(t, 7, 11)
signing := warp.CoronaSigningBytes(env.Message.ID())
sig, gk := runPulsarCeremony(t, 3, 2, string(signing))
pulse, err := SerializeCoronaSig(sig)
require.NoError(t, err)
env.CoronaSig = pulse
v := NewRingtailVerifier(&stubResolver{gk: gk, suiteID: string(warp.DefaultCoronaSuiteID)})
require.NoError(t, v.VerifyRingtailThreshold(coronaInputs(env)))
}
func TestRingtailVerifierRejectsTamperedEnvelopeFields(t *testing.T) {
env := envFixture(t, 7, 11)
signing := warp.CoronaSigningBytes(env.Message.ID())
sig, gk := runPulsarCeremony(t, 3, 2, string(signing))
pulse, err := SerializeCoronaSig(sig)
require.NoError(t, err)
env.CoronaSig = pulse
// Tamper with KeyEraID after signing — D changes, so the corona sig over
// the original D no longer verifies. (The Beam now also binds this
// field via D, closing the old Beam-unauthenticated-lineage gap.)
env.Message.SourceKeyEraID = 999
v := NewRingtailVerifier(&stubResolver{gk: gk, suiteID: string(warp.DefaultCoronaSuiteID)})
require.ErrorIs(t, v.VerifyRingtailThreshold(coronaInputs(env)), ErrCoronaVerifyFailed)
}
func TestRingtailVerifierRejectsOnAbsentPulse(t *testing.T) {
env := envFixture(t, 1, 1)
v := NewRingtailVerifier(&stubResolver{suiteID: string(warp.DefaultCoronaSuiteID)})
require.ErrorIs(t, v.VerifyRingtailThreshold(coronaInputs(env)), ErrCoronaAbsent)
}
func TestRingtailVerifierRejectsResolverError(t *testing.T) {
env := envFixture(t, 1, 1)
env.CoronaSig = bytes.Repeat([]byte{0xFF}, 32)
v := NewRingtailVerifier(&stubResolver{err: errors.New("registry down")})
require.ErrorIs(t, v.VerifyRingtailThreshold(coronaInputs(env)), ErrCoronaGroupKeyResolverFailed)
}
func TestRingtailVerifierRejectsNilGroupKey(t *testing.T) {
env := envFixture(t, 1, 1)
env.CoronaSig = bytes.Repeat([]byte{0xFF}, 32)
v := NewRingtailVerifier(&stubResolver{gk: nil, suiteID: string(warp.DefaultCoronaSuiteID)})
require.ErrorIs(t, v.VerifyRingtailThreshold(coronaInputs(env)), ErrCoronaGroupKeyResolverFailed)
}
func TestRingtailVerifierRejectsSuiteMismatch(t *testing.T) {
env := envFixture(t, 1, 1)
env.CoronaSig = bytes.Repeat([]byte{0xFF}, 32)
_, gk := runPulsarCeremony(t, 3, 2, "x")
v := NewRingtailVerifier(&stubResolver{gk: gk, suiteID: "Pulsar-SHA3-experimental"})
require.ErrorIs(t, v.VerifyRingtailThreshold(coronaInputs(env)), ErrCoronaSuiteMismatch)
}
// TestCoronaSigningBytesBindsAllTranscriptFields proves the Corona subject
// (CoronaSigningBytes(D)) changes when ANY transcript field changes —
// because D folds them all.
func TestCoronaSigningBytesBindsAllTranscriptFields(t *testing.T) {
env := envFixture(t, 7, 11)
env.Message.SourceNebulaRoot = [32]byte{0x01, 0x02}
base := warp.CoronaSigningBytes(env.Message.ID())
mutate := func(f func(c *warp.Message)) []byte {
c := env.Message
f(&c)
return warp.CoronaSigningBytes(c.ID())
}
require.NotEqual(t, base, mutate(func(c *warp.Message) { c.SourceKeyEraID = 8 }))
require.NotEqual(t, base, mutate(func(c *warp.Message) { c.SourceGeneration = 12 }))
require.NotEqual(t, base, mutate(func(c *warp.Message) { c.SourceNebulaRoot = [32]byte{0x99} }))
require.NotEqual(t, base, mutate(func(c *warp.Message) { c.HashSuiteID = "Pulsar-SHA3-other" }))
require.NotEqual(t, base, mutate(func(c *warp.Message) { c.Payload = append([]byte("X"), c.Payload...) }))
}
// TestCoronaSigningBytesPrefix proves the Corona subject carries the
// CORONA domain-separation tag.
func TestCoronaSigningBytesPrefix(t *testing.T) {
env := envFixture(t, 1, 1)
out := warp.CoronaSigningBytes(env.Message.ID())
require.True(t, bytes.HasPrefix(out, []byte("LUX-WARP-ZAP-CORONA-v1")))
d := env.Message.ID()
require.True(t, bytes.HasSuffix(out, d[:]))
}
func TestHorizonFromEnvelopePopulatesAllLanes(t *testing.T) {
env := envFixture(t, 7, 11)
env.Message.SourceNebulaRoot = [32]byte{0x42}
env.CoronaSig = bytes.Repeat([]byte{0xCC}, 16)
env.MLDSACertSet = bytes.Repeat([]byte{0xDD}, 192)
h, err := HorizonFromEnvelope(env)
require.NoError(t, err)
require.Equal(t, env.Message.SourceChainID[:], h.SourceChainID[:])
require.Equal(t, env.Beam.Signature[:], h.Beam)
require.Equal(t, env.CoronaSig, h.CoronaRingtail)
require.Equal(t, env.MLDSACertSet, h.MLDSACertSet)
require.Equal(t, env.Message.SourceNebulaRoot, h.SourceNebulaRoot)
require.Equal(t, env.Message.SourceKeyEraID, h.SourceKeyEraID)
require.Equal(t, env.Message.SourceGeneration, h.SourceGeneration)
require.Equal(t, env.Message.HashSuiteOrDefault(), h.HashSuiteID)
require.Equal(t, env.Message.Bytes(), h.UnsignedMessageBytes)
}
func TestHorizonFromEnvelopeRejectsNil(t *testing.T) {
_, err := HorizonFromEnvelope(nil)
require.Error(t, err)
}