mirror of
https://github.com/luxfi/warp.git
synced 2026-10-30 15:13:04 +00:00
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.
164 lines
4.6 KiB
Go
164 lines
4.6 KiB
Go
// Copyright (C) 2019-2026, Lux Industries Inc. All rights reserved.
|
|
// See the file LICENSE for licensing terms.
|
|
|
|
// Warp pulse fuzz harness.
|
|
//
|
|
// Property anchor: proofs/quasar/warp-pq-soundness.tex
|
|
// Theorem ref:warp-v2-soundness.
|
|
//
|
|
// Properties under fuzzing:
|
|
//
|
|
// 1. SerializeCoronaSig(sig) ↔ DeserializeCoronaSig(bytes) round-trips on
|
|
// valid signatures (kernel-generated).
|
|
//
|
|
// 2. DeserializeCoronaSig(arbitrary bytes) never panics; tampered inputs
|
|
// produce a clean error.
|
|
//
|
|
// 3. Tampering any byte of a serialized pulse causes either a parse
|
|
// error (most cases) or a Verify failure (rare cases that happen
|
|
// to land on valid lattigo wire framing). Either is acceptable —
|
|
// we only forbid the silent-corruption / panic outcomes.
|
|
|
|
package pulsar
|
|
|
|
import (
|
|
"bytes"
|
|
"crypto/rand"
|
|
"testing"
|
|
|
|
corona "github.com/luxfi/corona/threshold"
|
|
)
|
|
|
|
// FuzzPulseDeserialize runs DeserializeCoronaSig over arbitrary bytes and
|
|
// confirms it never panics.
|
|
func FuzzPulseDeserialize(f *testing.F) {
|
|
// Seed: a real pulse from a 3-of-2 ceremony.
|
|
good := mustGoodPulseSeed()
|
|
f.Add(good)
|
|
// Truncated.
|
|
if len(good) > 16 {
|
|
f.Add(good[:16])
|
|
}
|
|
// Empty.
|
|
f.Add([]byte{})
|
|
// Plausible-shaped junk.
|
|
f.Add(append([]byte{0x10, 0x00, 0x00, 0x00}, bytes.Repeat([]byte{0xff}, 32)...))
|
|
|
|
f.Fuzz(func(t *testing.T, raw []byte) {
|
|
// Property: DeserializeCoronaSig never panics regardless of input.
|
|
_, _ = DeserializeCoronaSig(raw)
|
|
})
|
|
}
|
|
|
|
// FuzzPulseSerialize runs the full SerializeCoronaSig → DeserializeCoronaSig →
|
|
// corona.Verify round trip over valid pulses, then mutates one
|
|
// byte at a time and confirms either a parse error or a Verify failure
|
|
// — never a silent-success / panic outcome.
|
|
//
|
|
// We do NOT fuzz the kernel sign protocol itself — that would mean
|
|
// running the threshold ceremony per fuzz call, which is too slow.
|
|
// Instead we generate one good pulse before the fuzz loop, then use
|
|
// the fuzzer to drive byte mutations.
|
|
func FuzzPulseSerialize(f *testing.F) {
|
|
good, message, gk := mustGoodPulse()
|
|
|
|
// Seed: indices into good[] to flip.
|
|
f.Add(uint32(0))
|
|
f.Add(uint32(len(good) - 1))
|
|
f.Add(uint32(len(good) / 2))
|
|
|
|
f.Fuzz(func(t *testing.T, idx uint32) {
|
|
if len(good) == 0 {
|
|
t.Skip("no good seed")
|
|
}
|
|
// Flip one byte at the fuzzer-provided index.
|
|
i := int(idx) % len(good)
|
|
mut := append([]byte(nil), good...)
|
|
mut[i] ^= 0x01
|
|
|
|
sig, err := DeserializeCoronaSig(mut)
|
|
if err != nil {
|
|
// Parse error is the expected outcome on most flips.
|
|
return
|
|
}
|
|
// If the bytes happened to deserialize, then Verify against the
|
|
// original message and group key MUST fail — flipping any byte
|
|
// in a valid threshold signature breaks the verification
|
|
// equation with overwhelming probability.
|
|
if corona.Verify(gk, message, sig) {
|
|
t.Fatalf("byte-flip at %d still verified — collision in pulse encoding", i)
|
|
}
|
|
})
|
|
}
|
|
|
|
// TestFuzzCorpus_PulseReplay re-runs the round-trip + tamper checks on
|
|
// a fixed seed corpus deterministically for CI replay.
|
|
func TestFuzzCorpus_PulseReplay(t *testing.T) {
|
|
good, message, gk := mustGoodPulse()
|
|
sig, err := DeserializeCoronaSig(good)
|
|
if err != nil {
|
|
t.Fatalf("seed: deserialize failed: %v", err)
|
|
}
|
|
if !corona.Verify(gk, message, sig) {
|
|
t.Fatalf("seed: verify failed")
|
|
}
|
|
wire, err := SerializeCoronaSig(sig)
|
|
if err != nil {
|
|
t.Fatalf("seed: re-serialize failed: %v", err)
|
|
}
|
|
if !bytes.Equal(wire, good) {
|
|
t.Fatalf("seed: round-trip not byte-equal")
|
|
}
|
|
}
|
|
|
|
// mustGoodPulse runs a 3-of-2 Pulsar threshold ceremony and returns
|
|
// (serialized pulse, signed message, group key).
|
|
func mustGoodPulse() (wire []byte, msg string, gk *corona.GroupKey) {
|
|
const n, threshold = 3, 2
|
|
const message = "fuzz-pulse-seed"
|
|
|
|
shares, key, err := corona.GenerateKeys(threshold, n, rand.Reader)
|
|
if err != nil {
|
|
panic(err)
|
|
}
|
|
signers := make([]int, n)
|
|
for i := range signers {
|
|
signers[i] = i
|
|
}
|
|
prfKey := make([]byte, 32)
|
|
if _, err := rand.Read(prfKey); err != nil {
|
|
panic(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)
|
|
if err != nil {
|
|
panic(err)
|
|
}
|
|
r2[i] = d
|
|
}
|
|
sig, err := parties[0].Finalize(r2)
|
|
if err != nil {
|
|
panic(err)
|
|
}
|
|
wire, err = SerializeCoronaSig(sig)
|
|
if err != nil {
|
|
panic(err)
|
|
}
|
|
return wire, message, key
|
|
}
|
|
|
|
// mustGoodPulseSeed returns just the wire bytes for FuzzPulseDeserialize.
|
|
func mustGoodPulseSeed() []byte {
|
|
w, _, _ := mustGoodPulse()
|
|
return w
|
|
}
|