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.
88 lines
3.4 KiB
Go
88 lines
3.4 KiB
Go
// Copyright (C) 2019-2026, Lux Industries Inc. All rights reserved.
|
||
// See the file LICENSE for licensing terms.
|
||
|
||
package warp
|
||
|
||
import (
|
||
"errors"
|
||
|
||
"github.com/luxfi/ids"
|
||
"golang.org/x/crypto/sha3"
|
||
)
|
||
|
||
// codec.go — the Warp ZAP profile: the domain-specific constants and
|
||
// signing-domain construction layered on top of the generic canonical
|
||
// TLV mechanism in zap.go. There is exactly ONE codec (ZAP) and exactly
|
||
// ONE signing digest (D). No RLP, no codec version, no type registry.
|
||
|
||
// Kind discriminators. zapKindMessage is the first byte of a Message
|
||
// c14n stream; kindEnvelope is the envelope kind byte that follows the
|
||
// wire magic. They are distinct so a Message can never be mistaken for
|
||
// a full envelope and vice-versa.
|
||
const (
|
||
zapKindMessage byte = 0x01
|
||
kindEnvelope byte = 0x02
|
||
)
|
||
|
||
// Size limits. MaxMessageSize bounds a Message's canonical encoding;
|
||
// MaxEnvelopeSize bounds the full envelope (message + Beam + the two PQ
|
||
// lanes). Both are hard ceilings checked at the decode boundary.
|
||
const (
|
||
// MaxMessageSize is the maximum canonical Message size.
|
||
MaxMessageSize = 256 * KiB
|
||
|
||
// MaxEnvelopeSize is the maximum Envelope wire size. Bounded at
|
||
// 4×MaxMessageSize to leave room for the Pulse (~33 KB) and the
|
||
// ML-DSA cert set alongside the message and Beam.
|
||
MaxEnvelopeSize = 4 * MaxMessageSize
|
||
)
|
||
|
||
// Cross-cutting errors. Envelope-shape errors live in envelope.go.
|
||
var (
|
||
ErrInvalidSignature = errors.New("invalid signature")
|
||
ErrInvalidMessage = errors.New("invalid message")
|
||
ErrUnknownValidator = errors.New("unknown validator")
|
||
ErrInsufficientWeight = errors.New("insufficient weight")
|
||
)
|
||
|
||
// Domain-separation tags. D = keccak256(messageDST ‖ zap_c14n(Message))
|
||
// is the single signed digest (the "Prism" transcript): message ID,
|
||
// replay key, and on-chain messageHash all at once. Each lane signs the
|
||
// SAME D under its OWN tag, so a signature in one lane can never be
|
||
// replayed into another (BLS objects vs lattice objects are already
|
||
// non-interchangeable; the distinct tags close the door regardless).
|
||
const (
|
||
messageDST = "LUX-WARP-ZAP-CORE-v1"
|
||
beamDST = "LUX-WARP-ZAP-BEAM-v1"
|
||
coronaDST = "LUX-WARP-ZAP-CORONA-v1"
|
||
mldsaDST = "LUX-WARP-ZAP-MLDSA-v1"
|
||
)
|
||
|
||
// keccak256 is Ethereum's keccak256 — golang.org/x/crypto/sha3
|
||
// NewLegacyKeccak256 (Keccak padding 0x01), NOT NIST SHA3 (pad 0x06) and
|
||
// NOT crypto/sha256. The on-chain keccak256 opcode computes exactly this,
|
||
// so the digest D matches byte-for-byte between Go and Solidity.
|
||
func keccak256(parts ...[]byte) [32]byte {
|
||
h := sha3.NewLegacyKeccak256()
|
||
for _, p := range parts {
|
||
_, _ = h.Write(p)
|
||
}
|
||
var out [32]byte
|
||
copy(out[:], h.Sum(nil))
|
||
return out
|
||
}
|
||
|
||
// BeamSigningBytes returns the exact bytes the BLS Beam lane signs and
|
||
// verifies: beamDST ‖ D. The Beam now authenticates the full Message
|
||
// (including PQ lineage) rather than only the unsigned message body.
|
||
func BeamSigningBytes(d ids.ID) []byte { return append([]byte(beamDST), d[:]...) }
|
||
|
||
// CoronaSigningBytes returns the exact bytes the Corona Ringtail lattice
|
||
// threshold lane signs and verifies: coronaDST ‖ D. (This lane was formerly
|
||
// mislabeled "Pulse"; it is the corona kernel's Module-LWE signature.)
|
||
func CoronaSigningBytes(d ids.ID) []byte { return append([]byte(coronaDST), d[:]...) }
|
||
|
||
// MLDSASigningBytes returns the exact bytes the ML-DSA cert-set lane signs
|
||
// and verifies: mldsaDST ‖ D.
|
||
func MLDSASigningBytes(d ids.ID) []byte { return append([]byte(mldsaDST), d[:]...) }
|