Files
warp/pulsar/classification.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

96 lines
3.6 KiB
Go

// Copyright (C) 2019-2026, Lux Industries Inc. All rights reserved.
// See the file LICENSE for licensing terms.
// classification.go — Horizon-final envelope-SHAPE classifiers. The
// post-quantum lane structure is documented in LP-105 ("Lux stack lexicon",
// Proof-lane classification subsection) and
// proofs/definitions/finality-definitions.tex Definition ref:proof-lane and
// Remark ref:groth16-not-pq.
//
// Two envelope-shape predicates ship here:
//
// IsPQFinal(envelope) — true iff the envelope carries enough
// certificate-lane evidence to be Horizon-final under Prism. A
// Groth16 wrapper alone is NOT enough; a real Corona signature is required.
//
// HorizonFinalErr(envelope) — returns an error explaining WHY an
// envelope is not Horizon-final, or nil if it is.
//
// The proof-system PQ classifier IsPQRootOfTrust(provingSystem) is a
// subject-AGNOSTIC policy primitive and now lives in the warp ROOT package
// (warp.IsPQRootOfTrust) so both the warp envelope path AND the quasar
// consensus policy can reuse it without an import cycle. Use it from there.
//
// These helpers do NOT verify signatures; they classify the envelope
// SHAPE. Use VerifyWithOptions / VerifyPQLanes to check the bytes.
package pulsar
import (
"errors"
"strings"
"github.com/luxfi/warp"
)
// ErrNotHorizonFinal is returned when an envelope cannot be lifted into
// a Horizon-final certificate. It typically means the envelope is
// missing one or more of the Beam / ML-DSA / Corona lanes that Prism
// requires (Definition ref:prism in finality-definitions.tex).
var ErrNotHorizonFinal = errors.New("warp pulsar: envelope is not Horizon-final")
// IsPQFinal reports whether the envelope carries enough evidence to
// be Horizon-final under Prism. The required shape, per
// proofs/definitions/finality-definitions.tex Definition
// ref:horizon-cert, is:
//
// - A v1 Beam (BLS aggregate over the v1 Message). Always present;
// the v1 Message field is non-nil.
// - An ML-DSA cert set (either raw FIPS-204 signatures or a
// compressed Groth16 rollup of those signatures). The shape is
// the same on the wire.
// - A Corona Ringtail signature — a real Module-LWE lattice threshold
// signature over the envelope transcript. This is the PQ root of
// trust; without it the envelope is not Horizon-final, regardless of
// how impressive the wrapper proof attached to the ML-DSA lane is.
//
// In particular: an envelope carrying a Groth16 rollup of ML-DSA
// verification but no Corona signature is NOT Horizon-final. The Groth16
// wrapper is a classical compatibility / compression / privacy adapter
// (see warp.IsPQRootOfTrust); it does not contribute PQ liveness on its
// own.
func IsPQFinal(env *warp.Envelope) bool {
if env == nil {
return false
}
if !env.HasMLDSACertSet() {
return false
}
if !env.HasCorona() {
return false
}
return true
}
// HorizonFinalErr returns nil when env is Horizon-final-shaped, or
// ErrNotHorizonFinal wrapping a structured reason string when it is
// not. Use this in receivers that want to distinguish between
// "envelope shape OK, signature failed" (returned by VerifyV2) and
// "envelope shape inadmissible".
func HorizonFinalErr(env *warp.Envelope) error {
if env == nil {
return ErrNotHorizonFinal
}
missing := make([]string, 0, 3)
if !env.HasMLDSACertSet() {
missing = append(missing, "MLDSACertSet")
}
if !env.HasCorona() {
missing = append(missing, "CoronaRingtail")
}
if len(missing) > 0 {
return errors.New("warp pulsar: envelope missing required lanes: " + strings.Join(missing, ", "))
}
return nil
}