mirror of
https://github.com/luxfi/consensus.git
synced 2026-09-24 00:26:58 +00:00
- Introduce config/cert_policy.go: single CertPolicy record (Mode + Variant + TimeoutMs + Fallback) collapses three knobs from LP-217 / LP-202 / LP-204 into one operator-facing struct. Validated at chain genesis; refuses launch when invalid. Distinct from PQMode enum (pq_mode.go) โ CertPolicy is the deployment knob, PQMode is the protocol-level identifier. - Add config/cert_policy_test.go and config/bench_cert_policy_test.go covering validation matrix + fallback-tier degradation contract. - PQModeFromEnv: drop legacy LUX_CONSENSUS_PQ_MODE fallback per the "one canonical, unprefixed name" directive. CONSENSUS_PQ_MODE is the only honoured env var. - engine/pq/consensus.go: minor follow-up wiring. - paper/* + PQ-GPU-AUDIT.md + README.md: align prose with CertPolicy framing and unprefixed env names.
794 lines
25 KiB
Go
794 lines
25 KiB
Go
// Copyright (C) 2019-2026, Lux Industries Inc. All rights reserved.
|
|
// See the file LICENSE for licensing terms.
|
|
|
|
package config
|
|
|
|
import "testing"
|
|
|
|
// TestPQMode_String pins the canonical wire name for every mode.
|
|
// Anyone who renames a constant without updating the env-var docs / HIP
|
|
// breaks this test.
|
|
func TestPQMode_String(t *testing.T) {
|
|
cases := []struct {
|
|
mode PQMode
|
|
name string
|
|
}{
|
|
{PQModeBLS, "bls"},
|
|
{PQModeNasua, "nasua"},
|
|
{PQModePulsar, "pulsar"},
|
|
{PQModeQuasar, "quasar"},
|
|
{PQModeMLDSA, "mldsa"},
|
|
}
|
|
for _, c := range cases {
|
|
if got := c.mode.String(); got != c.name {
|
|
t.Errorf("PQMode(%d).String() = %q, want %q", c.mode, got, c.name)
|
|
}
|
|
}
|
|
}
|
|
|
|
// TestParsePQMode_Canonical accepts every canonical lower-case name.
|
|
func TestParsePQMode_Canonical(t *testing.T) {
|
|
for _, m := range []PQMode{
|
|
PQModeBLS, PQModeNasua, PQModePulsar, PQModeQuasar, PQModeMLDSA,
|
|
} {
|
|
got, err := ParsePQMode(m.String())
|
|
if err != nil {
|
|
t.Errorf("ParsePQMode(%q) errored: %v", m.String(), err)
|
|
continue
|
|
}
|
|
if got != m {
|
|
t.Errorf("ParsePQMode(%q) = %v, want %v", m.String(), got, m)
|
|
}
|
|
}
|
|
}
|
|
|
|
// TestParsePQMode_Aliases ensures legacy env strings still parse to the
|
|
// right mode. Drop entries here only when the alias has been removed
|
|
// from a major release.
|
|
func TestParsePQMode_Aliases(t *testing.T) {
|
|
cases := []struct {
|
|
alias string
|
|
want PQMode
|
|
}{
|
|
// BLS
|
|
{"", PQModeBLS},
|
|
{"BLS", PQModeBLS},
|
|
{"bls-only", PQModeBLS},
|
|
{"classical", PQModeBLS},
|
|
|
|
// Corona (academic, BLAKE3, trusted dealer)
|
|
{"rt", PQModeNasua},
|
|
{"academic", PQModeNasua},
|
|
{"bls-rt", PQModeNasua},
|
|
{"sha256-rt", PQModeNasua},
|
|
|
|
// Pulsar (production fork, SHA-3, Pedersen DKG)
|
|
{"sha3-rt", PQModePulsar},
|
|
{"production-rt", PQModePulsar},
|
|
{"bls-pulsar", PQModePulsar},
|
|
|
|
// Quasar โ component-named aliases only (no "triple"; tells callers
|
|
// nothing about what's in the cert).
|
|
{"rollup", PQModeQuasar},
|
|
{"groth16", PQModeQuasar},
|
|
{"bls-z", PQModeQuasar},
|
|
{"bls-zk", PQModeQuasar},
|
|
{"z-chain", PQModeQuasar},
|
|
{"pulsar-z", PQModeQuasar},
|
|
|
|
// MLDSA fallback
|
|
{"audit", PQModeMLDSA},
|
|
{"bls-mldsa", PQModeMLDSA},
|
|
}
|
|
for _, c := range cases {
|
|
got, err := ParsePQMode(c.alias)
|
|
if err != nil {
|
|
t.Errorf("ParsePQMode(%q) errored: %v", c.alias, err)
|
|
continue
|
|
}
|
|
if got != c.want {
|
|
t.Errorf("ParsePQMode(%q) = %v, want %v", c.alias, got, c.want)
|
|
}
|
|
}
|
|
}
|
|
|
|
func TestParsePQMode_Unknown(t *testing.T) {
|
|
if _, err := ParsePQMode("nonsense"); err == nil {
|
|
t.Errorf("ParsePQMode(\"nonsense\") accepted; want error")
|
|
}
|
|
}
|
|
|
|
// TestParsePQMode_NoCountingWords pins the policy that mode names must
|
|
// describe what's in the cert. "triple" / "triple-quantum" / "double" /
|
|
// "stack" tell a caller nothing about which witnesses are signed and are
|
|
// rejected. Drop a name from the rejected list only when there is a
|
|
// concrete deployment requiring it AND a HIP amendment justifying it.
|
|
func TestParsePQMode_NoCountingWords(t *testing.T) {
|
|
rejected := []string{"triple", "triple-quantum", "double", "stack", "all", "max"}
|
|
for _, s := range rejected {
|
|
if _, err := ParsePQMode(s); err == nil {
|
|
t.Errorf("ParsePQMode(%q) accepted; counting words must be rejected per HIP-0077", s)
|
|
}
|
|
}
|
|
}
|
|
|
|
// TestPQMode_PolicyID pins the wire policy ID per mode. Anyone changing
|
|
// the wire layer must update this table at the same time.
|
|
func TestPQMode_PolicyID(t *testing.T) {
|
|
cases := []struct {
|
|
mode PQMode
|
|
want uint16
|
|
}{
|
|
{PQModeBLS, 1}, // PolicyQuorum
|
|
{PQModeNasua, 5}, // PolicyPQ
|
|
{PQModePulsar, 5}, // PolicyPQ (same wire shape, SHA-3 negotiated out-of-band)
|
|
{PQModeQuasar, 4}, // PolicyQuantum
|
|
{PQModeMLDSA, 6}, // PolicyPZ
|
|
}
|
|
for _, c := range cases {
|
|
if got := c.mode.PolicyID(); got != c.want {
|
|
t.Errorf("PQMode(%d).PolicyID() = %d, want %d", c.mode, got, c.want)
|
|
}
|
|
}
|
|
}
|
|
|
|
// TestPQMode_HashProfile asserts the canonical hash family per mode.
|
|
// Strings are the human-readable form of HashSuiteID and must stay in
|
|
// lockstep with TestPQMode_HashSuiteID below.
|
|
//
|
|
// NIST-aligned mapping (HIP-0077 ยง"Lux consensus PQ modes"):
|
|
//
|
|
// bls -> "none" no hash-family commitment
|
|
// corona -> "blake3-legacy" academic kernel, non-normative
|
|
// pulsar -> "sha3-nist" FIPS 202 + SP 800-185 (cSHAKE256 family)
|
|
// quasar -> "sha3-nist" same kernel as Pulsar
|
|
// mldsa -> "sha3-nist" ML-DSA-65 uses SHAKE256 (FIPS 202)
|
|
func TestPQMode_HashProfile(t *testing.T) {
|
|
cases := []struct {
|
|
mode PQMode
|
|
want string
|
|
}{
|
|
{PQModeBLS, "none"},
|
|
{PQModeNasua, "blake3-legacy"},
|
|
{PQModePulsar, "sha3-nist"},
|
|
{PQModeQuasar, "sha3-nist"},
|
|
{PQModeMLDSA, "sha3-nist"},
|
|
}
|
|
for _, c := range cases {
|
|
if got := c.mode.HashProfile(); got != c.want {
|
|
t.Errorf("PQMode(%d).HashProfile() = %q, want %q", c.mode, got, c.want)
|
|
}
|
|
}
|
|
}
|
|
|
|
// TestPQMode_HashSuiteID pins the wire HashSuiteID byte for every mode.
|
|
// Closes HIP-0077 red-review F1 (silent finality forks between hash-profile
|
|
// islands sharing the same PolicyID).
|
|
//
|
|
// Numbering per the NIST submission prescription:
|
|
//
|
|
// 0x00 HashSuiteNone BLS-only, no hash commitment
|
|
// 0x01 HashSuiteSHA3NIST FIPS 202 family (normative)
|
|
// 0x02 HashSuiteBLAKE3Legacy academic / federation, non-normative
|
|
//
|
|
// Renumbering an existing entry breaks every cert ever signed under
|
|
// that mode; ADDING a new entry only requires a new free integer.
|
|
func TestPQMode_HashSuiteID(t *testing.T) {
|
|
cases := []struct {
|
|
mode PQMode
|
|
want HashSuiteID
|
|
}{
|
|
{PQModeBLS, HashSuiteNone}, // 0x00
|
|
{PQModeNasua, HashSuiteBLAKE3Legacy}, // 0x02
|
|
{PQModePulsar, HashSuiteSHA3NIST}, // 0x01
|
|
{PQModeQuasar, HashSuiteSHA3NIST}, // 0x01
|
|
{PQModeMLDSA, HashSuiteSHA3NIST}, // 0x01 (SHAKE256 โ FIPS 202)
|
|
}
|
|
for _, c := range cases {
|
|
if got := c.mode.HashSuiteID(); got != c.want {
|
|
t.Errorf("PQMode(%d).HashSuiteID() = %d (%q), want %d (%q)",
|
|
c.mode, got, got.String(), c.want, c.want.String())
|
|
}
|
|
}
|
|
}
|
|
|
|
// TestHashSuiteID_StableIntegers locks the integer values themselves.
|
|
// The HashSuiteID byte is on the wire of every Quasar finality cert;
|
|
// renumbering breaks forward and backward compatibility simultaneously.
|
|
// New suites MUST claim the next free integer.
|
|
func TestHashSuiteID_StableIntegers(t *testing.T) {
|
|
cases := []struct {
|
|
suite HashSuiteID
|
|
want uint8
|
|
}{
|
|
{HashSuiteNone, 0x00},
|
|
{HashSuiteSHA3NIST, 0x01},
|
|
{HashSuiteBLAKE3Legacy, 0x02},
|
|
}
|
|
for _, c := range cases {
|
|
if got := uint8(c.suite); got != c.want {
|
|
t.Errorf("HashSuiteID %q = 0x%02x, want 0x%02x", c.suite.String(), got, c.want)
|
|
}
|
|
}
|
|
}
|
|
|
|
// TestHashSuiteID_String pins the human-readable name for every defined
|
|
// suite. The names appear in logs and error messages.
|
|
func TestHashSuiteID_String(t *testing.T) {
|
|
cases := []struct {
|
|
suite HashSuiteID
|
|
want string
|
|
}{
|
|
{HashSuiteNone, "none"},
|
|
{HashSuiteSHA3NIST, "sha3-nist"},
|
|
{HashSuiteBLAKE3Legacy, "blake3-legacy"},
|
|
}
|
|
for _, c := range cases {
|
|
if got := c.suite.String(); got != c.want {
|
|
t.Errorf("HashSuiteID(0x%02x).String() = %q, want %q", uint8(c.suite), got, c.want)
|
|
}
|
|
}
|
|
}
|
|
|
|
// TestHashSuiteID_IsNormative โ production Lux meshes accept only
|
|
// FIPS-aligned suites. Non-normative suites may emit certs but not on
|
|
// any NIST-track production network.
|
|
func TestHashSuiteID_IsNormative(t *testing.T) {
|
|
if !HashSuiteSHA3NIST.IsNormative() {
|
|
t.Errorf("HashSuiteSHA3NIST must be normative")
|
|
}
|
|
if HashSuiteBLAKE3Legacy.IsNormative() {
|
|
t.Errorf("HashSuiteBLAKE3Legacy must be non-normative")
|
|
}
|
|
if HashSuiteNone.IsNormative() {
|
|
t.Errorf("HashSuiteNone (bls-only) must be non-normative")
|
|
}
|
|
}
|
|
|
|
// TestPQMode_SigSchemeID pins the default signature scheme the consensus
|
|
// layer will request for each mode. Mainnet declares PQModeMLDSA โ
|
|
// raw ML-DSA-65 (audit-grade); operators that want Pulsar-M-65 threshold
|
|
// must opt in explicitly via the cert assembler.
|
|
func TestPQMode_SigSchemeID(t *testing.T) {
|
|
cases := []struct {
|
|
mode PQMode
|
|
want SigSchemeID
|
|
}{
|
|
{PQModeBLS, SigSchemeNone},
|
|
{PQModeNasua, SigSchemeNasua},
|
|
{PQModePulsar, SigSchemePulsarR},
|
|
{PQModeQuasar, SigSchemePulsarR},
|
|
{PQModeMLDSA, SigSchemeMLDSA65}, // raw ML-DSA-65 default; ops opt-in to Pulsar-M-65
|
|
}
|
|
for _, c := range cases {
|
|
if got := c.mode.SigSchemeID(); got != c.want {
|
|
t.Errorf("PQMode(%d).SigSchemeID() = 0x%02x (%q), want 0x%02x (%q)",
|
|
c.mode, uint8(got), got.String(), uint8(c.want), c.want.String())
|
|
}
|
|
}
|
|
}
|
|
|
|
// TestSigSchemeID_StableIntegers locks the byte values per HIP-0077
|
|
// numbering blocks. Renumbering breaks every cert ever emitted; new
|
|
// schemes claim the next free integer in their block:
|
|
//
|
|
// 0x10 BLS / 0x20 Corona / 0x30 Pulsar.R / 0x40 raw ML-DSA / 0x50 Pulsar-M
|
|
func TestSigSchemeID_StableIntegers(t *testing.T) {
|
|
cases := []struct {
|
|
scheme SigSchemeID
|
|
want uint8
|
|
}{
|
|
{SigSchemeNone, 0x00},
|
|
{SigSchemeBLS12381, 0x10},
|
|
{SigSchemeNasua, 0x20},
|
|
{SigSchemePulsarR, 0x30},
|
|
{SigSchemeMLDSA44, 0x41},
|
|
{SigSchemeMLDSA65, 0x42},
|
|
{SigSchemeMLDSA87, 0x43},
|
|
{SigSchemePulsar44, 0x51},
|
|
{SigSchemePulsar65, 0x52}, // production default
|
|
{SigSchemePulsar87, 0x53},
|
|
}
|
|
for _, c := range cases {
|
|
if got := uint8(c.scheme); got != c.want {
|
|
t.Errorf("SigSchemeID %q = 0x%02x, want 0x%02x", c.scheme.String(), got, c.want)
|
|
}
|
|
}
|
|
}
|
|
|
|
// TestSigSchemeID_String pins the canonical wire name.
|
|
func TestSigSchemeID_String(t *testing.T) {
|
|
cases := []struct {
|
|
scheme SigSchemeID
|
|
want string
|
|
}{
|
|
{SigSchemeNone, "none"},
|
|
{SigSchemeBLS12381, "bls12-381"},
|
|
{SigSchemeNasua, "nasua"},
|
|
{SigSchemePulsarR, "pulsar-r"},
|
|
{SigSchemeMLDSA44, "ml-dsa-44"},
|
|
{SigSchemeMLDSA65, "ml-dsa-65"},
|
|
{SigSchemeMLDSA87, "ml-dsa-87"},
|
|
{SigSchemePulsar44, "pulsar-m-44"},
|
|
{SigSchemePulsar65, "pulsar-m-65"},
|
|
{SigSchemePulsar87, "pulsar-m-87"},
|
|
}
|
|
for _, c := range cases {
|
|
if got := c.scheme.String(); got != c.want {
|
|
t.Errorf("SigSchemeID(0x%02x).String() = %q, want %q", uint8(c.scheme), got, c.want)
|
|
}
|
|
}
|
|
}
|
|
|
|
// TestSigSchemeID_IsPulsarM separates the Pulsar-M threshold family from
|
|
// everything else. Pulsar-M outputs verify under unmodified FIPS 204
|
|
// ML-DSA.Verify but are produced by a threshold protocol.
|
|
func TestSigSchemeID_IsPulsarM(t *testing.T) {
|
|
pulsarM := []SigSchemeID{SigSchemePulsar44, SigSchemePulsar65, SigSchemePulsar87}
|
|
for _, s := range pulsarM {
|
|
if !s.IsPulsarM() {
|
|
t.Errorf("%q must be Pulsar-M", s.String())
|
|
}
|
|
}
|
|
notPulsarM := []SigSchemeID{
|
|
SigSchemeNone, SigSchemeBLS12381, SigSchemeNasua,
|
|
SigSchemePulsarR,
|
|
SigSchemeMLDSA44, SigSchemeMLDSA65, SigSchemeMLDSA87,
|
|
}
|
|
for _, s := range notPulsarM {
|
|
if s.IsPulsarM() {
|
|
t.Errorf("%q must not be Pulsar-M", s.String())
|
|
}
|
|
}
|
|
}
|
|
|
|
// TestSigSchemeID_IsRawMLDSA covers the 0x40 block โ single-party FIPS 204
|
|
// signatures (no threshold protocol).
|
|
func TestSigSchemeID_IsRawMLDSA(t *testing.T) {
|
|
rawMLDSA := []SigSchemeID{SigSchemeMLDSA44, SigSchemeMLDSA65, SigSchemeMLDSA87}
|
|
for _, s := range rawMLDSA {
|
|
if !s.IsRawMLDSA() {
|
|
t.Errorf("%q must be raw ML-DSA", s.String())
|
|
}
|
|
}
|
|
notRaw := []SigSchemeID{
|
|
SigSchemeNone, SigSchemeBLS12381, SigSchemeNasua, SigSchemePulsarR,
|
|
SigSchemePulsar44, SigSchemePulsar65, SigSchemePulsar87,
|
|
}
|
|
for _, s := range notRaw {
|
|
if s.IsRawMLDSA() {
|
|
t.Errorf("%q must not be raw ML-DSA", s.String())
|
|
}
|
|
}
|
|
}
|
|
|
|
// TestSigSchemeID_VerifiesUnderFIPS204 โ both raw ML-DSA (0x40) and
|
|
// Pulsar-M (0x50) verify under FIPS 204 ML-DSA.Verify.
|
|
func TestSigSchemeID_VerifiesUnderFIPS204(t *testing.T) {
|
|
yes := []SigSchemeID{
|
|
SigSchemeMLDSA44, SigSchemeMLDSA65, SigSchemeMLDSA87,
|
|
SigSchemePulsar44, SigSchemePulsar65, SigSchemePulsar87,
|
|
}
|
|
for _, s := range yes {
|
|
if !s.VerifiesUnderFIPS204() {
|
|
t.Errorf("%q must verify under FIPS 204", s.String())
|
|
}
|
|
}
|
|
no := []SigSchemeID{
|
|
SigSchemeNone, SigSchemeBLS12381, SigSchemeNasua, SigSchemePulsarR,
|
|
}
|
|
for _, s := range no {
|
|
if s.VerifiesUnderFIPS204() {
|
|
t.Errorf("%q must not verify under FIPS 204", s.String())
|
|
}
|
|
}
|
|
}
|
|
|
|
// TestProofPolicyID_StableIntegers locks the wire bytes for the ZK proof
|
|
// policy enum (security class). Z-Chain v1 ships with STARK_FRI_SHA3_PQ
|
|
// (0x10); classical IDs exist on the wire so audit pipelines flag
|
|
// misconfigurations.
|
|
//
|
|
// Renumbering an entry breaks every cert ever signed under it. Adding a
|
|
// new entry only requires a new free integer in the appropriate block.
|
|
func TestProofPolicyID_StableIntegers(t *testing.T) {
|
|
cases := []struct {
|
|
ps ProofPolicyID
|
|
want uint8
|
|
}{
|
|
{ProofPolicyNone, 0x00},
|
|
{ProofPolicySTARKFRISHA3PQ, 0x10},
|
|
{ProofPolicySTARKFRIKeccak, 0x11},
|
|
{ProofPolicyGroth16BN254Forbid, 0x80},
|
|
{ProofPolicyPLONKKZGForbid, 0x81},
|
|
{ProofPolicyHaloECForbid, 0x82},
|
|
{ProofPolicyIPAECForbid, 0x83},
|
|
}
|
|
for _, c := range cases {
|
|
if got := uint8(c.ps); got != c.want {
|
|
t.Errorf("ProofPolicyID %q = 0x%02x, want 0x%02x", c.ps.String(), got, c.want)
|
|
}
|
|
}
|
|
}
|
|
|
|
// TestProofPolicyID_String pins canonical wire names.
|
|
func TestProofPolicyID_String(t *testing.T) {
|
|
cases := []struct {
|
|
ps ProofPolicyID
|
|
want string
|
|
}{
|
|
{ProofPolicyNone, "none"},
|
|
{ProofPolicySTARKFRISHA3PQ, "stark-fri-sha3-pq"},
|
|
{ProofPolicySTARKFRIKeccak, "stark-fri-keccak-pq"},
|
|
{ProofPolicyGroth16BN254Forbid, "groth16-bn254-classical-forbidden-in-pq"},
|
|
{ProofPolicyPLONKKZGForbid, "plonk-kzg-classical-forbidden-in-pq"},
|
|
{ProofPolicyHaloECForbid, "halo-ec-classical-forbidden-in-pq"},
|
|
{ProofPolicyIPAECForbid, "ipa-ec-classical-forbidden-in-pq"},
|
|
}
|
|
for _, c := range cases {
|
|
if got := c.ps.String(); got != c.want {
|
|
t.Errorf("ProofPolicyID(0x%02x).String() = %q, want %q", uint8(c.ps), got, c.want)
|
|
}
|
|
}
|
|
}
|
|
|
|
// TestProofPolicyID_IsPostQuantum gates strict-PQ verifier acceptance.
|
|
// Lux primary-network producers MUST emit only IsPostQuantum=true; any
|
|
// classical wire byte makes it onto the wire only as a forbidden marker.
|
|
func TestProofPolicyID_IsPostQuantum(t *testing.T) {
|
|
pq := []ProofPolicyID{
|
|
ProofPolicySTARKFRISHA3PQ,
|
|
ProofPolicySTARKFRIKeccak,
|
|
}
|
|
for _, p := range pq {
|
|
if !p.IsPostQuantum() {
|
|
t.Errorf("%q must be IsPostQuantum=true", p.String())
|
|
}
|
|
}
|
|
notPQ := []ProofPolicyID{
|
|
ProofPolicyNone, // None is not PQ-positive; it's "no commitment".
|
|
ProofPolicyGroth16BN254Forbid,
|
|
ProofPolicyPLONKKZGForbid,
|
|
ProofPolicyHaloECForbid,
|
|
ProofPolicyIPAECForbid,
|
|
}
|
|
for _, p := range notPQ {
|
|
if p.IsPostQuantum() {
|
|
t.Errorf("%q must be IsPostQuantum=false", p.String())
|
|
}
|
|
}
|
|
}
|
|
|
|
// TestProofPolicyID_IsForbiddenInPQMode โ strict-PQ deployments use this
|
|
// to refuse misconfigured certs. The forbidden markers exist as explicit
|
|
// wire bytes so audit tooling can name them precisely.
|
|
func TestProofPolicyID_IsForbiddenInPQMode(t *testing.T) {
|
|
for _, p := range []ProofPolicyID{
|
|
ProofPolicyGroth16BN254Forbid,
|
|
ProofPolicyPLONKKZGForbid,
|
|
ProofPolicyHaloECForbid,
|
|
ProofPolicyIPAECForbid,
|
|
} {
|
|
if !p.IsForbiddenInPQMode() {
|
|
t.Errorf("%q must be forbidden in PQ mode", p.String())
|
|
}
|
|
}
|
|
if ProofPolicySTARKFRISHA3PQ.IsForbiddenInPQMode() {
|
|
t.Errorf("STARK_FRI_SHA3_PQ must NOT be forbidden")
|
|
}
|
|
}
|
|
|
|
// TestProofPolicyID_LegacyAliases keeps the deprecated ProofSystem* names
|
|
// compiling for one release. New code must use the ProofPolicy* names.
|
|
// Drop this test when the aliases are removed at the 2026-Aug-31 freeze.
|
|
func TestProofPolicyID_LegacyAliases(t *testing.T) {
|
|
if ProofSystemNone != ProofPolicyNone {
|
|
t.Errorf("legacy alias ProofSystemNone must equal ProofPolicyNone")
|
|
}
|
|
if ProofSystemSTARKFRISHA3PQ != ProofPolicySTARKFRISHA3PQ {
|
|
t.Errorf("legacy alias ProofSystemSTARKFRISHA3PQ must equal ProofPolicySTARKFRISHA3PQ")
|
|
}
|
|
if ProofSystemSTARKFRIKeccakPQ != ProofPolicySTARKFRIKeccak {
|
|
t.Errorf("legacy alias ProofSystemSTARKFRIKeccakPQ must equal ProofPolicySTARKFRIKeccak")
|
|
}
|
|
if ProofSystemGroth16BN254Forbid != ProofPolicyGroth16BN254Forbid {
|
|
t.Errorf("legacy alias ProofSystemGroth16BN254Forbid must equal ProofPolicyGroth16BN254Forbid")
|
|
}
|
|
if ProofSystemKZGForbid != ProofPolicyPLONKKZGForbid {
|
|
t.Errorf("legacy alias ProofSystemKZGForbid must equal ProofPolicyPLONKKZGForbid")
|
|
}
|
|
}
|
|
|
|
// TestProofBackendID_StableIntegers locks the wire bytes for the proof
|
|
// **backend** (implementation) enum, orthogonal to ProofPolicyID. Backends
|
|
// covered:
|
|
//
|
|
// 0x20 RISC0_SUCCINCT_STARK succinct receipt path (no Groth16 wrapper)
|
|
// 0x21 SP1_COMPRESSED_STARK SP1 compressed STARK
|
|
// 0x22 P3Q_STARK_FRI_SHA3 Lux P3Q (Plonky3 fork, cSHAKE256 Merkle)
|
|
// 0x23 STONE_CAIRO_STARK Cairo / Stone backend
|
|
// 0x24 STWO_CIRCLE_STARK Stwo Circle STARK
|
|
// 0x70 RISC0_RAW_STARK_DEV dev/CI only
|
|
// 0x71 SP1_CORE_STARK_DEV dev/CI only
|
|
// 0x80 GROTH16_WRAP_FORBID forbidden wrapper in strict-PQ
|
|
// 0x81 KZG_WRAP_FORBID forbidden wrapper in strict-PQ
|
|
func TestProofBackendID_StableIntegers(t *testing.T) {
|
|
cases := []struct {
|
|
backend ProofBackendID
|
|
want uint8
|
|
}{
|
|
{ProofBackendNone, 0x00},
|
|
{ProofBackendRISC0SuccinctSTARK, 0x20},
|
|
{ProofBackendSP1CompressedSTARK, 0x21},
|
|
{ProofBackendP3QSTARKFRISHA3, 0x22},
|
|
{ProofBackendStoneCairoSTARK, 0x23},
|
|
{ProofBackendStwoCircleSTARK, 0x24},
|
|
{ProofBackendRISC0RawSTARKDev, 0x70},
|
|
{ProofBackendSP1CoreSTARKDev, 0x71},
|
|
{ProofBackendGroth16WrapForbid, 0x80},
|
|
{ProofBackendKZGWrapForbid, 0x81},
|
|
}
|
|
for _, c := range cases {
|
|
if got := uint8(c.backend); got != c.want {
|
|
t.Errorf("ProofBackendID %q = 0x%02x, want 0x%02x",
|
|
c.backend.String(), got, c.want)
|
|
}
|
|
}
|
|
}
|
|
|
|
// TestProofBackendID_String pins canonical wire names so logs and audit
|
|
// pipelines see the same string in every cluster.
|
|
func TestProofBackendID_String(t *testing.T) {
|
|
cases := []struct {
|
|
backend ProofBackendID
|
|
want string
|
|
}{
|
|
{ProofBackendNone, "none"},
|
|
{ProofBackendRISC0SuccinctSTARK, "risc0-succinct-stark-pq"},
|
|
{ProofBackendSP1CompressedSTARK, "sp1-compressed-stark-pq"},
|
|
{ProofBackendP3QSTARKFRISHA3, "p3q-stark-fri-sha3-pq"},
|
|
{ProofBackendStoneCairoSTARK, "stone-cairo-stark-pq"},
|
|
{ProofBackendStwoCircleSTARK, "stwo-circle-stark-pq"},
|
|
{ProofBackendRISC0RawSTARKDev, "risc0-raw-stark-dev"},
|
|
{ProofBackendSP1CoreSTARKDev, "sp1-core-stark-dev"},
|
|
{ProofBackendGroth16WrapForbid, "groth16-wrap-classical-forbidden-in-pq"},
|
|
{ProofBackendKZGWrapForbid, "kzg-wrap-classical-forbidden-in-pq"},
|
|
}
|
|
for _, c := range cases {
|
|
if got := c.backend.String(); got != c.want {
|
|
t.Errorf("ProofBackendID(0x%02x).String() = %q, want %q",
|
|
uint8(c.backend), got, c.want)
|
|
}
|
|
}
|
|
}
|
|
|
|
// TestProofBackendID_IsProductionPQ separates the production block (0x20)
|
|
// from dev (0x70) and forbidden (0x80). Strict-PQ profiles MUST allow only
|
|
// production backends.
|
|
func TestProofBackendID_IsProductionPQ(t *testing.T) {
|
|
prod := []ProofBackendID{
|
|
ProofBackendRISC0SuccinctSTARK,
|
|
ProofBackendSP1CompressedSTARK,
|
|
ProofBackendP3QSTARKFRISHA3,
|
|
ProofBackendStoneCairoSTARK,
|
|
ProofBackendStwoCircleSTARK,
|
|
}
|
|
for _, b := range prod {
|
|
if !b.IsProductionPQ() {
|
|
t.Errorf("%q must be IsProductionPQ=true", b.String())
|
|
}
|
|
}
|
|
nonProd := []ProofBackendID{
|
|
ProofBackendNone,
|
|
ProofBackendRISC0RawSTARKDev,
|
|
ProofBackendSP1CoreSTARKDev,
|
|
ProofBackendGroth16WrapForbid,
|
|
ProofBackendKZGWrapForbid,
|
|
}
|
|
for _, b := range nonProd {
|
|
if b.IsProductionPQ() {
|
|
t.Errorf("%q must be IsProductionPQ=false", b.String())
|
|
}
|
|
}
|
|
}
|
|
|
|
// TestProofBackendID_IsForbiddenInPQMode mirrors the policy-layer test.
|
|
func TestProofBackendID_IsForbiddenInPQMode(t *testing.T) {
|
|
for _, b := range []ProofBackendID{
|
|
ProofBackendGroth16WrapForbid,
|
|
ProofBackendKZGWrapForbid,
|
|
} {
|
|
if !b.IsForbiddenInPQMode() {
|
|
t.Errorf("%q must be forbidden in PQ mode", b.String())
|
|
}
|
|
}
|
|
for _, b := range []ProofBackendID{
|
|
ProofBackendNone,
|
|
ProofBackendRISC0SuccinctSTARK,
|
|
ProofBackendSP1CompressedSTARK,
|
|
ProofBackendP3QSTARKFRISHA3,
|
|
ProofBackendStoneCairoSTARK,
|
|
ProofBackendStwoCircleSTARK,
|
|
ProofBackendRISC0RawSTARKDev,
|
|
ProofBackendSP1CoreSTARKDev,
|
|
} {
|
|
if b.IsForbiddenInPQMode() {
|
|
t.Errorf("%q must NOT carry the forbidden marker", b.String())
|
|
}
|
|
}
|
|
}
|
|
|
|
// TestIdentitySchemeID_StableIntegers locks the wire bytes for validator
|
|
// identity schemes. Block 0x40 mirrors raw ML-DSA in SigSchemeID so the
|
|
// byte pattern is consistent across the wire.
|
|
//
|
|
// 0x00 None no identity scheme committed
|
|
// 0x41 ML-DSA-44 NIST PQ Cat 2 (devnet only)
|
|
// 0x42 ML-DSA-65 NIST PQ Cat 3 (production identity default)
|
|
// 0x43 ML-DSA-87 NIST PQ Cat 5 (high-value root identities)
|
|
func TestIdentitySchemeID_StableIntegers(t *testing.T) {
|
|
cases := []struct {
|
|
ident IdentitySchemeID
|
|
want uint8
|
|
}{
|
|
{IdentitySchemeNone, 0x00},
|
|
{IdentitySchemeMLDSA44, 0x41},
|
|
{IdentitySchemeMLDSA65, 0x42}, // production default
|
|
{IdentitySchemeMLDSA87, 0x43},
|
|
}
|
|
for _, c := range cases {
|
|
if got := uint8(c.ident); got != c.want {
|
|
t.Errorf("IdentitySchemeID %q = 0x%02x, want 0x%02x",
|
|
c.ident.String(), got, c.want)
|
|
}
|
|
}
|
|
}
|
|
|
|
// TestIdentitySchemeID_String pins canonical wire names.
|
|
func TestIdentitySchemeID_String(t *testing.T) {
|
|
cases := []struct {
|
|
ident IdentitySchemeID
|
|
want string
|
|
}{
|
|
{IdentitySchemeNone, "none"},
|
|
{IdentitySchemeMLDSA44, "ml-dsa-44"},
|
|
{IdentitySchemeMLDSA65, "ml-dsa-65"},
|
|
{IdentitySchemeMLDSA87, "ml-dsa-87"},
|
|
}
|
|
for _, c := range cases {
|
|
if got := c.ident.String(); got != c.want {
|
|
t.Errorf("IdentitySchemeID(0x%02x).String() = %q, want %q",
|
|
uint8(c.ident), got, c.want)
|
|
}
|
|
}
|
|
}
|
|
|
|
// TestIdentitySchemeID_IsFIPS204 โ every non-None identity scheme uses
|
|
// unmodified FIPS 204 ML-DSA verification.
|
|
func TestIdentitySchemeID_IsFIPS204(t *testing.T) {
|
|
for _, i := range []IdentitySchemeID{
|
|
IdentitySchemeMLDSA44,
|
|
IdentitySchemeMLDSA65,
|
|
IdentitySchemeMLDSA87,
|
|
} {
|
|
if !i.IsFIPS204() {
|
|
t.Errorf("%q must verify under FIPS 204", i.String())
|
|
}
|
|
}
|
|
if IdentitySchemeNone.IsFIPS204() {
|
|
t.Errorf("IdentitySchemeNone must not advertise FIPS 204")
|
|
}
|
|
}
|
|
|
|
// TestPQMode_DKGRequired asserts the DKG class. Public chains MUST pick
|
|
// modes whose DKG class != "trusted-dealer".
|
|
func TestPQMode_DKGRequired(t *testing.T) {
|
|
cases := []struct {
|
|
mode PQMode
|
|
want string
|
|
}{
|
|
{PQModeBLS, "none"},
|
|
{PQModeNasua, "trusted-dealer"},
|
|
{PQModePulsar, "pedersen-dkg-over-rq"},
|
|
{PQModeQuasar, "pedersen-dkg-over-rq"},
|
|
{PQModeMLDSA, "none"},
|
|
}
|
|
for _, c := range cases {
|
|
if got := c.mode.DKGRequired(); got != c.want {
|
|
t.Errorf("PQMode(%d).DKGRequired() = %q, want %q", c.mode, got, c.want)
|
|
}
|
|
}
|
|
}
|
|
|
|
// TestPQMode_SuitableForPublicChain enforces HIP-0077 ยง"PQ defaults":
|
|
// production Lux meshes accept Pulsar / Quasar / MLDSA. BLS (no PQ)
|
|
// and Corona (trusted-dealer DKG) are rejected.
|
|
func TestPQMode_SuitableForPublicChain(t *testing.T) {
|
|
cases := []struct {
|
|
mode PQMode
|
|
want bool
|
|
}{
|
|
{PQModeBLS, false},
|
|
{PQModeNasua, false},
|
|
{PQModePulsar, true},
|
|
{PQModeQuasar, true},
|
|
{PQModeMLDSA, true},
|
|
}
|
|
for _, c := range cases {
|
|
if got := c.mode.SuitableForPublicChain(); got != c.want {
|
|
t.Errorf("PQMode(%d).SuitableForPublicChain() = %v, want %v", c.mode, got, c.want)
|
|
}
|
|
}
|
|
}
|
|
|
|
// TestPQModeFromBool pins the boolean-shorthand contract.
|
|
func TestPQModeFromBool(t *testing.T) {
|
|
if got := PQModeFromBool(true); got != PQModeQuasar {
|
|
t.Errorf("PQModeFromBool(true) = %v, want PQModeQuasar", got)
|
|
}
|
|
if got := PQModeFromBool(false); got != PQModeBLS {
|
|
t.Errorf("PQModeFromBool(false) = %v, want PQModeBLS", got)
|
|
}
|
|
}
|
|
|
|
// TestPQMode_IsPostQuantum is true for everything except plain BLS.
|
|
func TestPQMode_IsPostQuantum(t *testing.T) {
|
|
cases := []struct {
|
|
mode PQMode
|
|
want bool
|
|
}{
|
|
{PQModeBLS, false},
|
|
{PQModeNasua, true},
|
|
{PQModePulsar, true},
|
|
{PQModeQuasar, true},
|
|
{PQModeMLDSA, true},
|
|
}
|
|
for _, c := range cases {
|
|
if got := c.mode.IsPostQuantum(); got != c.want {
|
|
t.Errorf("PQMode(%d).IsPostQuantum() = %v, want %v", c.mode, got, c.want)
|
|
}
|
|
}
|
|
}
|
|
|
|
// TestPQModeFromEnv covers the env-var override path. CONSENSUS_PQ_MODE is
|
|
// the one and only env var honoured; the legacy LUX_-prefixed name is gone.
|
|
func TestPQModeFromEnv(t *testing.T) {
|
|
t.Setenv("CONSENSUS_PQ_MODE", "")
|
|
if got, err := PQModeFromEnv(PQModeQuasar); err != nil || got != PQModeQuasar {
|
|
t.Errorf("empty env: got (%v, %v), want (PQModeQuasar, nil)", got, err)
|
|
}
|
|
|
|
t.Setenv("CONSENSUS_PQ_MODE", "pulsar")
|
|
if got, err := PQModeFromEnv(PQModeBLS); err != nil || got != PQModePulsar {
|
|
t.Errorf("env=pulsar: got (%v, %v), want (PQModePulsar, nil)", got, err)
|
|
}
|
|
|
|
t.Setenv("CONSENSUS_PQ_MODE", "garbage")
|
|
got, err := PQModeFromEnv(PQModeQuasar)
|
|
if err == nil {
|
|
t.Errorf("env=garbage: expected error, got nil; mode=%v", got)
|
|
}
|
|
if got != PQModeQuasar {
|
|
t.Errorf("env=garbage: fell back to %v, want PQModeQuasar (the supplied default)", got)
|
|
}
|
|
}
|
|
|
|
// TestPQModeFromEnv_LegacyNameIgnored asserts that the dropped legacy
|
|
// LUX_CONSENSUS_PQ_MODE env var is no longer honoured. Only CONSENSUS_PQ_MODE
|
|
// affects the resolved mode.
|
|
func TestPQModeFromEnv_LegacyNameIgnored(t *testing.T) {
|
|
// Legacy name set, canonical unset: default returned, legacy is dead.
|
|
t.Setenv("CONSENSUS_PQ_MODE", "")
|
|
t.Setenv("LUX_CONSENSUS_PQ_MODE", "pulsar")
|
|
if got, err := PQModeFromEnv(PQModeBLS); err != nil || got != PQModeBLS {
|
|
t.Errorf("legacy-only: got (%v, %v), want (PQModeBLS, nil) โ legacy var must be ignored", got, err)
|
|
}
|
|
|
|
// Both set: canonical wins (legacy is ignored entirely).
|
|
t.Setenv("CONSENSUS_PQ_MODE", "quasar")
|
|
t.Setenv("LUX_CONSENSUS_PQ_MODE", "bls")
|
|
if got, err := PQModeFromEnv(PQModeMLDSA); err != nil || got != PQModeQuasar {
|
|
t.Errorf("both set: got (%v, %v), want (PQModeQuasar, nil) โ canonical must win", got, err)
|
|
}
|
|
}
|