Files
zeekayandHanzo Dev 54208c77f4 lint: gofmt the seven files CI flags, and drop an ineffectual map read
golangci-lint has been failing the `lint` job on:

    engine/chain/attestation.go:89:1               (gofmt)
    engine/chain/cert.go:86:1                      (gofmt)
    pkg/wire/policies.go:777:1                     (gofmt)
    protocol/quasar/compact_evidence.go:96:1       (gofmt)
    protocol/quasar/consensus_cert.go:576:1        (gofmt)
    protocol/quasar/pulsar_sampled_cert.go:50:1    (gofmt)
    protocol/quasar/pulsar_sampled_subject.go:18:1 (gofmt)
    engine/chain/integration.go:864:2              (ineffassign)

The gofmt diffs are entirely struct-field comment alignment — no code moves.

The ineffassign is real. followVerifiedBlock did

    pending, exists := rt.Transitive.pendingBlocks[blockID]

and `pending` is never read: the only other mentions are the reassignment
inside `if !exists` and the store on the next line. Only presence matters, so
the value is no longer bound. Behaviour is identical — an already-tracked
block keeps its entry, a new one gets a fresh PendingBlock.

Scope note: `gofmt -l .` lists 30 files here, but `.golangci.yml` sets
`tests: false`, so the 23 test files are excluded and CI never sees them.
Only the 7 it actually flags are touched.

Verified: gofmt clean on all 8, `go build ./engine/... ./protocol/quasar/...
./pkg/wire/...` exits 0, and golangci-lint no longer reports gofmt or
ineffassign anywhere in those trees.

Does NOT address the separate `test` failure — protocol/quasar times out at
900s, which is its own investigation.

Co-authored-by: Hanzo Dev <[email protected]>
2026-08-01 08:18:52 -07:00

908 lines
26 KiB
Go

