Files
zeekay 954cd81ecf fix(warp): close red-review findings on the typed evidence model (R1/R2/R3/R4)
R1 (MEDIUM): bind P3Q ProvingSystem to the verified proof. VerifyP3QRollup now
returns the proof system it ACTUALLY verified; the dispatcher rejects
(ErrP3QProvingSystemMismatch) if it differs from the claimed ProvingSystem the
strict-PQ gate keys on — a classical proof relabeled "stark-rescue" can no longer
pass strict finality. New TestP3QProvingSystemMismatchRejected.

R2 (MEDIUM): unexport the bytes dispatcher (VerifyFinalityEvidence ->
verifyFinalityEvidence). It is NOT a finality verdict (returns nil for the
non-strict cert-set / an ungated classical P3Q lane); unexporting forces
consumers through AcceptQuasarCert (consensus M) / VerifyWithOptions (cross-chain
D) — the only finality entry points — removing the footgun before the API freezes.

R3: document the verify-path SECURITY PRECONDITIONS on LaneVerifierSet — resolver
authenticity (keys MUST come from the authenticated on-chain key lineage) and
per-lane key independence (no shared Pulsar/Corona seed, else dual-PQ collapses to
single-PQ).

R4 (LOW): the Pulsar import-hygiene guard now t.Fatal's on go-list failure instead
of silently skipping a load-bearing check.

Full warp suite green (23 tests).
2026-06-27 14:08:56 -07:00

167 lines
7.0 KiB
Go

