mirror of
https://github.com/luxfi/consensus.git
synced 2026-09-24 00:26:58 +00:00
Comment + test-data renames in 4 files. No semantic change. Per the canonical no-subnet rule. protocol/quasar/core.go: 5 comment lines (subnets/chains → chains). protocol/quasar/comprehensive_test.go: test data "New-Subnet" → "New-Chain". protocol/quasar/dynamic_test.go: TestAutoRegisterNewSubnet → TestAutoRegisterNewChain. bench/lux_vs_grpc_test.go: local var `subnet` → `chainID`. Build clean. Renamed test passes (TestAutoRegisterNewChain).
901 lines
26 KiB
Go
901 lines
26 KiB
Go
// Copyright (C) 2025, Lux Industries Inc All rights reserved.
|
|
// Quasar - The supermassive black hole at the center of the blockchain galaxy
|
|
// Processes blocks from all chains with quantum consensus
|
|
|
|
package quasar
|
|
|
|
import (
|
|
"context"
|
|
"crypto/sha256"
|
|
"encoding/hex"
|
|
"errors"
|
|
"fmt"
|
|
"sync"
|
|
"time"
|
|
|
|
"github.com/luxfi/consensus/config"
|
|
)
|
|
|
|
// Quasar is the supermassive black hole at the center of the blockchain galaxy
|
|
// It collects blocks from ALL chains (P, X, C, + any new chains) and applies quantum consensus
|
|
// External systems (bridges, contracts) can use RPC to add blocks to the event horizon
|
|
type Quasar struct {
|
|
mu sync.RWMutex
|
|
|
|
// Dynamic chain registration - automatically includes new chains
|
|
chainBuffers map[string]chan *ChainBlock // chainName -> buffer
|
|
|
|
// Standard chain buffers (P/X/C)
|
|
pChainBlocks chan *ChainBlock
|
|
xChainBlocks chan *ChainBlock
|
|
cChainBlocks chan *ChainBlock
|
|
|
|
// Quantum consensus engine - the event horizon
|
|
signer *signer
|
|
|
|
// Epoch-based Corona key management
|
|
// Keys rotate when validator set changes (max 1x per hour)
|
|
epochManager *EpochManager
|
|
|
|
// Quantum finality state - the singularity
|
|
pendingBlocks map[string]*QuantumBlock // blockHash -> awaiting threshold sigs
|
|
finalizedBlocks map[string]*QuantumBlock // blockHash -> finalized block
|
|
quantumHeight uint64
|
|
|
|
// Metrics - radiation emitted by the accretion disk
|
|
processedBlocks uint64
|
|
quantumProofs uint64
|
|
|
|
// Chain registry - track all registered chains
|
|
registeredChains map[string]bool // chainName -> active
|
|
|
|
// Context for starting chain processors
|
|
ctx context.Context
|
|
|
|
// profile, when non-nil and strict-PQ / FIPS, demands triple-mode
|
|
// vote acceptance: addVoteLocked refuses any per-validator sig that
|
|
// doesn't carry both BLS and MLDSA layers. Closes CR-10 at the
|
|
// vote-accept layer (cert.Verify() closes it at finalisation).
|
|
profile *config.ChainSecurityProfile
|
|
}
|
|
|
|
// ChainBlock is an alias for Block used in chain-specific submission methods.
|
|
type ChainBlock = Block
|
|
|
|
// QuantumBlock represents a quantum-finalized aggregate block.
|
|
type QuantumBlock struct {
|
|
Height uint64
|
|
Epoch uint64 // Epoch in which this block was created
|
|
SourceBlocks []*Block
|
|
QuantumHash string
|
|
BLSSignature []byte
|
|
CoronaProof []byte // ML-DSA signature
|
|
Timestamp time.Time
|
|
CreatedAt time.Time // When this block entered the pending set
|
|
ValidatorSigs map[string]*QuasarSig
|
|
}
|
|
|
|
const (
|
|
// pendingBlockTTL is the maximum time a block may remain in the pending set
|
|
// before being evicted. Blocks that cannot gather enough signatures within
|
|
// this window are stale and should be re-submitted.
|
|
pendingBlockTTL = 1 * time.Minute
|
|
|
|
// finalizedEpochRetention is the number of recent epochs whose finalized
|
|
// blocks are kept in memory. Older epochs are pruned to bound memory.
|
|
// Matches EpochManager history depth.
|
|
finalizedEpochRetention uint64 = 6
|
|
)
|
|
|
|
// NewQuasar creates the supermassive black hole at the center of the blockchain galaxy.
|
|
// Threshold must be >= 2 for production use. For single-node testing, use NewTestQuasar.
|
|
func NewQuasar(threshold int) (*Quasar, error) {
|
|
if threshold < 2 {
|
|
return nil, ErrThresholdTooLow
|
|
}
|
|
s, err := newSigner(threshold)
|
|
if err != nil {
|
|
return nil, fmt.Errorf("failed to create signer: %w", err)
|
|
}
|
|
|
|
core := &Quasar{
|
|
pChainBlocks: make(chan *ChainBlock, 100),
|
|
xChainBlocks: make(chan *ChainBlock, 100),
|
|
cChainBlocks: make(chan *ChainBlock, 100),
|
|
chainBuffers: make(map[string]chan *ChainBlock),
|
|
signer: s,
|
|
epochManager: NewEpochManager(threshold, 3), // Keep 3 epochs in history
|
|
pendingBlocks: make(map[string]*QuantumBlock),
|
|
finalizedBlocks: make(map[string]*QuantumBlock),
|
|
quantumHeight: 0,
|
|
registeredChains: make(map[string]bool),
|
|
}
|
|
|
|
// Auto-register primary chains (errors ignored as these are guaranteed to succeed on init)
|
|
_ = core.RegisterChain("P-Chain")
|
|
_ = core.RegisterChain("X-Chain")
|
|
_ = core.RegisterChain("C-Chain")
|
|
|
|
return core, nil
|
|
}
|
|
|
|
// NewTestQuasar creates a Quasar instance for single-node testing with threshold >= 1.
|
|
// Must NOT be used in production.
|
|
func NewTestQuasar(threshold int) (*Quasar, error) {
|
|
if threshold < 1 {
|
|
return nil, errors.New("threshold must be at least 1")
|
|
}
|
|
s, err := newSigner(threshold)
|
|
if err != nil {
|
|
return nil, fmt.Errorf("failed to create signer: %w", err)
|
|
}
|
|
|
|
core := &Quasar{
|
|
pChainBlocks: make(chan *ChainBlock, 100),
|
|
xChainBlocks: make(chan *ChainBlock, 100),
|
|
cChainBlocks: make(chan *ChainBlock, 100),
|
|
chainBuffers: make(map[string]chan *ChainBlock),
|
|
signer: s,
|
|
epochManager: NewEpochManager(threshold, 3),
|
|
pendingBlocks: make(map[string]*QuantumBlock),
|
|
finalizedBlocks: make(map[string]*QuantumBlock),
|
|
quantumHeight: 0,
|
|
registeredChains: make(map[string]bool),
|
|
}
|
|
|
|
_ = core.RegisterChain("P-Chain")
|
|
_ = core.RegisterChain("X-Chain")
|
|
_ = core.RegisterChain("C-Chain")
|
|
|
|
return core, nil
|
|
}
|
|
|
|
// Start begins drawing blocks into the Quasar's gravitational pull
|
|
func (q *Quasar) Start(ctx context.Context) error {
|
|
// Store context for dynamic chain registration
|
|
q.mu.Lock()
|
|
q.ctx = ctx
|
|
q.mu.Unlock()
|
|
|
|
// Start block processors for legacy chains
|
|
go q.processPChain(ctx)
|
|
go q.processXChain(ctx)
|
|
go q.processCChain(ctx)
|
|
|
|
// Start processors for ALL dynamically registered chains
|
|
go q.ProcessDynamicChains(ctx)
|
|
|
|
// Start quantum finalization engine - the singularity
|
|
go q.quantumFinalizer(ctx)
|
|
|
|
fmt.Println("[QUASAR] Event horizon activated - all chains drawn into quantum consensus")
|
|
return nil
|
|
}
|
|
|
|
// SubmitPChainBlock submits a P-Chain block for quantum consensus
|
|
func (q *Quasar) SubmitPChainBlock(block *ChainBlock) {
|
|
block.ChainName = "P-Chain"
|
|
select {
|
|
case q.pChainBlocks <- block:
|
|
default:
|
|
// Buffer full, drop oldest
|
|
<-q.pChainBlocks
|
|
q.pChainBlocks <- block
|
|
}
|
|
}
|
|
|
|
// SubmitXChainBlock submits an X-Chain block for quantum consensus
|
|
func (q *Quasar) SubmitXChainBlock(block *ChainBlock) {
|
|
block.ChainName = "X-Chain"
|
|
select {
|
|
case q.xChainBlocks <- block:
|
|
default:
|
|
<-q.xChainBlocks
|
|
q.xChainBlocks <- block
|
|
}
|
|
}
|
|
|
|
// SubmitCChainBlock submits a C-Chain block for quantum consensus
|
|
func (q *Quasar) SubmitCChainBlock(block *ChainBlock) {
|
|
block.ChainName = "C-Chain"
|
|
select {
|
|
case q.cChainBlocks <- block:
|
|
default:
|
|
<-q.cChainBlocks
|
|
q.cChainBlocks <- block
|
|
}
|
|
}
|
|
|
|
// processPChain handles P-Chain blocks
|
|
func (q *Quasar) processPChain(ctx context.Context) {
|
|
for {
|
|
select {
|
|
case <-ctx.Done():
|
|
return
|
|
case block := <-q.pChainBlocks:
|
|
q.processBlock(block)
|
|
}
|
|
}
|
|
}
|
|
|
|
// processXChain handles X-Chain blocks
|
|
func (q *Quasar) processXChain(ctx context.Context) {
|
|
for {
|
|
select {
|
|
case <-ctx.Done():
|
|
return
|
|
case block := <-q.xChainBlocks:
|
|
q.processBlock(block)
|
|
}
|
|
}
|
|
}
|
|
|
|
// processCChain handles C-Chain blocks
|
|
func (q *Quasar) processCChain(ctx context.Context) {
|
|
for {
|
|
select {
|
|
case <-ctx.Done():
|
|
return
|
|
case block := <-q.cChainBlocks:
|
|
q.processBlock(block)
|
|
}
|
|
}
|
|
}
|
|
|
|
// processBlock applies quantum consensus to a single block.
|
|
// Uses background context; for context-aware processing use processBlockWithContext.
|
|
func (q *Quasar) processBlock(block *ChainBlock) {
|
|
q.processBlockWithContext(context.Background(), block)
|
|
}
|
|
|
|
// processBlockWithContext applies quantum consensus to a single block, respecting context cancellation.
|
|
// The block is placed in pending state and only finalized when QThreshold signatures are collected.
|
|
func (q *Quasar) processBlockWithContext(ctx context.Context, block *ChainBlock) {
|
|
// Check context before acquiring lock
|
|
if ctx.Err() != nil {
|
|
return
|
|
}
|
|
|
|
q.mu.Lock()
|
|
defer q.mu.Unlock()
|
|
|
|
// Check context after acquiring lock
|
|
if ctx.Err() != nil {
|
|
return
|
|
}
|
|
|
|
// Create quantum hash combining block data
|
|
quantumHash := q.computeQuantumHash(block)
|
|
|
|
// Skip if already finalized or pending
|
|
if _, exists := q.finalizedBlocks[quantumHash]; exists {
|
|
return
|
|
}
|
|
if _, exists := q.pendingBlocks[quantumHash]; exists {
|
|
return
|
|
}
|
|
|
|
// Create pending quantum block
|
|
now := time.Now()
|
|
qBlock := &QuantumBlock{
|
|
Height: q.quantumHeight + 1,
|
|
Epoch: q.epochManager.GetCurrentEpoch(),
|
|
SourceBlocks: []*ChainBlock{block},
|
|
QuantumHash: quantumHash,
|
|
Timestamp: now,
|
|
CreatedAt: now,
|
|
ValidatorSigs: make(map[string]*QuasarSig),
|
|
}
|
|
|
|
q.pendingBlocks[quantumHash] = qBlock
|
|
|
|
// Sign with local validator key (self-vote)
|
|
sig, err := q.signer.SignMessageWithContext(ctx, "validator1", []byte(quantumHash))
|
|
if err == nil {
|
|
q.addVoteLocked(quantumHash, "validator1", sig)
|
|
}
|
|
}
|
|
|
|
// ReceiveVote records a validator's signature for a pending block.
|
|
// When the threshold is met, the block is finalized.
|
|
// Returns true if the vote was accepted, false if the block is unknown or already finalized.
|
|
func (q *Quasar) ReceiveVote(quantumHash string, validatorID string, sig *QuasarSig) bool {
|
|
q.mu.Lock()
|
|
defer q.mu.Unlock()
|
|
return q.addVoteLocked(quantumHash, validatorID, sig)
|
|
}
|
|
|
|
// addVoteLocked adds a vote and finalizes the block if threshold is met.
|
|
// Caller must hold q.mu.
|
|
//
|
|
// Under a strict-PQ / FIPS profile (CR-10) every accepted vote MUST be
|
|
// triple-layer: BLS share + MLDSA per-validator sig. (The Corona
|
|
// aggregate is produced at the higher BundleSigner layer over the
|
|
// pending block once threshold is met; per-validator votes contribute
|
|
// only the BLS share and MLDSA identity sig.) Profile-driven, not
|
|
// heuristic — a non-strict profile preserves the legacy path.
|
|
func (q *Quasar) addVoteLocked(quantumHash string, validatorID string, sig *QuasarSig) bool {
|
|
// Already finalized -- nothing to do
|
|
if _, exists := q.finalizedBlocks[quantumHash]; exists {
|
|
return false
|
|
}
|
|
|
|
qBlock, exists := q.pendingBlocks[quantumHash]
|
|
if !exists {
|
|
return false
|
|
}
|
|
|
|
// Triple-mode gate: under strict-PQ the per-validator sig MUST
|
|
// carry every layer the profile demands. Reject silently-downgraded
|
|
// votes BEFORE running signature verification (cheaper to detect a
|
|
// structural deficiency than a forged BLS share).
|
|
if q.demandsTripleLocked() {
|
|
if !q.signer.IsTripleMode() {
|
|
// Signer is not even configured for triple-mode. Under a
|
|
// strict profile this is a hard misconfiguration; refuse
|
|
// every vote until the signer is fixed.
|
|
return false
|
|
}
|
|
if sig == nil || len(sig.BLS) == 0 || len(sig.MLDSA) == 0 {
|
|
return false
|
|
}
|
|
}
|
|
|
|
// Verify the signature before accepting
|
|
if !q.signer.VerifyQuasarSig([]byte(quantumHash), sig) {
|
|
return false
|
|
}
|
|
|
|
// Record vote (dedup by validator ID)
|
|
qBlock.ValidatorSigs[validatorID] = sig
|
|
|
|
// Check threshold
|
|
if len(qBlock.ValidatorSigs) >= q.signer.threshold {
|
|
// Move from pending to finalized
|
|
delete(q.pendingBlocks, quantumHash)
|
|
q.finalizedBlocks[quantumHash] = qBlock
|
|
q.quantumHeight++
|
|
q.processedBlocks++
|
|
}
|
|
|
|
return true
|
|
}
|
|
|
|
// demandsTripleLocked reports whether the bound profile requires triple-mode
|
|
// votes. Caller MUST hold q.mu (read or write).
|
|
func (q *Quasar) demandsTripleLocked() bool {
|
|
return q.profile != nil && q.profile.IsPQ()
|
|
}
|
|
|
|
// SetProfile binds a ChainSecurityProfile to this Quasar instance. When
|
|
// the profile is strict-PQ or FIPS, addVoteLocked refuses any vote whose
|
|
// QuasarSig does not carry every layer the profile demands. Closes CR-10
|
|
// at the vote-accept layer.
|
|
//
|
|
// Nil profile preserves legacy behaviour (any vote-shape accepted).
|
|
func (q *Quasar) SetProfile(profile *config.ChainSecurityProfile) {
|
|
q.mu.Lock()
|
|
defer q.mu.Unlock()
|
|
q.profile = profile
|
|
}
|
|
|
|
// GetPendingCount returns the number of blocks awaiting threshold signatures.
|
|
func (q *Quasar) GetPendingCount() int {
|
|
q.mu.RLock()
|
|
defer q.mu.RUnlock()
|
|
return len(q.pendingBlocks)
|
|
}
|
|
|
|
// quantumFinalizer runs the quantum finalization process
|
|
func (q *Quasar) quantumFinalizer(ctx context.Context) {
|
|
ticker := time.NewTicker(10 * time.Second)
|
|
defer ticker.Stop()
|
|
|
|
for {
|
|
select {
|
|
case <-ctx.Done():
|
|
return
|
|
case <-ticker.C:
|
|
q.evictStalePendingBlocks()
|
|
q.finalizeQuantumEpoch()
|
|
}
|
|
}
|
|
}
|
|
|
|
// evictStalePendingBlocks removes pending blocks older than pendingBlockTTL.
|
|
// Blocks that cannot gather enough signatures within the TTL window are
|
|
// considered stale and must be re-submitted.
|
|
func (q *Quasar) evictStalePendingBlocks() {
|
|
q.mu.Lock()
|
|
defer q.mu.Unlock()
|
|
|
|
now := time.Now()
|
|
for hash, qb := range q.pendingBlocks {
|
|
if now.Sub(qb.CreatedAt) > pendingBlockTTL {
|
|
delete(q.pendingBlocks, hash)
|
|
}
|
|
}
|
|
}
|
|
|
|
// finalizeQuantumEpoch creates a quantum proof for the current epoch
|
|
func (q *Quasar) finalizeQuantumEpoch() {
|
|
q.mu.Lock()
|
|
defer q.mu.Unlock()
|
|
|
|
if len(q.finalizedBlocks) == 0 {
|
|
return
|
|
}
|
|
|
|
// Create epoch hash from all blocks
|
|
epochData := ""
|
|
for hash := range q.finalizedBlocks {
|
|
epochData += hash
|
|
}
|
|
|
|
_ = sha256.Sum256([]byte(epochData))
|
|
|
|
// Generate quantum proof (BLS aggregated + Corona)
|
|
// Aggregation of multi-validator proofs is handled by the Signer
|
|
q.quantumProofs++
|
|
|
|
// Prune finalized blocks from old epochs to bound memory growth.
|
|
currentEpoch := q.epochManager.GetCurrentEpoch()
|
|
if currentEpoch > finalizedEpochRetention {
|
|
cutoff := currentEpoch - finalizedEpochRetention
|
|
for hash, qb := range q.finalizedBlocks {
|
|
if qb.Epoch < cutoff {
|
|
delete(q.finalizedBlocks, hash)
|
|
}
|
|
}
|
|
}
|
|
|
|
// Log quantum finality achievement
|
|
fmt.Printf("[QUANTUM] Epoch finalized at height %d with %d blocks, proof #%d\n",
|
|
q.quantumHeight, len(q.finalizedBlocks), q.quantumProofs)
|
|
}
|
|
|
|
// computeQuantumHash creates a quantum-resistant hash
|
|
func (q *Quasar) computeQuantumHash(block *Block) string {
|
|
// Combine block data with quantum parameters
|
|
data := fmt.Sprintf("%s:%x:%d:%d",
|
|
block.ChainName,
|
|
block.ID[:],
|
|
block.Height,
|
|
block.Timestamp.Unix())
|
|
|
|
// SHA-256 provides 128-bit quantum security (Grover's sqrt speedup on 256-bit)
|
|
hash := sha256.Sum256([]byte(data))
|
|
return hex.EncodeToString(hash[:])
|
|
}
|
|
|
|
// GetQuantumHeight returns the current quantum finalized height
|
|
func (q *Quasar) GetQuantumHeight() uint64 {
|
|
q.mu.RLock()
|
|
defer q.mu.RUnlock()
|
|
return q.quantumHeight
|
|
}
|
|
|
|
// GetMetrics returns aggregator metrics
|
|
func (q *Quasar) GetMetrics() (processedBlocks, quantumProofs uint64) {
|
|
q.mu.RLock()
|
|
defer q.mu.RUnlock()
|
|
return q.processedBlocks, q.quantumProofs
|
|
}
|
|
|
|
// VerifyQuantumFinality checks if a block has quantum finality.
|
|
// For context-aware verification, use VerifyQuantumFinalityWithContext.
|
|
func (q *Quasar) VerifyQuantumFinality(blockHash string) bool {
|
|
return q.VerifyQuantumFinalityWithContext(context.Background(), blockHash)
|
|
}
|
|
|
|
// VerifyQuantumFinalityWithContext checks if a block has quantum finality, respecting context cancellation.
|
|
func (q *Quasar) VerifyQuantumFinalityWithContext(ctx context.Context, blockHash string) bool {
|
|
// Check context before acquiring lock
|
|
if ctx.Err() != nil {
|
|
return false
|
|
}
|
|
|
|
q.mu.RLock()
|
|
defer q.mu.RUnlock()
|
|
|
|
// Check context after acquiring lock
|
|
if ctx.Err() != nil {
|
|
return false
|
|
}
|
|
|
|
qBlock, exists := q.finalizedBlocks[blockHash]
|
|
if !exists {
|
|
return false
|
|
}
|
|
|
|
// Verify both BLS and Corona signatures
|
|
for validatorID, sig := range qBlock.ValidatorSigs {
|
|
// Check context periodically during loop
|
|
if ctx.Err() != nil {
|
|
return false
|
|
}
|
|
if !q.signer.VerifyQuasarSigWithContext(ctx, []byte(blockHash), sig) {
|
|
fmt.Printf("[QUANTUM] Invalid signature from validator %s\n", validatorID)
|
|
return false
|
|
}
|
|
}
|
|
|
|
return true
|
|
}
|
|
|
|
// RegisterChain dynamically registers a new chain for automatic quantum security
|
|
// All new chains are automatically protected by the event horizon
|
|
func (q *Quasar) RegisterChain(chainName string) error {
|
|
q.mu.Lock()
|
|
|
|
if q.registeredChains[chainName] {
|
|
q.mu.Unlock()
|
|
return nil // Already registered
|
|
}
|
|
|
|
// Create buffer for this chain
|
|
q.chainBuffers[chainName] = make(chan *ChainBlock, 100)
|
|
q.registeredChains[chainName] = true
|
|
|
|
// Get context for starting processor
|
|
ctx := q.ctx
|
|
q.mu.Unlock()
|
|
|
|
// Start processor for this chain if we have a context (i.e., Start was called)
|
|
if ctx != nil {
|
|
go q.processChain(ctx, chainName)
|
|
}
|
|
|
|
fmt.Printf("[QUASAR] Chain '%s' pulled into event horizon - quantum security active\n", chainName)
|
|
return nil
|
|
}
|
|
|
|
// SubmitBlock is the universal RPC endpoint for ANY chain/contract to add blocks
|
|
// External systems (bridge, contracts) use this to enter the event horizon
|
|
func (q *Quasar) SubmitBlock(block *ChainBlock) error {
|
|
q.mu.RLock()
|
|
// Auto-register chain if not yet registered
|
|
if !q.registeredChains[block.ChainName] {
|
|
q.mu.RUnlock()
|
|
if err := q.RegisterChain(block.ChainName); err != nil {
|
|
return fmt.Errorf("failed to auto-register chain %s: %w", block.ChainName, err)
|
|
}
|
|
q.mu.RLock()
|
|
}
|
|
|
|
buffer, exists := q.chainBuffers[block.ChainName]
|
|
q.mu.RUnlock()
|
|
|
|
if !exists {
|
|
return fmt.Errorf("chain %s not registered", block.ChainName)
|
|
}
|
|
|
|
select {
|
|
case buffer <- block:
|
|
// Block accepted into event horizon
|
|
return nil
|
|
default:
|
|
// Buffer full, drop oldest and insert new
|
|
<-buffer
|
|
buffer <- block
|
|
return nil
|
|
}
|
|
}
|
|
|
|
// ProcessDynamicChains starts processors for all dynamically registered chains
|
|
// This runs alongside the legacy P/X/C chain processors
|
|
func (q *Quasar) ProcessDynamicChains(ctx context.Context) {
|
|
q.mu.RLock()
|
|
chains := make([]string, 0, len(q.chainBuffers))
|
|
for chain := range q.chainBuffers {
|
|
chains = append(chains, chain)
|
|
}
|
|
q.mu.RUnlock()
|
|
|
|
// Start a processor for each registered chain
|
|
for _, chainName := range chains {
|
|
go q.processChain(ctx, chainName)
|
|
}
|
|
}
|
|
|
|
// processChain handles blocks from any dynamically registered chain
|
|
func (q *Quasar) processChain(ctx context.Context, chainName string) {
|
|
q.mu.RLock()
|
|
buffer := q.chainBuffers[chainName]
|
|
q.mu.RUnlock()
|
|
|
|
for {
|
|
select {
|
|
case <-ctx.Done():
|
|
return
|
|
case block := <-buffer:
|
|
q.processBlock(block)
|
|
}
|
|
}
|
|
}
|
|
|
|
// GetRegisteredChains returns all chains currently in the event horizon
|
|
func (q *Quasar) GetRegisteredChains() []string {
|
|
q.mu.RLock()
|
|
defer q.mu.RUnlock()
|
|
|
|
chains := make([]string, 0, len(q.registeredChains))
|
|
for chain := range q.registeredChains {
|
|
chains = append(chains, chain)
|
|
}
|
|
return chains
|
|
}
|
|
|
|
// ============================================================================
|
|
// Epoch-Based Validator Management
|
|
// BLS and Corona validator sets are kept synchronized.
|
|
// Corona keys rotate at most once per hour when validator set changes.
|
|
// ============================================================================
|
|
|
|
// InitializeValidators sets up the initial validator set with both BLS and Corona keys.
|
|
// This should be called once at genesis or node startup.
|
|
func (q *Quasar) InitializeValidators(validatorIDs []string) error {
|
|
q.mu.Lock()
|
|
defer q.mu.Unlock()
|
|
|
|
if len(validatorIDs) < 2 {
|
|
return fmt.Errorf("need at least 2 validators")
|
|
}
|
|
|
|
// Add validators to signer (BLS)
|
|
for _, id := range validatorIDs {
|
|
if err := q.signer.AddValidator(id, 1); err != nil {
|
|
return fmt.Errorf("failed to add validator %s to BLS: %w", id, err)
|
|
}
|
|
}
|
|
|
|
// Initialize Corona epoch with same validator set
|
|
_, err := q.epochManager.InitializeEpoch(validatorIDs)
|
|
if err != nil {
|
|
return fmt.Errorf("failed to initialize Corona epoch: %w", err)
|
|
}
|
|
|
|
fmt.Printf("[QUASAR] Initialized %d validators with BLS + Corona (epoch 0)\n", len(validatorIDs))
|
|
return nil
|
|
}
|
|
|
|
// UpdateValidatorSet updates the validator set, rotating Corona keys if needed.
|
|
// Returns true if Corona keys were rotated.
|
|
// Rate-limited to at most 1 rotation per hour.
|
|
func (q *Quasar) UpdateValidatorSet(validatorIDs []string) (rotated bool, err error) {
|
|
q.mu.Lock()
|
|
defer q.mu.Unlock()
|
|
|
|
// Update BLS validators
|
|
// First, track current validators
|
|
currentBLS := make(map[string]bool)
|
|
for id, v := range q.signer.validators {
|
|
if v.Active {
|
|
currentBLS[id] = true
|
|
}
|
|
}
|
|
|
|
// Add new validators
|
|
newIDs := make(map[string]bool)
|
|
for _, id := range validatorIDs {
|
|
newIDs[id] = true
|
|
if !currentBLS[id] {
|
|
if err := q.signer.AddValidator(id, 1); err != nil {
|
|
return false, fmt.Errorf("failed to add validator %s: %w", id, err)
|
|
}
|
|
}
|
|
}
|
|
|
|
// Deactivate removed validators
|
|
for id := range currentBLS {
|
|
if !newIDs[id] {
|
|
if v, exists := q.signer.validators[id]; exists {
|
|
v.Active = false
|
|
}
|
|
}
|
|
}
|
|
|
|
// Attempt to rotate Corona keys (will fail if rate-limited or unchanged)
|
|
_, err = q.epochManager.RotateEpoch(validatorIDs, false)
|
|
if err == nil {
|
|
rotated = true
|
|
fmt.Printf("[QUASAR] Rotated Corona keys to epoch %d for %d validators\n",
|
|
q.epochManager.GetCurrentEpoch(), len(validatorIDs))
|
|
} else if errors.Is(err, ErrEpochRateLimited) || errors.Is(err, ErrNoValidatorChange) {
|
|
// Not an error - just rate limited or no change
|
|
rotated = false
|
|
err = nil
|
|
}
|
|
|
|
return rotated, err
|
|
}
|
|
|
|
// AddValidator adds a single validator, triggering key rotation if rate limit allows.
|
|
func (q *Quasar) AddValidator(validatorID string, weight uint64) (rotated bool, err error) {
|
|
q.mu.Lock()
|
|
defer q.mu.Unlock()
|
|
|
|
// Add to BLS
|
|
if err := q.signer.AddValidator(validatorID, weight); err != nil {
|
|
return false, err
|
|
}
|
|
|
|
// Get current validator list and add new one
|
|
validators := q.getActiveValidatorIDsLocked()
|
|
if !contains(validators, validatorID) {
|
|
validators = append(validators, validatorID)
|
|
}
|
|
|
|
// Attempt Corona rotation
|
|
_, err = q.epochManager.RotateEpoch(validators, false)
|
|
if err == nil {
|
|
rotated = true
|
|
fmt.Printf("[QUASAR] Added validator %s, rotated to epoch %d\n",
|
|
validatorID, q.epochManager.GetCurrentEpoch())
|
|
} else if errors.Is(err, ErrEpochRateLimited) || errors.Is(err, ErrNoValidatorChange) || errors.Is(err, ErrInvalidValidatorSet) {
|
|
// Rate limited, no change, or insufficient validators (e.g., first validator added)
|
|
// Validator added to BLS but RT keys not rotated yet
|
|
rotated = false
|
|
err = nil
|
|
fmt.Printf("[QUASAR] Added validator %s to BLS (RT rotation pending)\n", validatorID)
|
|
}
|
|
|
|
return rotated, err
|
|
}
|
|
|
|
// RemoveValidator removes a validator, triggering key rotation if rate limit allows.
|
|
func (q *Quasar) RemoveValidator(validatorID string) (rotated bool, err error) {
|
|
q.mu.Lock()
|
|
defer q.mu.Unlock()
|
|
|
|
// Deactivate in BLS
|
|
if v, exists := q.signer.validators[validatorID]; exists {
|
|
v.Active = false
|
|
}
|
|
|
|
// Get remaining validators
|
|
validators := q.getActiveValidatorIDsLocked()
|
|
|
|
// Attempt Corona rotation
|
|
_, err = q.epochManager.RotateEpoch(validators, false)
|
|
if err == nil {
|
|
rotated = true
|
|
fmt.Printf("[QUASAR] Removed validator %s, rotated to epoch %d\n",
|
|
validatorID, q.epochManager.GetCurrentEpoch())
|
|
} else if errors.Is(err, ErrEpochRateLimited) || errors.Is(err, ErrNoValidatorChange) || errors.Is(err, ErrInvalidValidatorSet) {
|
|
rotated = false
|
|
err = nil
|
|
fmt.Printf("[QUASAR] Removed validator %s from BLS (RT rotation pending)\n", validatorID)
|
|
}
|
|
|
|
return rotated, err
|
|
}
|
|
|
|
// ForceEpochRotation forces Corona key rotation if minimum time has passed.
|
|
// Use this for periodic security refreshes even without validator changes.
|
|
func (q *Quasar) ForceEpochRotation() (rotated bool, err error) {
|
|
q.mu.Lock()
|
|
validators := q.getActiveValidatorIDsLocked()
|
|
q.mu.Unlock()
|
|
|
|
keys, rotated, err := q.epochManager.ForceRotateIfExpired()
|
|
if err != nil {
|
|
return false, err
|
|
}
|
|
if rotated {
|
|
fmt.Printf("[QUASAR] Force-rotated Corona keys to epoch %d\n", keys.Epoch)
|
|
}
|
|
|
|
// Also attempt rotation if rate limit allows
|
|
if !rotated {
|
|
_, err = q.epochManager.RotateEpoch(validators, true)
|
|
if err == nil {
|
|
rotated = true
|
|
} else if errors.Is(err, ErrEpochRateLimited) {
|
|
err = nil
|
|
}
|
|
}
|
|
|
|
return rotated, err
|
|
}
|
|
|
|
// GetEpochStats returns current epoch statistics.
|
|
func (q *Quasar) GetEpochStats() EpochStats {
|
|
return q.epochManager.Stats()
|
|
}
|
|
|
|
// GetCurrentEpoch returns the current Corona epoch number.
|
|
func (q *Quasar) GetCurrentEpoch() uint64 {
|
|
return q.epochManager.GetCurrentEpoch()
|
|
}
|
|
|
|
// GetEpochManager returns the epoch manager for advanced operations.
|
|
func (q *Quasar) GetEpochManager() *EpochManager {
|
|
return q.epochManager
|
|
}
|
|
|
|
// SignMessage signs a message with the specified validator's key.
|
|
func (q *Quasar) SignMessage(validatorID string, message []byte) (*QuasarSig, error) {
|
|
return q.signer.SignMessage(validatorID, message)
|
|
}
|
|
|
|
// AggregateSignatures aggregates multiple signatures into one.
|
|
func (q *Quasar) AggregateSignatures(message []byte, signatures []*QuasarSig) (*AggregatedSignature, error) {
|
|
return q.signer.AggregateSignatures(message, signatures)
|
|
}
|
|
|
|
// VerifyAggregatedSignature verifies an aggregated signature.
|
|
func (q *Quasar) VerifyAggregatedSignature(message []byte, sig *AggregatedSignature) bool {
|
|
return q.signer.VerifyAggregatedSignature(message, sig)
|
|
}
|
|
|
|
// SignMessageWithContext signs a message with context cancellation support.
|
|
func (q *Quasar) SignMessageWithContext(ctx context.Context, validatorID string, message []byte) (*QuasarSig, error) {
|
|
return q.signer.SignMessageWithContext(ctx, validatorID, message)
|
|
}
|
|
|
|
// AggregateSignaturesWithContext aggregates multiple signatures with context support.
|
|
func (q *Quasar) AggregateSignaturesWithContext(ctx context.Context, message []byte, signatures []*QuasarSig) (*AggregatedSignature, error) {
|
|
return q.signer.AggregateSignaturesWithContext(ctx, message, signatures)
|
|
}
|
|
|
|
// VerifyAggregatedSignatureWithContext verifies an aggregated signature with context support.
|
|
func (q *Quasar) VerifyAggregatedSignatureWithContext(ctx context.Context, message []byte, sig *AggregatedSignature) bool {
|
|
return q.signer.VerifyAggregatedSignatureWithContext(ctx, message, sig)
|
|
}
|
|
|
|
// IsThresholdMode returns true if the signer is in threshold mode.
|
|
func (q *Quasar) IsThresholdMode() bool {
|
|
return q.signer.IsThresholdMode()
|
|
}
|
|
|
|
// IsDualThresholdMode returns true if the signer is in dual threshold mode (BLS + Corona).
|
|
func (q *Quasar) IsDualThresholdMode() bool {
|
|
return q.signer.IsDualThresholdMode()
|
|
}
|
|
|
|
// GetActiveValidatorCount returns the number of active validators.
|
|
func (q *Quasar) GetActiveValidatorCount() int {
|
|
return q.signer.GetActiveValidatorCount()
|
|
}
|
|
|
|
// GetThreshold returns the consensus threshold.
|
|
func (q *Quasar) GetThreshold() int {
|
|
return q.signer.GetThreshold()
|
|
}
|
|
|
|
// VerifyQuasarSig verifies a QuasarSig signature.
|
|
func (q *Quasar) VerifyQuasarSig(message []byte, sig *QuasarSig) bool {
|
|
return q.signer.VerifyQuasarSig(message, sig)
|
|
}
|
|
|
|
// CoronaRound1 executes Round 1 of the Corona threshold signing protocol.
|
|
func (q *Quasar) CoronaRound1(validatorID string, sessionID int, prfKey []byte) (*CoronaRound1Data, error) {
|
|
data, err := q.signer.CoronaRound1(validatorID, sessionID, prfKey)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
return &CoronaRound1Data{PartyID: data.PartyID}, nil
|
|
}
|
|
|
|
// Internal helpers
|
|
|
|
func (q *Quasar) getActiveValidatorIDsLocked() []string {
|
|
ids := make([]string, 0, len(q.signer.validators))
|
|
for id, v := range q.signer.validators {
|
|
if v.Active {
|
|
ids = append(ids, id)
|
|
}
|
|
}
|
|
return ids
|
|
}
|
|
|
|
func contains(slice []string, item string) bool {
|
|
for _, s := range slice {
|
|
if s == item {
|
|
return true
|
|
}
|
|
}
|
|
return false
|
|
}
|