Files
zeekayandHanzo Dev 4abf37c952 config: K is a number of draws, not a headcount
The live guard refused to start a chain whose K was below the number of
validators that existed. But sampling is stake-weighted: a validator is drawn in
proportion to what it staked, however many validators there are, and K only
sets how many draws a poll makes. Tying K to the headcount gave a validator
holding a billionth of the stake the same standing as one holding a fifth, and
refused to boot a producing chain because a small staker had joined.

The guard keeps what makes a committee safe — the overlap bound and at least
single-fault tolerance — and stops counting heads. The tier floor stays a
reported target: refusing to start does not add validators, it pins every node
on whatever it last booted.

Co-authored-by: Hanzo Dev <[email protected]>
2026-08-16 11:42:58 -07:00

1035 lines
41 KiB
Go
Raw Permalink Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
// Copyright (C) 2019-2026, Lux Industries Inc. All rights reserved.
// See the file LICENSE for licensing terms.
// Package config — PQMode is the single switch by which an operator selects
// what cryptographic witnesses ride alongside BLS-12-381 in a Quasar finality
// certificate. Each mode is a fully orthogonal point in the design space and
// is named for the sub-protocol that defines its post-quantum surface.
//
// The truth, in one table:
//
// mode classical threshold lattice per-validator PQ rollup DKG required FIPS-approvable?
// bls BLS-12-381 — — — — no (BLS not in FIPS 186)
// corona BLS-12-381 Corona / BLAKE3 — — trusted dealer no (BLAKE3 not in FIPS 202)
// pulsar BLS-12-381 Pulsar / SHA-3 (SP-185) — — Pedersen DKG over R_q + reshare partial (BLS still classical)
// quasar BLS-12-381 Pulsar / SHA-3 (SP-185) ML-DSA-65 (rolled) Groth16 / BN254-Z Pedersen DKG over R_q + reshare no (Groth16/BN254 classical, not FIPS)
// mldsa BLS-12-381 — ML-DSA-65 raw (SHAKE) — — yes (drop BLS or use FIPS-approved aggregate)
//
// Two production paths:
//
// BLS → Pulsar → Quasar public chains (open validator set, epoch rotation)
// BLS → Corona federation / bridge MPC (fixed dealer, no rotation)
//
// Quasar is the Lux-mesh default. Corona is for fixed federations only —
// its trusted-dealer DKG is unsuitable for an open public chain. MLDSA is the
// audit-grade fallback **and** the FIPS-approvable path: ML-DSA-65 (FIPS 204)
// + SHAKE256 (FIPS 202) is the only mode where every PQ component sits inside
// the NIST-approved algorithm set. See HIP-0077 §"FIPS profile" for the full
// drop-list (no Pulsar/Corona/Groth16/secp256k1) and the implementation
// switch needed.
//
// Pulsar vs Quasar in one sentence: Pulsar = BLS + one R-LWE threshold layer;
// Quasar = Pulsar + a second M-LWE layer (ML-DSA-65) compressed via Groth16
// onto Z-Chain. Same threshold layer, plus a defense-in-depth lattice that
// keeps cert size constant.
package config
import (
"fmt"
)
// PQMode selects the post-quantum signature stack the consensus engine layers
// over the BLS-12-381 fast path. See the package doc-comment for the full
// option matrix; each constant below pins one row.
type PQMode uint8
const (
// PQModeBLS — BLS aggregate only. 48-byte cert. Classical fast path,
// no PQ surface. Benchmarks and legacy testnets only; rejected on any
// production Lux mesh per HIP-0077.
PQModeBLS PQMode = iota
// PQModeNasua — BLS + the academic Nasua 2-round LWE threshold
// signature. Hash profile is **BLAKE3** (`primitives/hash.go` uses
// `github.com/zeebo/blake3` for every challenge / share / transcript
// binding). Trusted-dealer DKG, fixed federation, no proactive
// resharing. Suitable for bridge MPC and small fixed-membership
// federations; **not** an open public chain stance because it has no
// DKG that survives epoch rotation, and BLAKE3 is outside the NIST
// FIPS 202 approved hash family so this mode is **not** FIPS-approvable.
// Provided by `github.com/luxfi/nasua` (academic port).
PQModeNasua
// PQModePulsar — BLS + the Pulsar production fork of Corona. Same
// 2-round threshold algorithm as Corona, but Pulsar's canonical
// hash profile is **SHA-3 (cSHAKE256 / KMAC256 / TupleHash256,
// FIPS 202 + NIST SP 800-185)** and Pulsar adds the production
// lifecycle Corona lacks: Pedersen DKG over R_q with proper hiding,
// and proactive secret resharing for epoch validator rotation.
// Pulsar additionally ships a non-normative `Pulsar-BLAKE3` legacy
// suite for byte-equality regressions only.
// Suitable for open public chains with **same-committee** proactive
// resharing; cross-committee reshare in Pulsar v0.1 changes the
// master pubkey because the committee root is bound into the cSHAKE
// seed mix — cross-committee rotation is on the Pulsar v0.2 roadmap.
// Provided by `github.com/luxfi/pulsar`.
PQModePulsar
// PQModeQuasar — BLS + Pulsar + ML-DSA-65 rolled into a Groth16 proof
// anchored on Z-Chain. Two distinct lattice constructions backing
// each finality cert (Pulsar = R-LWE threshold; ML-DSA = MLWE per
// validator), both still constant-size in the cert. Strongest
// available; **Lux mesh default** per HIP-0077. Where the Z-Chain
// witness path is not yet wired in a given deployment, Quasar
// degrades to MLDSA semantics so the strong stance is preserved at
// higher bandwidth cost.
// Implemented by `lux/consensus/protocol/quasar`.
PQModeQuasar
// PQModeMLDSA — BLS + per-validator ML-DSA-65, no rollup, no
// threshold. Cert size: 48 + N*3309 B (linear in validator count).
// Audit-grade fallback when Quasar's Z-Chain Groth16 witness is
// unavailable, or when an external auditor needs every validator's
// raw signature in every cert. ML-DSA primitive comes from Pulsar's
// FIPS-204 implementation.
PQModeMLDSA
)
// String returns the canonical single-word name.
func (m PQMode) String() string {
switch m {
case PQModeBLS:
return "bls"
case PQModeNasua:
return "nasua"
case PQModePulsar:
return "pulsar"
case PQModeQuasar:
return "quasar"
case PQModeMLDSA:
return "mldsa"
default:
return fmt.Sprintf("pq-mode(%d)", uint8(m))
}
}
// PolicyID maps a PQMode to the canonical wire policy ID.
//
// Translation, kept here so we don't drag pkg/wire into config:
//
// bls -> 1 (PolicyQuorum)
// corona -> 5 (PolicyPQ, P+Q witness set, SHA-256 profile)
// pulsar -> 5 (PolicyPQ, P+Q witness set, SHA-3 profile)
// quasar -> 4 (PolicyQuantum, P+Q+Z witness set)
// mldsa -> 6 (PolicyPZ, P+Z witness set, per-validator ML-DSA)
//
// Corona and Pulsar share a wire PolicyID because the witness set on
// the wire is the same shape — the difference is the hash profile and
// DKG path, both negotiated out-of-band at network setup.
func (m PQMode) PolicyID() uint16 {
switch m {
case PQModeBLS:
return 1 // PolicyQuorum
case PQModeNasua, PQModePulsar:
return 5 // PolicyPQ
case PQModeQuasar:
return 4 // PolicyQuantum
case PQModeMLDSA:
return 6 // PolicyPZ
default:
return 1
}
}
// HashSuiteID is the wire byte that identifies which hash family a Quasar
// finality certificate was produced under. It is a separate axis from
// PolicyID: Pulsar and Corona share PolicyID 5 (P+Q witness set) but use
// different hash families (SHA-3 vs BLAKE3), so the policy byte alone is
// not enough for a receiver to know which kernel to instantiate.
//
// HashSuiteID is bound into the cert transcript so that flipping the byte
// post-signing changes the digest the threshold signature covers — a
// cross-suite confusion attack therefore fails on signature verification,
// not just on a string-equality check. See HIP-0077 §"Lux consensus PQ
// modes" and proofs/pulsar/hash-suite-separation.tex for the formal
// argument.
//
// **Numbering policy (NIST-aligned).** SHA3_NIST is the normative entry
// (0x01) so reviewers reading a cert envelope see the FIPS-aligned suite
// first. BLAKE3 keeps its own ID (0x02) but is marked LEGACY because it
// is outside the FIPS 202 family; non-normative for any NIST-track
// submission. SHAKE256 does NOT get its own ID — SHAKE256 is FIPS 202
// and therefore part of SHA3_NIST. Modes whose only PQ hash is SHAKE256
// (e.g. raw ML-DSA-65) advertise SHA3_NIST.
//
// The byte is open-ended: future hash families MUST claim the next free
// ID and pin it in pq_mode_test.go before any code references it. Never
// reuse a retired ID. The wire reserves 0x00 for "no hash-family
// commitment" so legacy BLS-only certs round-trip cleanly.
type HashSuiteID uint8
const (
// HashSuiteNone — no hash-family commitment. BLS-only modes whose
// threshold layer is absent do not pin a hash suite at the consensus
// layer; their kernels carry their own hash scoping internally.
HashSuiteNone HashSuiteID = 0x00
// HashSuiteSHA3NIST — the normative SP 800-185 hash family:
// SHAKE256 + cSHAKE256 + KMAC256 + TupleHash256 (FIPS 202 +
// NIST SP 800-185). The canonical Lux-mesh and Lux primary-network
// choice. Every NIST-track Pulsar variant (Pulsar.R production
// profile, Pulsar.M family) signs under this suite. ML-DSA-65 also
// rides here because SHAKE256 is FIPS 202 and not a separate family.
HashSuiteSHA3NIST HashSuiteID = 0x01
// HashSuiteBLAKE3Legacy — Corona academic profile and Pulsar's
// pre-pin legacy suite. BLAKE3 keyed XOF for every challenge / share
// / transcript binding (see github.com/luxfi/nasua
// primitives/hash.go and github.com/luxfi/corona/hash/blake3.go).
// BLAKE3 is outside FIPS 202 and therefore non-normative for any
// NIST submission; the byte exists so legacy / academic / federation-
// MPC deployments can still emit a verifiable cert.
HashSuiteBLAKE3Legacy HashSuiteID = 0x02
)
// String returns the canonical lower-case name of the hash suite. Empty
// string for HashSuiteNone so cert printers can omit the line cleanly.
func (h HashSuiteID) String() string {
switch h {
case HashSuiteNone:
return "none"
case HashSuiteSHA3NIST:
return "sha3-nist"
case HashSuiteBLAKE3Legacy:
return "blake3-legacy"
default:
return fmt.Sprintf("hash-suite(0x%02x)", uint8(h))
}
}
// IsNormative reports whether this hash suite is on the NIST-approved
// algorithm path (FIPS 202 family). Production Lux meshes MUST emit
// only normative suites; legacy/academic suites are bridge-MPC + audit
// fixtures only.
func (h HashSuiteID) IsNormative() bool {
return h == HashSuiteSHA3NIST
}
// HashSuiteID reports the wire HashSuiteID byte for this PQ mode. The
// byte is the receiver's only signal of which hash kernel to instantiate
// when verifying a cert built under this mode — Pulsar and Corona share
// PolicyID 5, so PolicyID alone cannot distinguish them.
//
// Closes HIP-0077 red-review F1 (silent finality forks between
// hash-profile islands sharing the same PolicyID).
//
// Mapping:
//
// bls -> HashSuiteNone (0x00) no hash-family commitment
// corona -> HashSuiteBLAKE3Legacy (0x02) academic profile, non-normative
// pulsar -> HashSuiteSHA3NIST (0x01) production profile (cSHAKE256 family)
// quasar -> HashSuiteSHA3NIST (0x01) same threshold-layer kernel as Pulsar
// mldsa -> HashSuiteSHA3NIST (0x01) ML-DSA-65 uses SHAKE256 (FIPS 202)
func (m PQMode) HashSuiteID() HashSuiteID {
switch m {
case PQModeBLS:
return HashSuiteNone
case PQModeNasua:
return HashSuiteBLAKE3Legacy
case PQModePulsar, PQModeQuasar, PQModeMLDSA:
return HashSuiteSHA3NIST
default:
return HashSuiteNone
}
}
// HashProfile reports the canonical hash-family name for the mode. This
// is the human-readable string form of HashSuiteID and stays consistent
// with it for every mode: "none" / "sha3-nist" / "blake3-legacy".
// Receivers SHOULD compare HashSuiteID bytes (cheap, exact); HashProfile
// exists for logs, error messages, and cross-language wire-debug only.
func (m PQMode) HashProfile() string {
return m.HashSuiteID().String()
}
// SigSchemeID is the wire byte that identifies which signature scheme
// produced the signature in this slot of a Quasar finality certificate.
// Orthogonal to PolicyID, HashSuiteID, and ProofSystemID.
//
// Bound into the cert transcript so a flipped byte breaks signature
// verification, not just type-equality.
//
// **Numbering blocks** (open-ended). The two ML-DSA-related blocks are
// kept distinct on purpose:
//
// - 0x40 block names the **raw single-party** primitive (ML-DSA-44/65/87
// per FIPS 204). Used when a per-validator signature appears in the
// cert without threshold aggregation (mldsa audit-grade fallback,
// identity attestations on Z-Chain, etc.).
//
// - 0x50 block names the **Pulsar-M threshold variant** at the same
// parameter sets. The verification *relation* is FIPS 204 ML-DSA.Verify
// for both blocks, but the producing protocol differs (single-party
// vs threshold) and the wire intentionally separates them so a
// receiver knows which kernel emitted the bytes.
//
// 0x00 — None / unspecified (BLS-only certs do not commit to a sig scheme)
// 0x10..0x1F — Classical (BLS-12-381 aggregate at 0x10)
// 0x20..0x2F — Corona (academic R-LWE)
// 0x30..0x3F — Pulsar.R (production R-LWE family)
// 0x40..0x4F — Raw ML-DSA per FIPS 204 (single-party):
// 0x41 = ML-DSA-44 (NIST PQ Cat 2)
// 0x42 = ML-DSA-65 (NIST PQ Cat 3)
// 0x43 = ML-DSA-87 (NIST PQ Cat 5)
// 0x50..0x5F — Pulsar-M threshold (M-LWE, FIPS 204-compatible):
// 0x51 = Pulsar-M-44 (NIST PQ Cat 2, devnet only)
// 0x52 = Pulsar-M-65 (NIST PQ Cat 3, **production default**)
// 0x53 = Pulsar-M-87 (NIST PQ Cat 5, high-value roots)
// 0x60.. — reserved for future variants
//
// New entries claim the next free integer in their block and MUST land in
// pq_mode_test.go before any code references them. Never reuse a retired ID.
type SigSchemeID uint8
const (
SigSchemeNone SigSchemeID = 0x00
SigSchemeBLS12381 SigSchemeID = 0x10
SigSchemeNasua SigSchemeID = 0x20
SigSchemePulsarR SigSchemeID = 0x30
// Raw single-party ML-DSA (FIPS 204).
SigSchemeMLDSA44 SigSchemeID = 0x41
SigSchemeMLDSA65 SigSchemeID = 0x42
SigSchemeMLDSA87 SigSchemeID = 0x43
// Pulsar-M threshold (M-LWE, output-interchangeable with FIPS 204).
SigSchemePulsar44 SigSchemeID = 0x51
SigSchemePulsar65 SigSchemeID = 0x52 // production default
SigSchemePulsar87 SigSchemeID = 0x53
)
// String returns the canonical wire name of the signature scheme.
func (s SigSchemeID) String() string {
switch s {
case SigSchemeNone:
return "none"
case SigSchemeBLS12381:
return "bls12-381"
case SigSchemeNasua:
return "nasua"
case SigSchemePulsarR:
return "pulsar-r"
case SigSchemeMLDSA44:
return "ml-dsa-44"
case SigSchemeMLDSA65:
return "ml-dsa-65"
case SigSchemeMLDSA87:
return "ml-dsa-87"
case SigSchemePulsar44:
return "pulsar-m-44"
case SigSchemePulsar65:
return "pulsar-m-65"
case SigSchemePulsar87:
return "pulsar-m-87"
default:
return fmt.Sprintf("sig-scheme(0x%02x)", uint8(s))
}
}
// IsPulsarM reports whether this scheme is in the Pulsar-M threshold
// family (0x50 block). Pulsar-M outputs verify under unmodified FIPS 204
// ML-DSA.Verify; the threshold protocol that produced them is what this
// flag separates from raw ML-DSA.
func (s SigSchemeID) IsPulsarM() bool {
return s == SigSchemePulsar44 ||
s == SigSchemePulsar65 ||
s == SigSchemePulsar87
}
// IsRawMLDSA reports whether this scheme is in the raw single-party
// ML-DSA block (0x40). These signatures are produced by FIPS 204
// ML-DSA.Sign (one private key, no threshold protocol); the same
// verifier accepts them as IsPulsarM outputs.
func (s SigSchemeID) IsRawMLDSA() bool {
return s == SigSchemeMLDSA44 ||
s == SigSchemeMLDSA65 ||
s == SigSchemeMLDSA87
}
// VerifiesUnderFIPS204 reports whether the unmodified FIPS 204 ML-DSA
// verifier will accept signatures emitted under this scheme. Both raw
// ML-DSA (0x40 block) and Pulsar-M (0x50 block) qualify; nothing else
// in the current enum does.
func (s SigSchemeID) VerifiesUnderFIPS204() bool {
return s.IsRawMLDSA() || s.IsPulsarM()
}
// SigSchemeID reports the default threshold-signature scheme for this
// PQ mode. The default is the production-grade choice; operators MAY
// override (e.g. Pulsar-M-87 for governance checkpoints) by passing an
// explicit SigSchemeID through the cert assembler.
//
// Mapping (HIP-0077 production defaults):
//
// bls -> SigSchemeNone BLS aggregate carried separately
// corona -> SigSchemeNasua federation MPC
// pulsar -> SigSchemePulsarR R-LWE production
// quasar -> SigSchemePulsarR threshold layer is Pulsar.R; Z-Chain witness separate
// mldsa -> SigSchemeMLDSA65 raw ML-DSA-65 (audit-grade); ops opt-in to Pulsar-M-65 explicitly
func (m PQMode) SigSchemeID() SigSchemeID {
switch m {
case PQModeBLS:
return SigSchemeNone
case PQModeNasua:
return SigSchemeNasua
case PQModePulsar, PQModeQuasar:
return SigSchemePulsarR
case PQModeMLDSA:
return SigSchemeMLDSA65
default:
return SigSchemeNone
}
}
// ProofPolicyID is the wire byte that identifies the **security class**
// (policy) the proof system implements. Orthogonal to ProofBackendID
// (which names the implementation). Strict-PQ deployments MUST accept
// only IDs whose `IsPostQuantum()` returns true; classical IDs are
// reserved as explicit forbidden markers so audit tooling can name a
// misconfiguration precisely.
//
// Numbering:
//
// 0x00 — None
// 0x10..0x1F — STARK / FRI policies (PQ-secure)
// 0x10 = STARK_FRI_SHA3_PQ canonical, NIST-aligned
// 0x11 = STARK_FRI_KECCAK_PQ Keccak Merkle (FIPS 202; non-NIST-recommended-default)
// 0x80..0xFF — Classical / forbidden in PQ mode
// 0x80 = GROTH16_BN254_CLASSICAL
// 0x81 = PLONK_KZG_CLASSICAL
// 0x82 = HALO_EC_CLASSICAL
// 0x83 = IPA_EC_CLASSICAL
//
// `ProofSystemID` is a deprecated alias for `ProofPolicyID` for one
// release; new code uses `ProofPolicyID`.
type ProofPolicyID uint8
// ProofSystemID is a deprecated type alias for ProofPolicyID. Existing
// callers continue to work; new code uses ProofPolicyID. Removed after
// the 2026-Aug-31 encoding-freeze gate.
type ProofSystemID = ProofPolicyID
const (
ProofPolicyNone ProofPolicyID = 0x00
ProofPolicySTARKFRISHA3PQ ProofPolicyID = 0x10 // canonical Z-Chain policy
ProofPolicySTARKFRIKeccak ProofPolicyID = 0x11 // FIPS 202 Keccak alternative
// Forbidden in strict-PQ mode. The wire keeps an ID for each so
// audit tooling can name a misconfiguration explicitly.
ProofPolicyGroth16BN254Forbid ProofPolicyID = 0x80
ProofPolicyPLONKKZGForbid ProofPolicyID = 0x81
ProofPolicyHaloECForbid ProofPolicyID = 0x82
ProofPolicyIPAECForbid ProofPolicyID = 0x83
// Legacy aliases — keep callers compiling through the rename.
// New code uses the ProofPolicy* names.
ProofSystemNone = ProofPolicyNone
ProofSystemSTARKFRISHA3PQ = ProofPolicySTARKFRISHA3PQ
ProofSystemSTARKFRIKeccakPQ = ProofPolicySTARKFRIKeccak
ProofSystemGroth16BN254Forbid = ProofPolicyGroth16BN254Forbid
ProofSystemKZGForbid = ProofPolicyPLONKKZGForbid
)
// String returns the canonical wire name.
func (p ProofPolicyID) String() string {
switch p {
case ProofPolicyNone:
return "none"
case ProofPolicySTARKFRISHA3PQ:
return "stark-fri-sha3-pq"
case ProofPolicySTARKFRIKeccak:
return "stark-fri-keccak-pq"
case ProofPolicyGroth16BN254Forbid:
return "groth16-bn254-classical-forbidden-in-pq"
case ProofPolicyPLONKKZGForbid:
return "plonk-kzg-classical-forbidden-in-pq"
case ProofPolicyHaloECForbid:
return "halo-ec-classical-forbidden-in-pq"
case ProofPolicyIPAECForbid:
return "ipa-ec-classical-forbidden-in-pq"
default:
return fmt.Sprintf("proof-policy(0x%02x)", uint8(p))
}
}
// IsPostQuantum reports whether this policy is acceptable in strict-PQ
// mode. Strict-PQ producers MUST emit only IsPostQuantum=true policies;
// verifiers in strict-PQ mode MUST refuse anything else.
func (p ProofPolicyID) IsPostQuantum() bool {
switch p {
case ProofPolicySTARKFRISHA3PQ,
ProofPolicySTARKFRIKeccak:
return true
}
return false
}
// IsForbiddenInPQMode reports whether this proof policy carries the
// explicit forbidden marker. Used by audit tooling to detect a
// misconfiguration where a classical proof would otherwise sneak past
// a PQ-only deployment.
func (p ProofPolicyID) IsForbiddenInPQMode() bool {
return p == ProofPolicyGroth16BN254Forbid ||
p == ProofPolicyPLONKKZGForbid ||
p == ProofPolicyHaloECForbid ||
p == ProofPolicyIPAECForbid
}
// ProofBackendID is the wire byte that identifies which **implementation**
// produced a Z-Chain proof. Orthogonal to ProofPolicyID (which names the
// security class). This separation lets Z-Chain benchmark SP1, RISC Zero,
// P3Q, Stone (Cairo), and Stwo (Circle STARK) without changing chain
// semantics — they all satisfy the same ProofPolicyID, just with
// different underlying implementations.
//
// Numbering:
//
// 0x00 — None (no backend; e.g. policy-only declarations)
// 0x10..0x1F — STARK / FRI implementations (production)
// 0x20 = RISC0_SUCCINCT_STARK_PQ RISC Zero succinct receipt path (no Groth16 wrapper)
// 0x21 = SP1_COMPRESSED_STARK_PQ SP1 compressed STARK (no Groth16 wrapper)
// 0x22 = P3Q_STARK_FRI_SHA3_PQ Lux P3Q (Plonky3 fork, cSHAKE256 Merkle)
// 0x23 = STONE_CAIRO_STARK_PQ Cairo / Stone backend
// 0x24 = STWO_CIRCLE_STARK_PQ Stwo Circle STARK
// 0x30..0x3F — Direct full-node verification (no succinct proof). The
// "proof" is the certificate itself: each signer's
// unmodified FIPS 204/205 signature, a weighted-validator-set
// Merkle inclusion path, and a weight-threshold predicate.
// Soundness rests ONLY on the underlying FIPS verifier plus
// Merkle second-preimage resistance — there is no
// succinctness layer and therefore no STARK/SNARK trusted
// component to audit. A STARK compression backend over the
// SAME relation is a separate id in the 0x10/0x20 block.
// 0x30 = DIRECT_WEIGHTED_QUORUM_PQ per-signer FIPS verify + Merkle + weight predicate
// 0x70..0x7F — Dev-only / non-production (CI, testnet, audit fuzzing)
// 0x70 = RISC0_RAW_STARK_DEV
// 0x71 = SP1_CORE_STARK_DEV
// 0x80..0xFF — Forbidden in strict-PQ mode (mirror of ProofPolicyID
// forbidden block; backends that wrap classical wrappers
// carry the same explicit refusal markers).
type ProofBackendID uint8
const (
ProofBackendNone ProofBackendID = 0x00
ProofBackendRISC0SuccinctSTARK ProofBackendID = 0x20
ProofBackendSP1CompressedSTARK ProofBackendID = 0x21
ProofBackendP3QSTARKFRISHA3 ProofBackendID = 0x22
ProofBackendStoneCairoSTARK ProofBackendID = 0x23
ProofBackendStwoCircleSTARK ProofBackendID = 0x24
// ProofBackendDirectWeightedQuorum is the direct full-node-verifiable
// weighted quorum certificate backend (HIP-0079 §"full-node-verifiable
// mode"). Verification is direct: for each signer record, run unmodified
// FIPS 204 (ML-DSA) or FIPS 205 (SLH-DSA) Verify over the
// domain-separated consensus message, check the weighted-validator-set
// Merkle inclusion path, then assert the accumulated voting weight meets
// the quorum threshold. No succinct proof, no recursion, no trusted
// setup; the per-signer signatures ARE the proof. This is threshold
// CERTIFICATION, not threshold signing — no key material is shared,
// reconstructed, or combined. See protocol/quasar/quorum_cert.go.
ProofBackendDirectWeightedQuorum ProofBackendID = 0x30
// Dev-only — never produced in production strict-PQ deployments.
ProofBackendRISC0RawSTARKDev ProofBackendID = 0x70
ProofBackendSP1CoreSTARKDev ProofBackendID = 0x71
// Forbidden in strict-PQ mode. Mirrors ProofPolicyID forbidden block
// at the backend layer (e.g. a backend that wraps STARK in Groth16
// for cheaper EVM verification).
ProofBackendGroth16WrapForbid ProofBackendID = 0x80
ProofBackendKZGWrapForbid ProofBackendID = 0x81
)
// String returns the canonical wire name.
func (b ProofBackendID) String() string {
switch b {
case ProofBackendNone:
return "none"
case ProofBackendRISC0SuccinctSTARK:
return "risc0-succinct-stark-pq"
case ProofBackendSP1CompressedSTARK:
return "sp1-compressed-stark-pq"
case ProofBackendP3QSTARKFRISHA3:
return "p3q-stark-fri-sha3-pq"
case ProofBackendStoneCairoSTARK:
return "stone-cairo-stark-pq"
case ProofBackendStwoCircleSTARK:
return "stwo-circle-stark-pq"
case ProofBackendDirectWeightedQuorum:
return "direct-weighted-quorum-pq"
case ProofBackendRISC0RawSTARKDev:
return "risc0-raw-stark-dev"
case ProofBackendSP1CoreSTARKDev:
return "sp1-core-stark-dev"
case ProofBackendGroth16WrapForbid:
return "groth16-wrap-classical-forbidden-in-pq"
case ProofBackendKZGWrapForbid:
return "kzg-wrap-classical-forbidden-in-pq"
default:
return fmt.Sprintf("proof-backend(0x%02x)", uint8(b))
}
}
// IsProductionPQ reports whether this backend is acceptable in strict-PQ
// production mode. Dev backends (0x70 block) and forbidden backends
// (0x80 block) return false.
//
// ProofBackendDirectWeightedQuorum qualifies: its soundness rests on
// unmodified FIPS 204/205 verification plus Merkle second-preimage
// resistance, which is strictly inside the NIST-approved algorithm set
// (no classical EC, no trusted setup). It is the most conservative
// production-PQ posture — there is no succinctness assumption to break.
func (b ProofBackendID) IsProductionPQ() bool {
switch b {
case ProofBackendRISC0SuccinctSTARK,
ProofBackendSP1CompressedSTARK,
ProofBackendP3QSTARKFRISHA3,
ProofBackendStoneCairoSTARK,
ProofBackendStwoCircleSTARK,
ProofBackendDirectWeightedQuorum:
return true
}
return false
}
// IsForbiddenInPQMode mirrors ProofPolicyID.IsForbiddenInPQMode at the
// backend layer.
func (b ProofBackendID) IsForbiddenInPQMode() bool {
return b == ProofBackendGroth16WrapForbid ||
b == ProofBackendKZGWrapForbid
}
// IdentitySchemeID is the wire byte that identifies which signature scheme
// a validator uses for **identity** (registration, key rotation,
// revocation, attestation) — distinct from the threshold-finality
// SigSchemeID. Identity signatures are single-party, FIPS 204 ML-DSA.
//
// Numbering — block 0x40 mirrors raw ML-DSA in SigSchemeID so the byte
// pattern is consistent across the wire.
//
// 0x00 — None (no identity scheme committed; legacy / anonymous)
// 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)
type IdentitySchemeID uint8
const (
IdentitySchemeNone IdentitySchemeID = 0x00
IdentitySchemeMLDSA44 IdentitySchemeID = 0x41
IdentitySchemeMLDSA65 IdentitySchemeID = 0x42 // production default
IdentitySchemeMLDSA87 IdentitySchemeID = 0x43
)
// String returns the canonical wire name.
func (i IdentitySchemeID) String() string {
switch i {
case IdentitySchemeNone:
return "none"
case IdentitySchemeMLDSA44:
return "ml-dsa-44"
case IdentitySchemeMLDSA65:
return "ml-dsa-65"
case IdentitySchemeMLDSA87:
return "ml-dsa-87"
default:
return fmt.Sprintf("identity-scheme(0x%02x)", uint8(i))
}
}
// IsFIPS204 reports whether this identity scheme uses unmodified
// FIPS 204 ML-DSA verification. All defined non-None values do; the
// helper exists so audit tooling can assert this property explicitly.
func (i IdentitySchemeID) IsFIPS204() bool {
return i == IdentitySchemeMLDSA44 ||
i == IdentitySchemeMLDSA65 ||
i == IdentitySchemeMLDSA87
}
// Note: ProofFormatID and VerifierID are defined in security_profile.go
// because they are part of the locked-profile axis set, not the
// mode-selection enum group above. Look there for their numbering tables
// and behaviour predicates.
// DKGRequired reports whether an open public chain can run this mode.
// Modes whose threshold layer relies on a trusted dealer (Corona) cannot;
// modes with no threshold (BLS, MLDSA) are vacuously fine; modes with
// proper DKG (Pulsar, Quasar) are the production target.
func (m PQMode) DKGRequired() string {
switch m {
case PQModeBLS, PQModeMLDSA:
return "none"
case PQModeNasua:
return "trusted-dealer" // unsuitable for open public chains
case PQModePulsar, PQModeQuasar:
return "pedersen-dkg-over-rq"
default:
return "unknown"
}
}
// IsPostQuantum reports whether the mode carries any PQ witness on top of
// BLS. False only for PQModeBLS.
func (m PQMode) IsPostQuantum() bool {
return m != PQModeBLS
}
// SuitableForPublicChain reports whether the mode is appropriate for an
// open public chain (epoch rotation, no trusted dealer). False for
// PQModeNasua (trusted-dealer DKG only) and PQModeBLS (no PQ at all);
// true for everything else.
func (m PQMode) SuitableForPublicChain() bool {
switch m {
case PQModePulsar, PQModeQuasar, PQModeMLDSA:
return true
default:
return false
}
}
// =============================================================================
// End-to-end PQ enums (HIP-0078 §"E2E PQ surface").
//
// These five axes are orthogonal to the threshold / finality axis above:
// they pin what the chain accepts at the **wallet**, **transaction
// authorisation**, **contract authorisation**, **key exchange**, and
// **recovery** layers respectively. Each axis carries its own forbidden
// marker block (0x90+) so an audit pipeline can name a classical leakage
// explicitly rather than detecting it by absence.
//
// Numbering convention shared across the five enums:
//
// 0x00 — Invalid / unspecified (rejected by every strict verifier)
// 0x01..0x0F — KEM family (reserved on KeyExchangeID; unused on schemes)
// 0x05..0x07 — SLH-DSA stateless backstop (RecoverySchemeID only)
// 0x10..0x2F — Multi-party / proof-carrying composite schemes
// 0x42..0x43 — ML-DSA-65 / ML-DSA-87 (FIPS 204 lattice signature)
// 0x90.. — Forbidden in strict-PQ mode (ECDSA / X25519 / classical BLS)
// 0xFF — None sentinel (RecoverySchemeID only — high-value M-DSA-87
// provides the backstop without a separate stateless scheme)
//
// Renumbering an existing entry breaks every state-root and envelope ever
// signed under it. Adding an entry only requires a new free integer in the
// appropriate block.
// =============================================================================
// WalletSchemeID identifies the signature scheme a wallet uses to sign
// outbound transactions. The wallet axis is independent of validator
// identity (IdentitySchemeID), threshold finality (SigSchemeID), and
// contract authorisation (ContractAuthID) — a single chain may pin
// different schemes per axis. Strict-PQ chains pin a lattice scheme here
// and refuse ECDSA wallets via ChainSecurityProfile.ForbidECDSAWallets.
type WalletSchemeID uint8
const (
WalletSchemeInvalid WalletSchemeID = 0x00
WalletSchemeMLDSA65 WalletSchemeID = 0x42 // FIPS 204 Cat 3 (production default)
WalletSchemeMLDSA87 WalletSchemeID = 0x43 // FIPS 204 Cat 5 (high-value)
WalletSchemeECDSAUnsafe WalletSchemeID = 0x90 // classical ECDSA; refused in strict-PQ
)
// String returns the canonical wire name.
func (w WalletSchemeID) String() string {
switch w {
case WalletSchemeInvalid:
return "invalid"
case WalletSchemeMLDSA65:
return "ml-dsa-65"
case WalletSchemeMLDSA87:
return "ml-dsa-87"
case WalletSchemeECDSAUnsafe:
return "ecdsa-unsafe-classical-forbidden-in-pq"
default:
return fmt.Sprintf("wallet-scheme(0x%02x)", uint8(w))
}
}
// IsPostQuantum reports whether this wallet scheme is acceptable in
// strict-PQ mode. ML-DSA-65 and ML-DSA-87 qualify; classical ECDSA does
// not.
func (w WalletSchemeID) IsPostQuantum() bool {
return w == WalletSchemeMLDSA65 || w == WalletSchemeMLDSA87
}
// IsForbiddenInPQMode reports whether this wallet scheme carries the
// explicit forbidden marker. Used by audit tooling to detect a
// misconfiguration where a classical wallet would otherwise sneak past
// a PQ-only deployment.
func (w WalletSchemeID) IsForbiddenInPQMode() bool {
return w == WalletSchemeECDSAUnsafe
}
// TxSchemeID identifies the signature scheme a transaction's outer
// authorisation envelope was signed under. Distinct from WalletSchemeID
// (which names the wallet's identity key): tx auth may be delegated to
// a session key, a typed multisig, or a wallet's primary key — the
// scheme byte names the cryptographic primitive that produced the
// signature on the wire.
type TxSchemeID uint8
const (
TxSchemeInvalid TxSchemeID = 0x00
TxSchemeMLDSA65 TxSchemeID = 0x42
TxSchemeMLDSA87 TxSchemeID = 0x43
TxSchemeECDSAUnsafe TxSchemeID = 0x90
)
// String returns the canonical wire name.
func (t TxSchemeID) String() string {
switch t {
case TxSchemeInvalid:
return "invalid"
case TxSchemeMLDSA65:
return "ml-dsa-65"
case TxSchemeMLDSA87:
return "ml-dsa-87"
case TxSchemeECDSAUnsafe:
return "ecdsa-unsafe-classical-forbidden-in-pq"
default:
return fmt.Sprintf("tx-scheme(0x%02x)", uint8(t))
}
}
// IsPostQuantum reports whether this tx scheme is acceptable in
// strict-PQ mode.
func (t TxSchemeID) IsPostQuantum() bool {
return t == TxSchemeMLDSA65 || t == TxSchemeMLDSA87
}
// IsForbiddenInPQMode reports whether this tx scheme carries the
// explicit forbidden marker.
func (t TxSchemeID) IsForbiddenInPQMode() bool {
return t == TxSchemeECDSAUnsafe
}
// ContractAuthID identifies the authorisation primitive a smart contract
// uses to gate a call. Broader than TxSchemeID: a contract may accept
// raw lattice signatures, multi-party threshold signatures, session-
// scoped capabilities, or Z-Chain validity proofs. The byte names the
// kind; the contract's authoriser policy names which kinds it accepts.
//
// The 0x10–0x2F block covers composite primitives (multi-party,
// proof-carrying); the 0x40 block mirrors raw FIPS 204 ML-DSA; the
// 0x90+ block names classical primitives that are explicitly forbidden
// in strict-PQ mode (ECDSA and classical BLS aggregates).
type ContractAuthID uint8
const (
ContractAuthInvalid ContractAuthID = 0x00
ContractAuthZChainProof ContractAuthID = 0x10 // Z-Chain validity proof
ContractAuthMultisigMLDSA ContractAuthID = 0x20 // threshold ML-DSA multisig
ContractAuthSessionPQ ContractAuthID = 0x21 // PQ session capability
ContractAuthMLDSA65 ContractAuthID = 0x42
ContractAuthMLDSA87 ContractAuthID = 0x43
ContractAuthECDSAUnsafe ContractAuthID = 0x90
ContractAuthBLSUnsafe ContractAuthID = 0x91 // classical BLS aggregate
)
// String returns the canonical wire name.
func (c ContractAuthID) String() string {
switch c {
case ContractAuthInvalid:
return "invalid"
case ContractAuthZChainProof:
return "z-chain-proof"
case ContractAuthMultisigMLDSA:
return "multisig-ml-dsa"
case ContractAuthSessionPQ:
return "session-pq"
case ContractAuthMLDSA65:
return "ml-dsa-65"
case ContractAuthMLDSA87:
return "ml-dsa-87"
case ContractAuthECDSAUnsafe:
return "ecdsa-unsafe-classical-forbidden-in-pq"
case ContractAuthBLSUnsafe:
return "bls-unsafe-classical-forbidden-in-pq"
default:
return fmt.Sprintf("contract-auth(0x%02x)", uint8(c))
}
}
// IsPostQuantum reports whether this contract-auth primitive is
// acceptable in strict-PQ mode. Lattice signatures, lattice multisigs,
// PQ session capabilities, and Z-Chain proofs qualify; classical ECDSA
// and classical BLS aggregates do not.
func (c ContractAuthID) IsPostQuantum() bool {
switch c {
case ContractAuthZChainProof,
ContractAuthMultisigMLDSA,
ContractAuthSessionPQ,
ContractAuthMLDSA65,
ContractAuthMLDSA87:
return true
}
return false
}
// IsForbiddenInPQMode reports whether this contract-auth primitive
// carries the explicit forbidden marker.
func (c ContractAuthID) IsForbiddenInPQMode() bool {
return c == ContractAuthECDSAUnsafe || c == ContractAuthBLSUnsafe
}
// KeyExchangeID identifies the KEM (Key Encapsulation Mechanism) used
// for session key establishment, encrypted-state-transition envelopes,
// and operator-to-operator key wrapping. Distinct from every signature
// axis: a KEM provides confidentiality, not authenticity. Strict-PQ
// chains pin a lattice KEM (ML-KEM, FIPS 203) and refuse classical
// X25519 via ChainSecurityProfile.ForbidClassicalKEM.
//
// F103 KEM semantics — chain-wide vs TLS-layer:
//
// The profile field names the post-quantum KEM component that MUST be
// present on the wire. A peer-to-peer TLS handshake is permitted to
// negotiate the IANA-registered hybrid X25519MLKEM768 (0x11ec) as the
// curve preference: the hybrid is STRICTLY STRONGER than pure
// ML-KEM-768 (an attacker must break BOTH X25519 AND ML-KEM to derive
// the session key) and is the format real-world TLS 1.3 stacks
// implement today. The profile's KeyExchangeMLKEM768 value therefore
// reads as "the ML-KEM-768 component is present", not "X25519 is
// forbidden in this layer". ForbidClassicalKEM remains the gate that
// refuses a pure-classical curve (e.g. an X25519-only handshake) at
// the application layer; it does NOT refuse a hybrid that includes
// ML-KEM-768.
//
// Forks that pin a strictly-pure ML-KEM-768 TLS posture do so by
// overriding the chain's KeyExchangeID, NOT by adding a separate
// "pure vs hybrid" enum byte: every additional axis is an additional
// downstream verifier obligation, and the audit signed off on the
// single-byte KeyExchangeID surface.
type KeyExchangeID uint8
const (
KeyExchangeInvalid KeyExchangeID = 0x00
KeyExchangeMLKEM768 KeyExchangeID = 0x01 // FIPS 203 Cat 3 (production default; hybrid X25519MLKEM768 also acceptable at TLS layer)
KeyExchangeMLKEM1024 KeyExchangeID = 0x02 // FIPS 203 Cat 5 (high-value)
KeyExchangeX25519Unsafe KeyExchangeID = 0x90 // pure classical X25519; refused in strict-PQ via ForbidClassicalKEM
)
// String returns the canonical wire name.
func (k KeyExchangeID) String() string {
switch k {
case KeyExchangeInvalid:
return "invalid"
case KeyExchangeMLKEM768:
return "ml-kem-768"
case KeyExchangeMLKEM1024:
return "ml-kem-1024"
case KeyExchangeX25519Unsafe:
return "x25519-unsafe-classical-forbidden-in-pq"
default:
return fmt.Sprintf("key-exchange(0x%02x)", uint8(k))
}
}
// IsPostQuantum reports whether this KEM is acceptable in strict-PQ
// mode. ML-KEM-768 and ML-KEM-1024 qualify; classical X25519 does not.
func (k KeyExchangeID) IsPostQuantum() bool {
return k == KeyExchangeMLKEM768 || k == KeyExchangeMLKEM1024
}
// IsForbiddenInPQMode reports whether this KEM carries the explicit
// forbidden marker.
func (k KeyExchangeID) IsForbiddenInPQMode() bool {
return k == KeyExchangeX25519Unsafe
}
// RecoverySchemeID identifies the signature scheme used by the chain's
// account-recovery / breakglass key. Two valid posture choices:
//
// - A stateless hash-based scheme (SLH-DSA, FIPS 205) at parameter
// set 128 / 192 / 256. Stateless backstop: even if every lattice
// assumption falls, recovery still works.
// - ML-DSA-87 (FIPS 204 Cat 5). Acceptable ONLY when the chain's
// HighValueSchemeID is also ML-DSA-87 / Pulsar-M-87 — the
// high-value scheme then provides the strong-classical fallback
// that recovery would otherwise need.
// - RecoverySchemeNone is permitted iff HighValueSchemeID is
// ML-DSA-87 or Pulsar-M-87 (no stateless backstop required when
// the high-value path is already at Cat 5).
type RecoverySchemeID uint8
const (
RecoverySchemeInvalid RecoverySchemeID = 0x00
RecoverySchemeSLHDSA128 RecoverySchemeID = 0x05 // FIPS 205 Cat 1
RecoverySchemeSLHDSA192 RecoverySchemeID = 0x06 // FIPS 205 Cat 3 (production default)
RecoverySchemeSLHDSA256 RecoverySchemeID = 0x07 // FIPS 205 Cat 5
RecoverySchemeMLDSA87 RecoverySchemeID = 0x43 // FIPS 204 Cat 5 (acceptable if HighValue is also Cat 5)
RecoverySchemeNone RecoverySchemeID = 0xFF // permitted only when HighValue is Cat 5
)
// String returns the canonical wire name.
func (r RecoverySchemeID) String() string {
switch r {
case RecoverySchemeInvalid:
return "invalid"
case RecoverySchemeSLHDSA128:
return "slh-dsa-128"
case RecoverySchemeSLHDSA192:
return "slh-dsa-192"
case RecoverySchemeSLHDSA256:
return "slh-dsa-256"
case RecoverySchemeMLDSA87:
return "ml-dsa-87"
case RecoverySchemeNone:
return "none"
default:
return fmt.Sprintf("recovery-scheme(0x%02x)", uint8(r))
}
}
// IsPostQuantum reports whether this recovery scheme is acceptable in
// strict-PQ mode. SLH-DSA (stateless hash-based) and ML-DSA-87 qualify;
// the None sentinel is policy-dependent (acceptable iff HighValue is
// Cat 5) and returns true here so the post-quantum predicate stays
// orthogonal to the cross-axis validity gate.
func (r RecoverySchemeID) IsPostQuantum() bool {
switch r {
case RecoverySchemeSLHDSA128,
RecoverySchemeSLHDSA192,
RecoverySchemeSLHDSA256,
RecoverySchemeMLDSA87,
RecoverySchemeNone:
return true
}
return false
}
// IsForbiddenInPQMode reports whether this recovery scheme carries the
// explicit forbidden marker. The current numbering reserves no 0x90+
// recovery scheme — any classical recovery posture is simply absent
// from the enum, so the predicate is trivially false today. Kept for
// API symmetry with the other E2E enums and for future-proofing.
func (r RecoverySchemeID) IsForbiddenInPQMode() bool {
return false
}
// IsStateless reports whether this recovery scheme is a stateless
// hash-based signature (SLH-DSA / FIPS 205). True for the 0x05..0x07
// block; false for ML-DSA-87 (lattice, stateful key tree implicit in
// the protocol) and for the None sentinel.
//
// Audit tooling uses IsStateless to confirm a chain's breakglass key
// has the strongest available standalone-of-lattice property: a
// stateless hash-based recovery survives even a complete break of
// lattice assumptions.
func (r RecoverySchemeID) IsStateless() bool {
switch r {
case RecoverySchemeSLHDSA128,
RecoverySchemeSLHDSA192,
RecoverySchemeSLHDSA256:
return true
}
return false
}