// Copyright (C) 2019-2026, Lux Industries Inc. All rights reserved.
// See the file LICENSE for licensing terms.
// p3q.go — the P3Q rollup lane (LP-218, "Post-Quantum Pulsar Proof") and the
// proof-system PQ classification that gates it.
//
// A P3Q root is a succinct proof that a weighted quorum of INDEPENDENT
// ML-DSA-65 signatures over the subject verified. It compresses an O(n)
// cert-set (EvidenceMLDSACertSet — its INPUT) into an O(1) root. The real
// prover/verifier is the Plonky3 FRI rollup at precompile 0x012205; it lives
// ABOVE warp (the precompile module imports warp + consensus) and is INJECTED
// as a P3QRollupVerifier. The warp verify path NEVER imports it — doing so
// would invert the dependency DAG (warp → precompile → warp).
//
// CRITICAL fail-closed rule: a P3Q root is only a POST-QUANTUM root of trust
// when its underlying proof SYSTEM is post-quantum. A Groth16 (pairing-based,
// Shor-broken) wrapper of ML-DSA verification is a CLASSICAL succinct proof of
// post-quantum signature verification — it is NOT a PQ root of trust. Such a
// root may satisfy a non-strict tier, but is REFUSED as a strict-PQ finality
// root unless policy explicitly opts in (P3QStrictRootOK / AcceptQuasarCert).
package warp
import (
"errors"
"strings"
"github.com/luxfi/ids"
)
// P3Q errors.
var (
// ErrP3QClassicalRoot is returned when a P3Q root backed by a classical
// (non-PQ) proof system is offered as a strict-PQ finality root without an
// explicit policy opt-in. The canonical case is a Groth16 rollup.
ErrP3QClassicalRoot = errors.New("warp: P3Q root proof system is not post-quantum; refused as strict-PQ finality root")
// ErrP3QVerifierUnavailable is the fail-closed result of the in-package
// P3Q stub verifier. The real FRI rollup verifier is injected from above
// warp; until one is wired, P3Q verification fails closed.
ErrP3QVerifierUnavailable = errors.New("warp: no P3Q rollup verifier wired (real verifier is injected above warp)")
// ErrSignerSetUnresolved is returned when a P3Q verifier could not resolve
// the signer set / weights it must check the rollup threshold against.
ErrSignerSetUnresolved = errors.New("warp: signer-set authority failed")
// ErrP3QProvingSystemMismatch is returned when a P3Q verifier reports it
// proved a DIFFERENT proof system than the evidence's claimed ProvingSystem.
// It binds the self-declared system (which the strict-PQ gate keys on) to the
// authenticated proof, so a classical root relabeled as post-quantum fails
// closed.
ErrP3QProvingSystemMismatch = errors.New("warp: P3Q proving system does not match the verified proof")
)
// P3QRoot is the typed P3Q rollup lane payload: the succinct root + proof, the
// proof system that produced it, and the quorum context the verifier checks
// against. The chain sees a compact root, not the underlying ML-DSA cert set.
type P3QRoot struct {
// SignerSetID identifies the validator set the rolled-up ML-DSA-65 keys
// belong to. Folded into M so the subject commits to it.
SignerSetID ids.ID
// EraHandle selects the signer-set snapshot (e.g. block height) the
// SignerSetAuthority resolves keys + weights against.
EraHandle uint64
// Root is the succinct commitment the proof opens (e.g. a Merkle/FRI root
// over the verified-signer set).
Root []byte
// Proof is the succinct argument bytes (e.g. a FRI/STARK proof).
Proof []byte
// ProvingSystem names the proof system — "stark-rescue", "lattice-zk",
// "groth16", … — and is the input to IsPQRootOfTrust. It determines
// whether this root can be a STRICT-PQ finality root.
ProvingSystem string
// Threshold is the weighted quorum the rollup attests was met among the
// independent ML-DSA-65 signers.
Threshold WeightThreshold
// SuiteID pins the lane suite (SuiteP3QMLDSARollup).
SuiteID SuiteID
}
// ValidatorMLDSAKey is one accountable signer in a P3Q / cert-set signer set:
// a validator identity, its ML-DSA-65 public key, and its stake weight.
type ValidatorMLDSAKey struct {
NodeID ids.NodeID
PublicKey []byte
Weight uint64
}
// SignerSetAuthority resolves the per-validator ML-DSA-65 public keys and stake
// weights for a (signerSetID, eraHandle). It is the key registry for the P3Q
// rollup lane AND the raw cert-set lane (both authorize via INDEPENDENT
// per-validator keys, NOT a threshold group key). It is a DISTINCT type from
// PulsarKeyEraResolver (one threshold group key) and from the corona group-key
// resolver — so an implementation can never alias Pulsar ↔ Corona ↔ P3Q key
// material.
type SignerSetAuthority interface {
ResolveSignerSet(
signerSetID ids.ID,
eraHandle uint64,
) (signers []ValidatorMLDSAKey, totalWeight uint64, err error)
}
// IsPQRootOfTrust classifies a succinct-proof wrapper system by post-quantum
// status. Returns true iff the wrapper is BELIEVED post-quantum-secure under
// current cryptanalysis. This is the SHARED, subject-agnostic policy primitive
// (it lives in the warp root, not in warp/pulsar, so both the warp envelope
// path and the quasar consensus policy can reuse it without an import cycle).
//
// "groth16", "groth16-bls12-381", "snark-pairing"
// → false. Pairing-based; broken under Shor's algorithm.
// "stark-rescue", "stark-poseidon", "lattice-zk"
// → true. Hash- or lattice-based; PQ-friendly assumptions.
// "none" or empty string
// → true. No wrapper means the underlying signature itself is the evidence;
// if that signature is ML-DSA / Corona, the PQ root lives in it directly.
// anything else → false (conservative).
//
// Case-insensitive on the system name.
func IsPQRootOfTrust(provingSystem string) bool {
switch strings.ToLower(strings.TrimSpace(provingSystem)) {
case "groth16",
"groth16-bls12-381",
"snark-pairing":
return false
case "stark-rescue",
"stark-poseidon",
"lattice-zk":
return true
case "none",
"":
return true
default:
return false
}
}
// P3QStrictRootOK reports whether a P3Q root may serve as a STRICT-PQ finality
// root. It returns nil iff the root's proof system is post-quantum; otherwise
// ErrP3QClassicalRoot. Policy that explicitly accepts a classical P3Q root
// (e.g. a non-strict tier) MUST NOT call this — it is the strict gate.
func P3QStrictRootOK(root P3QRoot) error {
if !IsPQRootOfTrust(root.ProvingSystem) {
return ErrP3QClassicalRoot
}
return nil
}
// unavailableP3QVerifier is the in-package fail-closed P3Q verifier. It is the
// default when no real (injected) FRI rollup verifier is wired, so a P3Q lane
// can never silently pass without a verifier.
type unavailableP3QVerifier struct{}
// UnavailableP3QVerifier returns a P3QRollupVerifier that always fails closed
// with ErrP3QVerifierUnavailable. Receivers wire the real verifier (the
// 0x012205 FRI rollup) from above warp; this is the safe default until they do.
func UnavailableP3QVerifier() P3QRollupVerifier { return unavailableP3QVerifier{} }
func (unavailableP3QVerifier) VerifyP3QRollup([]byte, P3QRoot, SignerSetAuthority) (string, error) {
return "", ErrP3QVerifierUnavailable
}