mirror of
https://github.com/luxfi/consensus.git
synced 2026-09-24 00:26:58 +00:00
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]>
908 lines
26 KiB
Go
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
|
|
}
|