// Copyright (C) 2019-2025, Lux Industries Inc. All rights reserved.
// See the file LICENSE for licensing terms.
package wire
import (
"context"
"crypto/sha256"
"fmt"
"sync"
"time"
"github.com/luxfi/consensus/config"
"github.com/luxfi/consensus/protocol/quasar"
"github.com/luxfi/crypto/bls"
"github.com/luxfi/crypto/mldsa"
coronaThreshold "github.com/luxfi/threshold/protocols/corona"
)
// maxCandidates is the upper bound on tracked candidates per policy instance.
// Prevents unbounded memory growth from malicious or buggy peers.
const maxCandidates = 100000
// =============================================================================
// POLICY IMPLEMENTATIONS
// =============================================================================
//
// Each policy implements FinalityPolicy for different K scenarios:
// - NonePolicy: K=1 self-sequencing (immediate finality)
// - QuorumPolicy: K=small threshold signature (3/5, 2/3)
// - SamplePolicy: K=large metastable sampling
// - L1Policy: K=external chain inclusion (OP Stack)
// - QuantumPolicy: BLS + Corona post-quantum
// =============================================================================
// =============================================================================
// NONE POLICY: K=1 Self-Sequencing
// =============================================================================
// NonePolicy provides immediate finality for single-node operation
type NonePolicy struct {
mu sync.RWMutex
candidates map[CandidateID]*Candidate
certs map[CandidateID]*Certificate
}
// NewNonePolicy creates a none policy
func NewNonePolicy() *NonePolicy {
return &NonePolicy{
candidates: make(map[CandidateID]*Candidate),
certs: make(map[CandidateID]*Certificate),
}
}
func (p *NonePolicy) PolicyID() PolicyID {
return PolicyNone
}
func (p *NonePolicy) OnCandidate(ctx context.Context, candidate *Candidate) error {
p.mu.Lock()
defer p.mu.Unlock()
if len(p.candidates) >= maxCandidates {
return fmt.Errorf("candidate limit reached (%d)", maxCandidates)
}
p.candidates[candidate.ID] = candidate
return nil
}
func (p *NonePolicy) OnVote(ctx context.Context, vote *Vote) error {
// No votes needed for K=1
return nil
}
func (p *NonePolicy) MaybeFinalize(ctx context.Context, candidateID CandidateID) (*Certificate, error) {
p.mu.Lock()
defer p.mu.Unlock()
if cert, ok := p.certs[candidateID]; ok {
return cert, nil
}
candidate, ok := p.candidates[candidateID]
if !ok {
return nil, nil // Not observed yet
}
// Immediate finality - create self-signed certificate
cert := &Certificate{
CandidateID: candidateID,
Height: candidate.Height,
PolicyID: PolicyNone,
Proof: []byte("self"), // Minimal proof
}
p.certs[candidateID] = cert
return cert, nil
}
func (p *NonePolicy) Verify(ctx context.Context, cert *Certificate) (bool, error) {
return cert.PolicyID == PolicyNone, nil
}
// =============================================================================
// QUORUM POLICY: K=small Threshold Signature
// =============================================================================
// QuorumPolicy provides threshold-based finality
type QuorumPolicy struct {
mu sync.RWMutex
threshold int // Number of votes needed (e.g., 3 of 5)
total int // Total validators
candidates map[CandidateID]*Candidate
votes map[CandidateID]map[VoterID]*Vote
certs map[CandidateID]*Certificate
}
// NewQuorumPolicy creates a quorum policy
func NewQuorumPolicy(threshold, total int) *QuorumPolicy {
return &QuorumPolicy{
threshold: threshold,
total: total,
candidates: make(map[CandidateID]*Candidate),
votes: make(map[CandidateID]map[VoterID]*Vote),
certs: make(map[CandidateID]*Certificate),
}
}
func (p *QuorumPolicy) PolicyID() PolicyID {
return PolicyQuorum
}
func (p *QuorumPolicy) OnCandidate(ctx context.Context, candidate *Candidate) error {
p.mu.Lock()
defer p.mu.Unlock()
if len(p.candidates) >= maxCandidates {
return fmt.Errorf("candidate limit reached (%d)", maxCandidates)
}
p.candidates[candidate.ID] = candidate
if _, ok := p.votes[candidate.ID]; !ok {
p.votes[candidate.ID] = make(map[VoterID]*Vote)
}
return nil
}
func (p *QuorumPolicy) OnVote(ctx context.Context, vote *Vote) error {
p.mu.Lock()
defer p.mu.Unlock()
if _, ok := p.votes[vote.CandidateID]; !ok {
p.votes[vote.CandidateID] = make(map[VoterID]*Vote)
}
p.votes[vote.CandidateID][vote.VoterID] = vote
return nil
}
func (p *QuorumPolicy) MaybeFinalize(ctx context.Context, candidateID CandidateID) (*Certificate, error) {
p.mu.Lock()
defer p.mu.Unlock()
if cert, ok := p.certs[candidateID]; ok {
return cert, nil
}
candidate, ok := p.candidates[candidateID]
if !ok {
return nil, nil
}
votes := p.votes[candidateID]
acceptVotes := 0
for _, v := range votes {
if v.Preference {
acceptVotes++
}
}
if acceptVotes < p.threshold {
return nil, nil // Not enough votes
}
// Build certificate proof: aggregated signatures
var proof []byte
var signers []byte
for voterID, vote := range votes {
if vote.Preference && len(vote.Signature) > 0 {
proof = append(proof, vote.Signature...)
signers = append(signers, voterID[:]...)
}
}
cert := &Certificate{
CandidateID: candidateID,
Height: candidate.Height,
PolicyID: PolicyQuorum,
Proof: proof,
Signers: signers,
}
p.certs[candidateID] = cert
return cert, nil
}
func (p *QuorumPolicy) Verify(ctx context.Context, cert *Certificate) (bool, error) {
if cert.PolicyID != PolicyQuorum {
return false, nil
}
// Verify signature proof against signers bitmap
// Proof contains aggregated BLS signature, signers contains validator bitmap
return len(cert.Proof) > 0 && len(cert.Signers) >= p.threshold*32, nil
}
// =============================================================================
// SAMPLE POLICY: K=large Metastable Sampling
// =============================================================================
// SamplePolicy provides metastable consensus for large validator sets
type SamplePolicy struct {
mu sync.RWMutex
k int // Sample size per round
alpha float64 // Agreement threshold
beta int // Consecutive rounds needed
candidates map[CandidateID]*sampleState
certs map[CandidateID]*Certificate
}
type sampleState struct {
candidate *Candidate
preference bool
confidence int
roundVotes map[uint64]map[VoterID]bool // round -> voter -> preference
currentRound uint64
}
// NewSamplePolicy creates a sample convergence policy
func NewSamplePolicy(k int, alpha float64, beta int) *SamplePolicy {
return &SamplePolicy{
k: k,
alpha: alpha,
beta: beta,
candidates: make(map[CandidateID]*sampleState),
certs: make(map[CandidateID]*Certificate),
}
}
func (p *SamplePolicy) PolicyID() PolicyID {
return PolicySampleConvergence
}
func (p *SamplePolicy) OnCandidate(ctx context.Context, candidate *Candidate) error {
p.mu.Lock()
defer p.mu.Unlock()
if len(p.candidates) >= maxCandidates {
return fmt.Errorf("candidate limit reached (%d)", maxCandidates)
}
p.candidates[candidate.ID] = &sampleState{
candidate: candidate,
preference: true, // Initial preference
roundVotes: make(map[uint64]map[VoterID]bool),
}
return nil
}
func (p *SamplePolicy) OnVote(ctx context.Context, vote *Vote) error {
p.mu.Lock()
defer p.mu.Unlock()
state, ok := p.candidates[vote.CandidateID]
if !ok {
return nil
}
if _, ok := state.roundVotes[vote.Round]; !ok {
state.roundVotes[vote.Round] = make(map[VoterID]bool)
}
state.roundVotes[vote.Round][vote.VoterID] = vote.Preference
// Check if round is complete
roundVotes := state.roundVotes[vote.Round]
if len(roundVotes) >= p.k {
// Count votes
yes := 0
for _, pref := range roundVotes {
if pref {
yes++
}
}
threshold := int(float64(p.k) * p.alpha)
newPref := yes >= threshold
if newPref == state.preference {
state.confidence++
} else {
state.preference = newPref
state.confidence = 1
}
state.currentRound = vote.Round
}
return nil
}
func (p *SamplePolicy) MaybeFinalize(ctx context.Context, candidateID CandidateID) (*Certificate, error) {
p.mu.Lock()
defer p.mu.Unlock()
if cert, ok := p.certs[candidateID]; ok {
return cert, nil
}
state, ok := p.candidates[candidateID]
if !ok {
return nil, nil
}
if state.confidence < p.beta {
return nil, nil // Not converged yet
}
if !state.preference {
return nil, nil // Converged to reject
}
// Build proof: confidence score + final round votes
proof := make([]byte, 0, 100)
proof = append(proof, byte(state.confidence))
proof = append(proof, Uint64ToBytes(state.currentRound)...)
cert := &Certificate{
CandidateID: candidateID,
Height: state.candidate.Height,
PolicyID: PolicySampleConvergence,
Proof: proof,
}
p.certs[candidateID] = cert
return cert, nil
}
func (p *SamplePolicy) Verify(ctx context.Context, cert *Certificate) (bool, error) {
if cert.PolicyID != PolicySampleConvergence {
return false, nil
}
if len(cert.Proof) < 9 {
return false, nil
}
confidence := int(cert.Proof[0])
return confidence >= p.beta, nil
}
// =============================================================================
// L1 INCLUSION POLICY: External Chain Finality (OP Stack)
// =============================================================================
// L1Policy provides finality via L1 chain inclusion
type L1Policy struct {
mu sync.RWMutex
l1Verifier L1Verifier
candidates map[CandidateID]*Candidate
certs map[CandidateID]*Certificate
}
// L1Verifier verifies L1 inclusion proofs
type L1Verifier interface {
// VerifyInclusion checks if candidate is included in L1
VerifyInclusion(ctx context.Context, candidateID CandidateID, proof []byte) (bool, error)
// GetInclusionProof retrieves inclusion proof for a candidate
GetInclusionProof(ctx context.Context, candidateID CandidateID) ([]byte, error)
}
// NewL1Policy creates an L1 inclusion policy
func NewL1Policy(verifier L1Verifier) *L1Policy {
return &L1Policy{
l1Verifier: verifier,
candidates: make(map[CandidateID]*Candidate),
certs: make(map[CandidateID]*Certificate),
}
}
func (p *L1Policy) PolicyID() PolicyID {
return PolicyL1Inclusion
}
func (p *L1Policy) OnCandidate(ctx context.Context, candidate *Candidate) error {
p.mu.Lock()
defer p.mu.Unlock()
if len(p.candidates) >= maxCandidates {
return fmt.Errorf("candidate limit reached (%d)", maxCandidates)
}
p.candidates[candidate.ID] = candidate
return nil
}
func (p *L1Policy) OnVote(ctx context.Context, vote *Vote) error {
// L1 policy doesn't use votes
return nil
}
func (p *L1Policy) MaybeFinalize(ctx context.Context, candidateID CandidateID) (*Certificate, error) {
p.mu.Lock()
defer p.mu.Unlock()
if cert, ok := p.certs[candidateID]; ok {
return cert, nil
}
candidate, ok := p.candidates[candidateID]
if !ok {
return nil, nil
}
// Get L1 inclusion proof
proof, err := p.l1Verifier.GetInclusionProof(ctx, candidateID)
if err != nil || proof == nil {
return nil, nil // Not included yet
}
cert := &Certificate{
CandidateID: candidateID,
Height: candidate.Height,
PolicyID: PolicyL1Inclusion,
Proof: proof,
}
p.certs[candidateID] = cert
return cert, nil
}
func (p *L1Policy) Verify(ctx context.Context, cert *Certificate) (bool, error) {
if cert.PolicyID != PolicyL1Inclusion {
return false, nil
}
return p.l1Verifier.VerifyInclusion(ctx, cert.CandidateID, cert.Proof)
}
// =============================================================================
// QUANTUM POLICY: BLS + Corona Post-Quantum
// =============================================================================
// QuantumPolicy combines BLS and Corona post-quantum signatures
// SECURITY CRITICAL: All votes MUST include both BLS and Corona signatures.
// Votes without dual signatures are rejected to ensure quantum-safe consensus.
type QuantumPolicy struct {
mu sync.RWMutex
threshold int
requireRT bool // When true, RT signature is REQUIRED on all votes
candidates map[CandidateID]*Candidate
blsVotes map[CandidateID]map[VoterID][]byte // BLS signatures
pqVotes map[CandidateID]map[VoterID][]byte // Corona signatures
certs map[CandidateID]*Certificate
// certPolicy is the chain's cert posture — the single source of truth
// for which QuasarCert legs are MANDATORY on the verify path. Derived
// from the chain's ChainSecurityProfile (profile.CertPolicy()). The
// policy-driven verifier (VerifyCertUnderPolicy) consults
// certPolicy.RequiredLegs(): leg presence is policy-driven, never
// cert-byte-driven, so an adversary cannot weaken a cert by omitting
// legs. The default (set by NewQuantumPolicy) is the Permissive-profile
// posture; strict-PQ chains construct with NewQuantumPolicyForProfile.
certPolicy config.CertPolicy
}
// RTRequirementError is returned when Corona signature is missing but required
type RTRequirementError struct {
Reason string
}
func (e *RTRequirementError) Error() string {
return "RT signature required: " + e.Reason
}
// NewQuantumPolicy creates a quantum-safe BLS+Corona policy
// By default, RT signatures are REQUIRED for Q-Chain consensus security.
// The cert posture defaults to the Permissive-profile policy (Hybrid:
// BLS + Pulsar + Corona). Strict-PQ chains MUST use
// NewQuantumPolicyForProfile so the verify path drops BLS from the
// required set.
func NewQuantumPolicy(threshold int) *QuantumPolicy {
return newQuantumPolicy(threshold, true, config.Permissive().CertPolicy())
}
// NewQuantumPolicyWithOptions creates a quantum policy with explicit RT requirement
func NewQuantumPolicyWithOptions(threshold int, requireRT bool) *QuantumPolicy {
return newQuantumPolicy(threshold, requireRT, config.Permissive().CertPolicy())
}
// NewQuantumPolicyForProfile creates a quantum policy whose cert posture is
// derived from the chain's ChainSecurityProfile — the single source of
// truth. A strict-PQ profile yields a Strict-variant CertPolicy (BLS not
// required), so the live verify path rejects a leg-stripped (e.g. BLS-only)
// cert. requireRT follows the policy: PQ profiles require the Corona/RT leg.
func NewQuantumPolicyForProfile(threshold int, profile *config.ChainSecurityProfile) *QuantumPolicy {
cp := profile.CertPolicy()
return newQuantumPolicy(threshold, cp.IsPostQuantum(), cp)
}
func newQuantumPolicy(threshold int, requireRT bool, cp config.CertPolicy) *QuantumPolicy {
return &QuantumPolicy{
threshold: threshold,
requireRT: requireRT,
candidates: make(map[CandidateID]*Candidate),
blsVotes: make(map[CandidateID]map[VoterID][]byte),
pqVotes: make(map[CandidateID]map[VoterID][]byte),
certs: make(map[CandidateID]*Certificate),
certPolicy: cp,
}
}
// SetRequireRT enables or disables RT requirement (for testing only)
func (p *QuantumPolicy) SetRequireRT(require bool) {
p.mu.Lock()
defer p.mu.Unlock()
p.requireRT = require
}
func (p *QuantumPolicy) PolicyID() PolicyID {
return PolicyQuantum
}
func (p *QuantumPolicy) OnCandidate(ctx context.Context, candidate *Candidate) error {
p.mu.Lock()
defer p.mu.Unlock()
if len(p.candidates) >= maxCandidates {
return fmt.Errorf("candidate limit reached (%d)", maxCandidates)
}
p.candidates[candidate.ID] = candidate
p.blsVotes[candidate.ID] = make(map[VoterID][]byte)
p.pqVotes[candidate.ID] = make(map[VoterID][]byte)
return nil
}
func (p *QuantumPolicy) OnVote(ctx context.Context, vote *Vote) error {
p.mu.Lock()
defer p.mu.Unlock()
if !vote.Preference {
return nil // Only track accept votes for cert
}
if len(vote.Signature) == 0 {
if p.requireRT {
return &RTRequirementError{Reason: "vote missing signature"}
}
return nil
}
scheme := vote.SignatureScheme()
// SECURITY: Enforce dual BLS+Corona requirement for quantum safety
if p.requireRT && scheme != SigQuasar {
return &RTRequirementError{
Reason: "Q-Chain requires dual BLS+Corona signature (SigQuasar), got scheme " + sigSchemeToString(scheme),
}
}
switch scheme {
case SigBLS:
// Only allowed if requireRT is false
if p.blsVotes[vote.CandidateID] == nil {
p.blsVotes[vote.CandidateID] = make(map[VoterID][]byte)
}
p.blsVotes[vote.CandidateID][vote.VoterID] = vote.Signature[1:] // Skip scheme byte
case SigCorona:
// Only allowed if requireRT is false
if p.pqVotes[vote.CandidateID] == nil {
p.pqVotes[vote.CandidateID] = make(map[VoterID][]byte)
}
p.pqVotes[vote.CandidateID][vote.VoterID] = vote.Signature[1:]
case SigQuasar:
// Quasar vote contains both: [scheme][bls_len_hi][bls_len_lo][bls][corona]
if len(vote.Signature) < 4 {
return &RTRequirementError{Reason: "malformed Quasar signature: too short"}
}
blsLen := int(vote.Signature[1])<<8 | int(vote.Signature[2])
if blsLen == 0 {
return &RTRequirementError{Reason: "malformed Quasar signature: BLS component missing"}
}
if len(vote.Signature) < 3+blsLen {
return &RTRequirementError{Reason: "malformed Quasar signature: truncated BLS"}
}
blsSig := vote.Signature[3 : 3+blsLen]
pqSig := vote.Signature[3+blsLen:]
if len(pqSig) == 0 {
return &RTRequirementError{Reason: "malformed Quasar signature: Corona component missing"}
}
if p.blsVotes[vote.CandidateID] == nil {
p.blsVotes[vote.CandidateID] = make(map[VoterID][]byte)
}
if p.pqVotes[vote.CandidateID] == nil {
p.pqVotes[vote.CandidateID] = make(map[VoterID][]byte)
}
p.blsVotes[vote.CandidateID][vote.VoterID] = blsSig
p.pqVotes[vote.CandidateID][vote.VoterID] = pqSig
default:
if p.requireRT {
return &RTRequirementError{Reason: "unsupported signature scheme for Q-Chain"}
}
}
return nil
}
// sigSchemeToString converts signature scheme byte to string for errors
func sigSchemeToString(scheme byte) string {
switch scheme {
case SigNone:
return "SigNone"
case SigEd25519:
return "SigEd25519"
case SigBLS:
return "SigBLS"
case SigCorona:
return "SigCorona"
case SigQuasar:
return "SigQuasar"
default:
return "Unknown"
}
}
func (p *QuantumPolicy) MaybeFinalize(ctx context.Context, candidateID CandidateID) (*Certificate, error) {
p.mu.Lock()
defer p.mu.Unlock()
if cert, ok := p.certs[candidateID]; ok {
return cert, nil
}
candidate, ok := p.candidates[candidateID]
if !ok {
return nil, nil
}
// Need threshold of BOTH BLS and PQ signatures
blsCount := len(p.blsVotes[candidateID])
pqCount := len(p.pqVotes[candidateID])
if blsCount < p.threshold || pqCount < p.threshold {
return nil, nil
}
// Build a real QuasarCert and serialize it. Aggregating BLS shares
// into a single 48-byte aggregate requires the BLS scheme/aggregator
// (held by the signer at a higher layer); here we concatenate the
// per-validator share bytes so the wire layer is self-contained, and
// embed individual ML-DSA-65 signatures in MLDSAProof.
qc := &quasar.QuasarCert{
BLS: concatSignatures(p.blsVotes[candidateID]),
Corona: concatSignatures(p.pqVotes[candidateID]),
MLDSARollup: nil,
Epoch: candidate.Height,
Finality: time.Now(),
Validators: blsCount,
}
proofBytes, err := qc.MarshalBinary()
if err != nil {
return nil, fmt.Errorf("quantum: marshal QuasarCert: %w", err)
}
var signers []byte
for voter := range p.blsVotes[candidateID] {
signers = append(signers, voter[:]...)
}
cert := &Certificate{
CandidateID: candidateID,
Height: candidate.Height,
PolicyID: PolicyQuantum,
Proof: proofBytes,
Signers: signers,
}
p.certs[candidateID] = cert
return cert, nil
}
// concatSignatures concatenates the values of a voter→signature map into a
// single byte slice. Ordering is undefined (the map is not ordered) but each
// component preserves byte-for-byte its individual signature.
func concatSignatures(m map[VoterID][]byte) []byte {
if len(m) == 0 {
return nil
}
total := 0
for _, s := range m {
total += len(s)
}
out := make([]byte, 0, total)
for _, s := range m {
out = append(out, s...)
}
return out
}
// Verify checks that the QuasarCert in cert.Proof is well-formed. Real
// cryptographic verification requires the caller to supply the BLS aggregate
// public key, the Corona group key, and the per-validator ML-DSA public
// keys via VerifyWithRealKeys.
func (p *QuantumPolicy) Verify(ctx context.Context, cert *Certificate) (bool, error) {
if cert.PolicyID != PolicyQuantum {
return false, nil
}
qc := &quasar.QuasarCert{}
if err := qc.UnmarshalBinary(cert.Proof); err != nil {
return false, nil
}
// Structural gate: BLS + Corona must be present for SigQuasar.
if len(qc.BLS) == 0 || len(qc.Corona) == 0 {
return false, nil
}
return true, nil
}
// certMessageDigest is the canonical message a QuasarCert commits to:
// sha256(cert.CandidateID || cert.Height). Single definition so the
// verify paths cannot disagree on what was signed.
func certMessageDigest(cert *Certificate) []byte {
h := sha256.New()
h.Write(cert.CandidateID[:])
var hb [8]byte
for i := 0; i < 8; i++ {
hb[i] = byte(cert.Height >> (8 * (7 - i)))
}
h.Write(hb[:])
return h.Sum(nil)
}
// VerifyCertUnderPolicy is THE live, policy-driven QuasarCert verify path
// (Red H3 wiring). It decodes the cert and routes through
// QuasarCert.VerifyUnderPolicy using the chain's certPolicy: which legs
// are MANDATORY is decided by certPolicy.RequiredLegs(), NOT by the cert
// bytes. On a strict-PQ chain (Variant=Strict) BLS is not in the required
// set and the PQ legs (Pulsar, Corona) plus the ML-DSA identity rollup are
// required — so a leg-stripped (e.g. BLS-only) cert is rejected here,
// before any chance of degenerating to the BLS-permissive path.
//
// keys supplies the per-leg verification keys for the legs the policy
// requires; a missing required key is a hard rejection (VerifyUnderPolicy
// Gate 1). This replaces the old VerifyWithRealKeys (implied-policy) path
// as the production verifier.
func (p *QuantumPolicy) VerifyCertUnderPolicy(cert *Certificate, keys quasar.CertKeys) error {
if cert.PolicyID != PolicyQuantum {
return fmt.Errorf("quantum: wrong policy %d", cert.PolicyID)
}
qc := &quasar.QuasarCert{}
if err := qc.UnmarshalBinary(cert.Proof); err != nil {
return fmt.Errorf("quantum: decode QuasarCert: %w", err)
}
p.mu.RLock()
cp := p.certPolicy
p.mu.RUnlock()
if !qc.VerifyUnderPolicy(certMessageDigest(cert), cp, keys) {
return fmt.Errorf("quantum: QuasarCert verification failed under cert policy %s", cp.WireName())
}
return nil
}
// VerifyWithKeys verifies a certificate against the chain's cert policy,
// supplying the three classical-era keys (BLS aggregate, Corona group,
// ML-DSA validators). It is a thin adapter over the policy-driven
// VerifyCertUnderPolicy: it builds a quasar.CertKeys from the supplied keys
// and delegates, so the mandatory-leg decision is the chain's CertPolicy,
// never the cert bytes.
//
// For a strict-PQ chain whose CertPolicy also requires the Pulsar /
// Magnetar legs, callers MUST use VerifyCertUnderPolicy directly with the
// full key set; this 3-key adapter cannot supply those keys and the verify
// will (correctly) reject for the missing required key.
func (p *QuantumPolicy) VerifyWithKeys(
cert *Certificate,
blsAggKey *bls.PublicKey,
rtGroupKey *coronaThreshold.GroupKey,
mldsaKeys []*mldsa.PublicKey,
) error {
return p.VerifyCertUnderPolicy(cert, quasar.CertKeys{
BLS: blsAggKey,
Corona: rtGroupKey,
MLDSA: mldsaKeys,
})
}
// =============================================================================
// HOSTNAME VALIDATION: No IP literals in peer gossip
// =============================================================================
// For P+Q security, peer addresses MUST use hostnames, never IP literals.
// This prevents IP spoofing attacks and ensures DNS-based identity verification.
// =============================================================================
// HostnameValidationError is returned when an IP literal is used instead of hostname
type HostnameValidationError struct {
Address string
Reason string
}
func (e *HostnameValidationError) Error() string {
return "hostname validation failed for " + e.Address + ": " + e.Reason
}
// ValidateHostnameOnly validates that an address uses a hostname, not an IP literal.
// Returns nil if valid, HostnameValidationError if invalid.
func ValidateHostnameOnly(addr string) error {
if addr == "" {
return &HostnameValidationError{Address: addr, Reason: "empty address"}
}
// Extract host part (before any port)
host := addr
if colonIdx := lastColon(addr); colonIdx != -1 {
// Check for IPv6 bracket notation [::1]:port
if len(addr) > 0 && addr[0] == '[' {
bracketIdx := indexOf(addr, ']')
if bracketIdx != -1 {
host = addr[1:bracketIdx] // IPv6 inside brackets
}
} else {
host = addr[:colonIdx]
}
}
// Check for IPv4 literals (digits and dots only)
if isIPv4Literal(host) {
return &HostnameValidationError{
Address: addr,
Reason: "IPv4 literals not allowed - use hostname",
}
}
// Check for IPv6 literals (contains colons, or bracketed)
if isIPv6Literal(host) {
return &HostnameValidationError{
Address: addr,
Reason: "IPv6 literals not allowed - use hostname",
}
}
// Valid hostname
return nil
}
// IsValidHostname returns true if the address uses a valid hostname
func IsValidHostname(addr string) bool {
return ValidateHostnameOnly(addr) == nil
}
// ValidatePeerAddress validates a peer address for P+Q compliance:
// - Must be hostname (not IP literal)
// - Must be non-empty
func ValidatePeerAddress(addr string) error {
return ValidateHostnameOnly(addr)
}
// isIPv4Literal checks if a string looks like an IPv4 address
func isIPv4Literal(s string) bool {
if s == "" {
return false
}
dotCount := 0
for i := 0; i < len(s); i++ {
c := s[i]
if c == '.' {
dotCount++
} else if c < '0' || c > '9' {
return false // Contains non-digit, non-dot
}
}
return dotCount == 3 // IPv4 has exactly 3 dots
}
// isIPv6Literal checks if a string looks like an IPv6 address
func isIPv6Literal(s string) bool {
if s == "" {
return false
}
// IPv6 contains colons and hex digits
hasColon := false
for i := 0; i < len(s); i++ {
c := s[i]
if c == ':' {
hasColon = true
} else if (c < '0' || c > '9') && (c < 'a' || c > 'f') && (c < 'A' || c > 'F') {
// Contains non-hex, non-colon - probably a hostname
return false
}
}
return hasColon
}
// lastColon finds the last colon in a string, returns -1 if not found
func lastColon(s string) int {
for i := len(s) - 1; i >= 0; i-- {
if s[i] == ':' {
return i
}
}
return -1
}
// indexOf finds the first occurrence of a byte in a string
func indexOf(s string, c byte) int {
for i := 0; i < len(s); i++ {
if s[i] == c {
return i
}
}
return -1
}