mirror of
https://github.com/luxfi/consensus.git
synced 2026-09-24 00:26:58 +00:00
Replaces every direct import of luxfi/corona/* and luxfi/pulsar/ref/go
in consensus with the canonical luxfi/threshold/protocols/{corona,pulsar}
alias surface. Pure import swap + go.mod bump; no behaviour change.
Files touched (16):
bench/pq_modes_bench_test.go
engine/pq/crypto.go
engine/pq/engine.go
pkg/wire/policies.go
protocol/quasar/cert_size_compare_test.go
protocol/quasar/corona_gob.go
protocol/quasar/dual_threshold_test.go
protocol/quasar/epoch.go
protocol/quasar/epoch_test.go
protocol/quasar/gpu_accel.go
protocol/quasar/grouped_threshold.go
protocol/quasar/quasar.go
protocol/quasar/types.go
protocol/quasar/wave_signer.go
protocol/quasar/wave_signer_test.go
protocol/quasar/witness.go
protocol/quasar/witness_pulsar_test.go
Import path swaps:
luxfi/corona/threshold -> luxfi/threshold/protocols/corona
luxfi/corona/keyera -> luxfi/threshold/protocols/corona/keyera
luxfi/corona/primitives -> luxfi/threshold/protocols/corona/primitives
luxfi/corona/reshare -> luxfi/threshold/protocols/corona/reshare
luxfi/corona/sign -> luxfi/threshold/protocols/corona/sign
luxfi/corona/gpu -> luxfi/threshold/protocols/corona/gpu
luxfi/pulsar/ref/go/pkg/pulsar -> luxfi/threshold/protocols/pulsar
The aliased package name "coronaThreshold" used by quasar / engine/pq /
pkg/wire identifiers is preserved at the call sites (it now refers to
the threshold/protocols/corona alias package). engine/pq/crypto.go is
the only file where the package was bare-imported as `threshold`; that
identifier was renamed to `corona` to match the new alias path.
go.mod:
- luxfi/threshold bumped to a branch pseudo-version that carries the
new alias subpackages; will pin to a stable tag once threshold's
companion commit lands and a release is cut.
- luxfi/corona and luxfi/pulsar drop to indirect (consensus now
reaches them only via threshold/protocols).
- luxfi/math v1.4.0 -> v1.4.1 and luxfi/metric v1.5.0 -> v1.5.7 are
transitive minimum-version bumps pulled by the new threshold
requirement; they are no-op for consensus call sites.
Comment-only references to "github.com/luxfi/corona/..." in bls.go,
round_digest.go, reshare_epoch.go, comprehensive_test.go,
engine_test.go, quasar_bench_test.go and config/pq_mode.go are
documentation pointers to where the underlying implementation lives;
they are accurate either way and are left unchanged (out of refactor
scope).
Layering shape after:
consensus -> threshold/protocols/{corona,pulsar} (stable alias)
|
v
luxfi/corona, luxfi/pulsar (primitive implementations)
Closes the 22-file layering violation flagged by the 2026-05-31
integration audit. Tests pass: ./engine/pq/..., ./pkg/wire/...,
./protocol/quasar/... (all key cases — TestEpochManager*,
TestRoundSigner*, TestVerifyPQFinality*, TestBLSThreshold*,
TestDualSigningFlow, TestCoronaCertSize* — green; the pre-existing
TestGroupedEpochManager_ParallelKeygen perf-bound flake on slow
machines was reproduced on unmodified main, so it is not a
regression).
848 lines
23 KiB
Go
848 lines
23 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/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
|
|
}
|
|
|
|
// 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.
|
|
func NewQuantumPolicy(threshold int) *QuantumPolicy {
|
|
return &QuantumPolicy{
|
|
threshold: threshold,
|
|
requireRT: true, // DEFAULT: RT REQUIRED for quantum safety
|
|
candidates: make(map[CandidateID]*Candidate),
|
|
blsVotes: make(map[CandidateID]map[VoterID][]byte),
|
|
pqVotes: make(map[CandidateID]map[VoterID][]byte),
|
|
certs: make(map[CandidateID]*Certificate),
|
|
}
|
|
}
|
|
|
|
// NewQuantumPolicyWithOptions creates a quantum policy with explicit RT requirement
|
|
func NewQuantumPolicyWithOptions(threshold int, requireRT bool) *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),
|
|
}
|
|
}
|
|
|
|
// 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
|
|
}
|
|
|
|
// VerifyWithKeys performs cryptographic verification of the certificate
|
|
// using the supplied BLS aggregate, Corona group, and ML-DSA validator
|
|
// keys. Returns nil iff every embedded signature verifies against the
|
|
// canonical message digest sha256(cert.CandidateID || cert.Height).
|
|
func (p *QuantumPolicy) VerifyWithKeys(
|
|
cert *Certificate,
|
|
blsAggKey *bls.PublicKey,
|
|
rtGroupKey *coronaThreshold.GroupKey,
|
|
mldsaKeys []*mldsa.PublicKey,
|
|
) 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)
|
|
}
|
|
|
|
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[:])
|
|
msg := h.Sum(nil)
|
|
|
|
if !qc.VerifyWithRealKeys(msg, blsAggKey, rtGroupKey, mldsaKeys) {
|
|
return fmt.Errorf("quantum: QuasarCert cryptographic verification failed")
|
|
}
|
|
return nil
|
|
}
|
|
|
|
// =============================================================================
|
|
// 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
|
|
}
|