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.
158 lines
5.7 KiB
Go
158 lines
5.7 KiB
Go
// Copyright (C) 2019-2026, Lux Industries Inc. All rights reserved.
|
|
// See the file LICENSE for licensing terms.
|
|
|
|
// quasar_subject.go — the consensus finality SUBJECT M.
|
|
//
|
|
// Where the warp cross-chain subject is D (the Warp Message ID), the quasar
|
|
// consensus subject is M: the digest every finality lane (Beam, Pulsar,
|
|
// Corona, P3Q) signs to attest that a block is final. Every lane verifier in
|
|
// evidence.go is subject-agnostic, so the only difference between cross-chain
|
|
// and consensus finality is which 32-byte digest is the subject.
|
|
//
|
|
// M = keccak256( "QUASAR_FINALITY_V1" ‖ c14n(params) )
|
|
//
|
|
// The c14n follows the ZAP discipline (zap.go): fixed-width big-endian, every
|
|
// field present, total-order canonical — a given params value has exactly one
|
|
// transcript encoding, and decode reproduces it byte-for-byte. Every field is
|
|
// fixed-width, so no length prefixes are required for unambiguity; the layout
|
|
// itself is the frame.
|
|
|
|
package warp
|
|
|
|
import (
|
|
"fmt"
|
|
|
|
"github.com/luxfi/ids"
|
|
)
|
|
|
|
// quasarFinalitySubjectDST is the domain-separation tag folded into M. It is
|
|
// distinct from messageDST ("LUX-WARP-ZAP-CORE-v1"), so a consensus finality
|
|
// subject can never collide with a cross-chain Warp message digest even on
|
|
// identical field bytes.
|
|
const quasarFinalitySubjectDST = "QUASAR_FINALITY_V1"
|
|
|
|
// quasarFinalityTranscriptLen is the exact byte length of a canonical M
|
|
// transcript: 32 + 8 + 8 + 32 + 32 + 32 + 8 + 8 + 8 + 8.
|
|
const quasarFinalityTranscriptLen = 32 + 8 + 8 + 32 + 32 + 32 + 8 + 8 + 8 + 8
|
|
|
|
// QuasarFinalityParams are the fields that uniquely identify a finalized
|
|
// block-decision the consensus quorum attests to. M binds ALL of them, so a
|
|
// signature over M is a signature over this exact decision and nothing else.
|
|
type QuasarFinalityParams struct {
|
|
// ChainID is the chain whose block is being finalized.
|
|
ChainID ids.ID
|
|
// Height is the block height.
|
|
Height uint64
|
|
// Round is the consensus round that decided the block.
|
|
Round uint64
|
|
// BlockID is the decided block's ID.
|
|
BlockID ids.ID
|
|
// StateRoot is the post-state root the decision commits to.
|
|
StateRoot [32]byte
|
|
// SignerSetID identifies the validator set / quorum authorizing finality.
|
|
SignerSetID ids.ID
|
|
// KeyEraID is the threshold key era in force for this decision.
|
|
KeyEraID uint64
|
|
// Generation is the proactive-refresh/reshare epoch WITHIN KeyEraID. Binding
|
|
// it into M pins a signature to the exact share generation, so a signature
|
|
// produced under one generation cannot be replayed for another within the
|
|
// same key era.
|
|
Generation uint64
|
|
// PChainHeight anchors the decision to the P-chain validator-set snapshot the
|
|
// signer set / key era was dealt over, so the subject commits to which
|
|
// validator set authorized it.
|
|
PChainHeight uint64
|
|
// PolicyID identifies the finality policy/tier the decision was made under.
|
|
// Folding it into M makes a signature commit to its tier, so a cert minted
|
|
// under a weaker policy cannot be replayed to satisfy a stronger one
|
|
// (cross-policy / tier-downgrade reuse fails: the subject differs).
|
|
PolicyID uint64
|
|
}
|
|
|
|
// MarshalTranscript returns the canonical c14n bytes of the finality params —
|
|
// the preimage (after the DST) of M. Total-order canonical and fixed-length
|
|
// (quasarFinalityTranscriptLen): re-marshaling a parsed transcript reproduces
|
|
// the exact bytes.
|
|
//
|
|
// Layout (all big-endian, no length prefixes — every field is fixed width):
|
|
//
|
|
// ChainID [32]
|
|
// Height u64
|
|
// Round u64
|
|
// BlockID [32]
|
|
// StateRoot [32]
|
|
// SignerSetID [32]
|
|
// KeyEraID u64
|
|
// Generation u64
|
|
// PChainHeight u64
|
|
// PolicyID u64
|
|
func (p QuasarFinalityParams) MarshalTranscript() []byte {
|
|
out := make([]byte, 0, quasarFinalityTranscriptLen)
|
|
out = appendFixed(out, p.ChainID[:])
|
|
out = appendU64(out, p.Height)
|
|
out = appendU64(out, p.Round)
|
|
out = appendFixed(out, p.BlockID[:])
|
|
out = appendFixed(out, p.StateRoot[:])
|
|
out = appendFixed(out, p.SignerSetID[:])
|
|
out = appendU64(out, p.KeyEraID)
|
|
out = appendU64(out, p.Generation)
|
|
out = appendU64(out, p.PChainHeight)
|
|
out = appendU64(out, p.PolicyID)
|
|
return out
|
|
}
|
|
|
|
// ParseQuasarFinalityTranscript decodes a canonical transcript produced by
|
|
// MarshalTranscript, rejecting any input that is not exactly
|
|
// quasarFinalityTranscriptLen bytes (a canonical-form violation).
|
|
func ParseQuasarFinalityTranscript(b []byte) (QuasarFinalityParams, error) {
|
|
var p QuasarFinalityParams
|
|
if len(b) != quasarFinalityTranscriptLen {
|
|
return p, fmt.Errorf("%w: transcript must be %d bytes, got %d",
|
|
ErrInvalidMessage, quasarFinalityTranscriptLen, len(b))
|
|
}
|
|
r := newZapReader(b)
|
|
if err := r.fixedInto(p.ChainID[:]); err != nil {
|
|
return p, err
|
|
}
|
|
var err error
|
|
if p.Height, err = r.u64(); err != nil {
|
|
return p, err
|
|
}
|
|
if p.Round, err = r.u64(); err != nil {
|
|
return p, err
|
|
}
|
|
if err = r.fixedInto(p.BlockID[:]); err != nil {
|
|
return p, err
|
|
}
|
|
if err = r.fixedInto(p.StateRoot[:]); err != nil {
|
|
return p, err
|
|
}
|
|
if err = r.fixedInto(p.SignerSetID[:]); err != nil {
|
|
return p, err
|
|
}
|
|
if p.KeyEraID, err = r.u64(); err != nil {
|
|
return p, err
|
|
}
|
|
if p.Generation, err = r.u64(); err != nil {
|
|
return p, err
|
|
}
|
|
if p.PChainHeight, err = r.u64(); err != nil {
|
|
return p, err
|
|
}
|
|
if p.PolicyID, err = r.u64(); err != nil {
|
|
return p, err
|
|
}
|
|
if err := r.end(); err != nil {
|
|
return p, err
|
|
}
|
|
return p, nil
|
|
}
|
|
|
|
// QuasarFinalitySubject returns M = keccak256(quasarFinalitySubjectDST ‖
|
|
// MarshalTranscript()) for the given params. M is the 32-byte subject every
|
|
// consensus finality lane signs. keccak256 (not NIST SHA3) matches warp's D so
|
|
// the same on-chain keccak opcode reproduces it byte-for-byte.
|
|
func QuasarFinalitySubject(p QuasarFinalityParams) [32]byte {
|
|
return keccak256([]byte(quasarFinalitySubjectDST), p.MarshalTranscript())
|
|
}
|