mirror of
https://github.com/luxfi/node.git
synced 2026-07-27 03:39:39 +00:00
consensus: remove superseded dormant Quasar hybrid-finality hub
node/consensus/quasar + node/consensus/zap + node/node/quasar.go were a
superseded duplicate of the live post-quantum finality path
(consensus/protocol/quasar WeightedQuorumCert, driven by the chain engine).
The node-side hub wrapped that live core but added only dormant scaffolding:
- an undriven P-Chain finality loop (finCh never fed; run() sat idle)
- a fail-closed CoronaCoordinator stub (Sign/Verify errored in prod,
InitializeCorona was test-only, so q.corona stayed nil)
- a QuantumFinality store nothing read (only n.Quasar.Stop() was ever called)
Dependency trace: zero cross-repo importers; the Q-Chain VM
(luxfi/chains/quantumvm) is external, independent, and finalized by the live
chain-engine QuorumCert path, not this hub; consensus/zap (ZAP/DID agentic
bridge) was a collateral dormant island on the same stub with no external
importers. Wiring was impossible without driving the live accept path or
adding a redundant second finality authority, both out of scope, so the hub
is removed rather than wired.
Live consensus/protocol/quasar and consensus/engine/chain are untouched.
Q-Chain VM registration, genesis, and critical-by-default status are
unchanged. Genuine PQ tests (TLS hybrid KEX, ML-DSA UTXO, hostname
addressing) are preserved; only the dormant-hub tests were removed.
This commit is contained in:
@@ -1,353 +0,0 @@
|
||||
// Copyright (C) 2019-2025, Lux Industries Inc. All rights reserved.
|
||||
// See the file LICENSE for licensing terms.
|
||||
|
||||
package quasar
|
||||
|
||||
import (
|
||||
"errors"
|
||||
"fmt"
|
||||
"time"
|
||||
)
|
||||
|
||||
// Configuration errors
|
||||
var (
|
||||
ErrInvalidK = errors.New("K must be positive")
|
||||
ErrInvalidAlpha = errors.New("alpha must be in (0, 1]")
|
||||
ErrInvalidBeta = errors.New("beta must be positive and <= K")
|
||||
ErrInvalidThreshold = errors.New("threshold must be >= 2 and <= parties")
|
||||
ErrInvalidQuorum = errors.New("quorum numerator must be <= denominator")
|
||||
ErrInvalidTimeout = errors.New("timeout must be positive")
|
||||
ErrInvalidInterval = errors.New("polling interval must be positive")
|
||||
)
|
||||
|
||||
// -----------------------------------------------------------------------------
|
||||
// Core Consensus Parameters (compile-time, immutable after construction)
|
||||
// -----------------------------------------------------------------------------
|
||||
|
||||
// CoreParams defines the fundamental Lux consensus parameters.
|
||||
// These are protocol-critical and must match across all validators.
|
||||
type CoreParams struct {
|
||||
// K is the sample size for each consensus query round.
|
||||
// Typical values: 20-25 for production networks.
|
||||
K int
|
||||
|
||||
// Alpha is the quorum threshold as a fraction of K.
|
||||
// A response is accepted if >= ceil(K * Alpha) validators agree.
|
||||
// Must be in (0.5, 1] for Byzantine fault tolerance.
|
||||
// Typical value: 0.8 (80% of sample must agree).
|
||||
Alpha float64
|
||||
|
||||
// BetaVirtuous is the number of consecutive successful polls
|
||||
// required to finalize a virtuous (non-conflicting) decision.
|
||||
// Higher values increase latency but improve consistency.
|
||||
// Typical value: 15-20.
|
||||
BetaVirtuous int
|
||||
|
||||
// BetaRogue is the number of consecutive successful polls
|
||||
// required to finalize a rogue (conflicting) decision.
|
||||
// Should be >= BetaVirtuous.
|
||||
// Typical value: 20-25.
|
||||
BetaRogue int
|
||||
}
|
||||
|
||||
// Validate checks CoreParams invariants.
|
||||
func (p CoreParams) Validate() error {
|
||||
if p.K <= 0 {
|
||||
return ErrInvalidK
|
||||
}
|
||||
if p.Alpha <= 0 || p.Alpha > 1 {
|
||||
return ErrInvalidAlpha
|
||||
}
|
||||
if p.BetaVirtuous <= 0 || p.BetaVirtuous > p.K {
|
||||
return ErrInvalidBeta
|
||||
}
|
||||
if p.BetaRogue <= 0 || p.BetaRogue > p.K {
|
||||
return ErrInvalidBeta
|
||||
}
|
||||
if p.BetaRogue < p.BetaVirtuous {
|
||||
return fmt.Errorf("BetaRogue (%d) must be >= BetaVirtuous (%d)", p.BetaRogue, p.BetaVirtuous)
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
// AlphaThreshold returns the minimum agreements needed for quorum.
|
||||
func (p CoreParams) AlphaThreshold() int {
|
||||
return int(float64(p.K)*p.Alpha + 0.999) // ceil
|
||||
}
|
||||
|
||||
// DefaultCoreParams returns production-ready core parameters.
|
||||
func DefaultCoreParams() CoreParams {
|
||||
return CoreParams{
|
||||
K: 20,
|
||||
Alpha: 0.8,
|
||||
BetaVirtuous: 15,
|
||||
BetaRogue: 20,
|
||||
}
|
||||
}
|
||||
|
||||
// -----------------------------------------------------------------------------
|
||||
// Threshold Signing Parameters (compile-time, for Corona/BLS threshold)
|
||||
// -----------------------------------------------------------------------------
|
||||
|
||||
// ThresholdParams defines t-of-n threshold signature configuration.
|
||||
type ThresholdParams struct {
|
||||
// NumParties is the total number of signing parties (validators).
|
||||
// Must be >= 3 for threshold signatures.
|
||||
NumParties int
|
||||
|
||||
// Threshold is the minimum signers required (t in t-of-n).
|
||||
// For BFT: typically 2/3 + 1.
|
||||
// Must be >= 2 and <= NumParties.
|
||||
Threshold int
|
||||
}
|
||||
|
||||
// Validate checks ThresholdParams invariants.
|
||||
func (p ThresholdParams) Validate() error {
|
||||
if p.NumParties < 3 {
|
||||
return fmt.Errorf("%w: need at least 3 parties, got %d", ErrInvalidThreshold, p.NumParties)
|
||||
}
|
||||
if p.Threshold < 2 || p.Threshold > p.NumParties {
|
||||
return fmt.Errorf("%w: threshold=%d, parties=%d", ErrInvalidThreshold, p.Threshold, p.NumParties)
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
// DefaultThresholdParams returns 2/3+1 threshold for n parties.
|
||||
func DefaultThresholdParams(numParties int) ThresholdParams {
|
||||
threshold := (numParties * 2 / 3) + 1
|
||||
if threshold < 2 {
|
||||
threshold = 2
|
||||
}
|
||||
if threshold > numParties {
|
||||
threshold = numParties
|
||||
}
|
||||
return ThresholdParams{
|
||||
NumParties: numParties,
|
||||
Threshold: threshold,
|
||||
}
|
||||
}
|
||||
|
||||
// -----------------------------------------------------------------------------
|
||||
// Quorum Parameters (compile-time, for BLS aggregate weight verification)
|
||||
// -----------------------------------------------------------------------------
|
||||
|
||||
// QuorumParams defines weight-based quorum requirements.
|
||||
type QuorumParams struct {
|
||||
// Numerator and Denominator define the minimum weight fraction.
|
||||
// Quorum is met when SignerWeight/TotalWeight >= Numerator/Denominator.
|
||||
// For BFT: typically 2/3 (Numerator=2, Denominator=3).
|
||||
Numerator uint64
|
||||
Denominator uint64
|
||||
}
|
||||
|
||||
// Validate checks QuorumParams invariants.
|
||||
func (p QuorumParams) Validate() error {
|
||||
if p.Denominator == 0 {
|
||||
return fmt.Errorf("%w: denominator cannot be zero", ErrInvalidQuorum)
|
||||
}
|
||||
if p.Numerator > p.Denominator {
|
||||
return fmt.Errorf("%w: numerator=%d > denominator=%d", ErrInvalidQuorum, p.Numerator, p.Denominator)
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
// RequiredWeight returns minimum weight needed for quorum given totalWeight.
|
||||
func (p QuorumParams) RequiredWeight(totalWeight uint64) uint64 {
|
||||
return totalWeight * p.Numerator / p.Denominator
|
||||
}
|
||||
|
||||
// IsMet returns true if signerWeight meets quorum given totalWeight.
|
||||
func (p QuorumParams) IsMet(signerWeight, totalWeight uint64) bool {
|
||||
return signerWeight >= p.RequiredWeight(totalWeight)
|
||||
}
|
||||
|
||||
// DefaultQuorumParams returns 2/3 quorum (67% of weight required).
|
||||
func DefaultQuorumParams() QuorumParams {
|
||||
return QuorumParams{
|
||||
Numerator: 2,
|
||||
Denominator: 3,
|
||||
}
|
||||
}
|
||||
|
||||
// -----------------------------------------------------------------------------
|
||||
// Runtime Configuration (can be adjusted, but affects liveness not safety)
|
||||
// -----------------------------------------------------------------------------
|
||||
|
||||
// RuntimeConfig holds tunable runtime parameters.
|
||||
// These affect performance and liveness but not consensus safety.
|
||||
type RuntimeConfig struct {
|
||||
// PollInterval is the delay between consensus query rounds.
|
||||
// Lower values decrease latency but increase network load.
|
||||
// Typical value: 100-500ms.
|
||||
PollInterval time.Duration
|
||||
|
||||
// QueryTimeout is the maximum time to wait for query responses.
|
||||
// Must be > PollInterval.
|
||||
// Typical value: 2-5s.
|
||||
QueryTimeout time.Duration
|
||||
|
||||
// FinalityChannelSize is the buffer size for the finality event channel.
|
||||
FinalityChannelSize int
|
||||
|
||||
// MaxConcurrentQueries limits parallel outstanding queries.
|
||||
// 0 means unlimited.
|
||||
MaxConcurrentQueries int
|
||||
}
|
||||
|
||||
// Validate checks RuntimeConfig invariants.
|
||||
func (c RuntimeConfig) Validate() error {
|
||||
if c.PollInterval <= 0 {
|
||||
return ErrInvalidInterval
|
||||
}
|
||||
if c.QueryTimeout <= 0 {
|
||||
return ErrInvalidTimeout
|
||||
}
|
||||
if c.QueryTimeout < c.PollInterval {
|
||||
return fmt.Errorf("query timeout (%v) must be >= poll interval (%v)", c.QueryTimeout, c.PollInterval)
|
||||
}
|
||||
if c.FinalityChannelSize < 0 {
|
||||
return fmt.Errorf("finality channel size must be >= 0")
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
// DefaultRuntimeConfig returns production-ready runtime configuration.
|
||||
func DefaultRuntimeConfig() RuntimeConfig {
|
||||
return RuntimeConfig{
|
||||
PollInterval: 250 * time.Millisecond,
|
||||
QueryTimeout: 2 * time.Second,
|
||||
FinalityChannelSize: 100,
|
||||
MaxConcurrentQueries: 0, // unlimited
|
||||
}
|
||||
}
|
||||
|
||||
// -----------------------------------------------------------------------------
|
||||
// Complete Configuration
|
||||
// -----------------------------------------------------------------------------
|
||||
|
||||
// Config is the complete Quasar consensus configuration.
|
||||
// Use ConfigBuilder for fluent construction.
|
||||
type Config struct {
|
||||
Core CoreParams
|
||||
Threshold ThresholdParams
|
||||
Quorum QuorumParams
|
||||
Runtime RuntimeConfig
|
||||
}
|
||||
|
||||
// Validate checks all configuration invariants.
|
||||
func (c Config) Validate() error {
|
||||
if err := c.Core.Validate(); err != nil {
|
||||
return fmt.Errorf("core params: %w", err)
|
||||
}
|
||||
if err := c.Threshold.Validate(); err != nil {
|
||||
return fmt.Errorf("threshold params: %w", err)
|
||||
}
|
||||
if err := c.Quorum.Validate(); err != nil {
|
||||
return fmt.Errorf("quorum params: %w", err)
|
||||
}
|
||||
if err := c.Runtime.Validate(); err != nil {
|
||||
return fmt.Errorf("runtime config: %w", err)
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
// DefaultConfig returns a production-ready configuration.
|
||||
// Call DefaultConfig().WithNumParties(n) to set validator count.
|
||||
func DefaultConfig() Config {
|
||||
return Config{
|
||||
Core: DefaultCoreParams(),
|
||||
Threshold: DefaultThresholdParams(3), // default 3 validators
|
||||
Quorum: DefaultQuorumParams(),
|
||||
Runtime: DefaultRuntimeConfig(),
|
||||
}
|
||||
}
|
||||
|
||||
// -----------------------------------------------------------------------------
|
||||
// ConfigBuilder provides fluent configuration construction
|
||||
// -----------------------------------------------------------------------------
|
||||
|
||||
// ConfigBuilder enables fluent Config construction with validation.
|
||||
type ConfigBuilder struct {
|
||||
config Config
|
||||
}
|
||||
|
||||
// NewConfigBuilder creates a builder starting from defaults.
|
||||
func NewConfigBuilder() *ConfigBuilder {
|
||||
return &ConfigBuilder{
|
||||
config: DefaultConfig(),
|
||||
}
|
||||
}
|
||||
|
||||
// WithK sets the sample size.
|
||||
func (b *ConfigBuilder) WithK(k int) *ConfigBuilder {
|
||||
b.config.Core.K = k
|
||||
return b
|
||||
}
|
||||
|
||||
// WithAlpha sets the quorum fraction.
|
||||
func (b *ConfigBuilder) WithAlpha(alpha float64) *ConfigBuilder {
|
||||
b.config.Core.Alpha = alpha
|
||||
return b
|
||||
}
|
||||
|
||||
// WithBeta sets both BetaVirtuous and BetaRogue.
|
||||
func (b *ConfigBuilder) WithBeta(virtuous, rogue int) *ConfigBuilder {
|
||||
b.config.Core.BetaVirtuous = virtuous
|
||||
b.config.Core.BetaRogue = rogue
|
||||
return b
|
||||
}
|
||||
|
||||
// WithNumParties sets the validator count and computes 2/3+1 threshold.
|
||||
func (b *ConfigBuilder) WithNumParties(n int) *ConfigBuilder {
|
||||
b.config.Threshold = DefaultThresholdParams(n)
|
||||
return b
|
||||
}
|
||||
|
||||
// WithThreshold sets an explicit threshold (overrides default 2/3+1).
|
||||
func (b *ConfigBuilder) WithThreshold(threshold int) *ConfigBuilder {
|
||||
b.config.Threshold.Threshold = threshold
|
||||
return b
|
||||
}
|
||||
|
||||
// WithQuorum sets the quorum fraction as numerator/denominator.
|
||||
func (b *ConfigBuilder) WithQuorum(num, denom uint64) *ConfigBuilder {
|
||||
b.config.Quorum.Numerator = num
|
||||
b.config.Quorum.Denominator = denom
|
||||
return b
|
||||
}
|
||||
|
||||
// WithPollInterval sets the polling interval.
|
||||
func (b *ConfigBuilder) WithPollInterval(d time.Duration) *ConfigBuilder {
|
||||
b.config.Runtime.PollInterval = d
|
||||
return b
|
||||
}
|
||||
|
||||
// WithQueryTimeout sets the query timeout.
|
||||
func (b *ConfigBuilder) WithQueryTimeout(d time.Duration) *ConfigBuilder {
|
||||
b.config.Runtime.QueryTimeout = d
|
||||
return b
|
||||
}
|
||||
|
||||
// WithFinalityChannelSize sets the finality channel buffer size.
|
||||
func (b *ConfigBuilder) WithFinalityChannelSize(size int) *ConfigBuilder {
|
||||
b.config.Runtime.FinalityChannelSize = size
|
||||
return b
|
||||
}
|
||||
|
||||
// Build validates and returns the configuration.
|
||||
func (b *ConfigBuilder) Build() (Config, error) {
|
||||
if err := b.config.Validate(); err != nil {
|
||||
return Config{}, err
|
||||
}
|
||||
return b.config, nil
|
||||
}
|
||||
|
||||
// MustBuild validates and returns the configuration, panicking on error.
|
||||
// Use only in tests or when configuration is known to be valid.
|
||||
func (b *ConfigBuilder) MustBuild() Config {
|
||||
cfg, err := b.Build()
|
||||
if err != nil {
|
||||
panic(fmt.Sprintf("invalid config: %v", err))
|
||||
}
|
||||
return cfg
|
||||
}
|
||||
@@ -1,434 +0,0 @@
|
||||
// Copyright (C) 2019-2025, Lux Industries Inc. All rights reserved.
|
||||
// See the file LICENSE for licensing terms.
|
||||
|
||||
package quasar
|
||||
|
||||
import (
|
||||
"testing"
|
||||
"time"
|
||||
)
|
||||
|
||||
func TestDefaultConfig(t *testing.T) {
|
||||
cfg := DefaultConfig()
|
||||
if err := cfg.Validate(); err != nil {
|
||||
t.Fatalf("default config should be valid: %v", err)
|
||||
}
|
||||
|
||||
// Verify defaults match documentation
|
||||
if cfg.Core.K != 20 {
|
||||
t.Errorf("expected K=20, got %d", cfg.Core.K)
|
||||
}
|
||||
if cfg.Core.Alpha != 0.8 {
|
||||
t.Errorf("expected Alpha=0.8, got %f", cfg.Core.Alpha)
|
||||
}
|
||||
if cfg.Core.BetaVirtuous != 15 {
|
||||
t.Errorf("expected BetaVirtuous=15, got %d", cfg.Core.BetaVirtuous)
|
||||
}
|
||||
if cfg.Core.BetaRogue != 20 {
|
||||
t.Errorf("expected BetaRogue=20, got %d", cfg.Core.BetaRogue)
|
||||
}
|
||||
if cfg.Quorum.Numerator != 2 || cfg.Quorum.Denominator != 3 {
|
||||
t.Errorf("expected 2/3 quorum, got %d/%d", cfg.Quorum.Numerator, cfg.Quorum.Denominator)
|
||||
}
|
||||
}
|
||||
|
||||
func TestCoreParamsValidation(t *testing.T) {
|
||||
tests := []struct {
|
||||
name string
|
||||
params CoreParams
|
||||
wantErr bool
|
||||
}{
|
||||
{
|
||||
name: "valid defaults",
|
||||
params: DefaultCoreParams(),
|
||||
wantErr: false,
|
||||
},
|
||||
{
|
||||
name: "zero K",
|
||||
params: CoreParams{K: 0, Alpha: 0.8, BetaVirtuous: 15, BetaRogue: 20},
|
||||
wantErr: true,
|
||||
},
|
||||
{
|
||||
name: "negative K",
|
||||
params: CoreParams{K: -1, Alpha: 0.8, BetaVirtuous: 15, BetaRogue: 20},
|
||||
wantErr: true,
|
||||
},
|
||||
{
|
||||
name: "alpha zero",
|
||||
params: CoreParams{K: 20, Alpha: 0, BetaVirtuous: 15, BetaRogue: 20},
|
||||
wantErr: true,
|
||||
},
|
||||
{
|
||||
name: "alpha greater than 1",
|
||||
params: CoreParams{K: 20, Alpha: 1.5, BetaVirtuous: 15, BetaRogue: 20},
|
||||
wantErr: true,
|
||||
},
|
||||
{
|
||||
name: "alpha exactly 1",
|
||||
params: CoreParams{K: 20, Alpha: 1.0, BetaVirtuous: 15, BetaRogue: 20},
|
||||
wantErr: false,
|
||||
},
|
||||
{
|
||||
name: "beta virtuous zero",
|
||||
params: CoreParams{K: 20, Alpha: 0.8, BetaVirtuous: 0, BetaRogue: 20},
|
||||
wantErr: true,
|
||||
},
|
||||
{
|
||||
name: "beta rogue less than virtuous",
|
||||
params: CoreParams{K: 20, Alpha: 0.8, BetaVirtuous: 20, BetaRogue: 15},
|
||||
wantErr: true,
|
||||
},
|
||||
{
|
||||
name: "beta exceeds K",
|
||||
params: CoreParams{K: 10, Alpha: 0.8, BetaVirtuous: 15, BetaRogue: 20},
|
||||
wantErr: true,
|
||||
},
|
||||
}
|
||||
|
||||
for _, tt := range tests {
|
||||
t.Run(tt.name, func(t *testing.T) {
|
||||
err := tt.params.Validate()
|
||||
if (err != nil) != tt.wantErr {
|
||||
t.Errorf("Validate() error = %v, wantErr %v", err, tt.wantErr)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestAlphaThreshold(t *testing.T) {
|
||||
tests := []struct {
|
||||
k int
|
||||
alpha float64
|
||||
want int
|
||||
}{
|
||||
{k: 20, alpha: 0.8, want: 16}, // 20 * 0.8 = 16
|
||||
{k: 20, alpha: 0.51, want: 11}, // ceil(10.2) = 11
|
||||
{k: 10, alpha: 0.67, want: 7}, // ceil(6.7) = 7
|
||||
{k: 5, alpha: 1.0, want: 5}, // 5 * 1.0 = 5
|
||||
}
|
||||
|
||||
for _, tt := range tests {
|
||||
t.Run("", func(t *testing.T) {
|
||||
p := CoreParams{K: tt.k, Alpha: tt.alpha, BetaVirtuous: 1, BetaRogue: 1}
|
||||
got := p.AlphaThreshold()
|
||||
if got != tt.want {
|
||||
t.Errorf("AlphaThreshold(%d, %f) = %d, want %d", tt.k, tt.alpha, got, tt.want)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestThresholdParamsValidation(t *testing.T) {
|
||||
tests := []struct {
|
||||
name string
|
||||
params ThresholdParams
|
||||
wantErr bool
|
||||
}{
|
||||
{
|
||||
name: "valid 3 of 5",
|
||||
params: ThresholdParams{NumParties: 5, Threshold: 3},
|
||||
wantErr: false,
|
||||
},
|
||||
{
|
||||
name: "valid 4 of 5",
|
||||
params: ThresholdParams{NumParties: 5, Threshold: 4},
|
||||
wantErr: false,
|
||||
},
|
||||
{
|
||||
name: "valid 2 of 3 minimum",
|
||||
params: ThresholdParams{NumParties: 3, Threshold: 2},
|
||||
wantErr: false,
|
||||
},
|
||||
{
|
||||
name: "too few parties",
|
||||
params: ThresholdParams{NumParties: 2, Threshold: 2},
|
||||
wantErr: true,
|
||||
},
|
||||
{
|
||||
name: "threshold too low",
|
||||
params: ThresholdParams{NumParties: 5, Threshold: 1},
|
||||
wantErr: true,
|
||||
},
|
||||
{
|
||||
name: "threshold exceeds parties",
|
||||
params: ThresholdParams{NumParties: 5, Threshold: 6},
|
||||
wantErr: true,
|
||||
},
|
||||
}
|
||||
|
||||
for _, tt := range tests {
|
||||
t.Run(tt.name, func(t *testing.T) {
|
||||
err := tt.params.Validate()
|
||||
if (err != nil) != tt.wantErr {
|
||||
t.Errorf("Validate() error = %v, wantErr %v", err, tt.wantErr)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestDefaultThresholdParams(t *testing.T) {
|
||||
tests := []struct {
|
||||
parties int
|
||||
wantThreshold int
|
||||
}{
|
||||
{parties: 3, wantThreshold: 3}, // 2/3 of 3 = 2, +1 = 3
|
||||
{parties: 4, wantThreshold: 3}, // 2/3 of 4 = 2, +1 = 3
|
||||
{parties: 5, wantThreshold: 4}, // 2/3 of 5 = 3, +1 = 4
|
||||
{parties: 10, wantThreshold: 7}, // 2/3 of 10 = 6, +1 = 7
|
||||
{parties: 21, wantThreshold: 15}, // 2/3 of 21 = 14, +1 = 15
|
||||
}
|
||||
|
||||
for _, tt := range tests {
|
||||
t.Run("", func(t *testing.T) {
|
||||
p := DefaultThresholdParams(tt.parties)
|
||||
if p.Threshold != tt.wantThreshold {
|
||||
t.Errorf("DefaultThresholdParams(%d).Threshold = %d, want %d",
|
||||
tt.parties, p.Threshold, tt.wantThreshold)
|
||||
}
|
||||
if err := p.Validate(); err != nil {
|
||||
t.Errorf("DefaultThresholdParams(%d) produced invalid params: %v", tt.parties, err)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestQuorumParamsValidation(t *testing.T) {
|
||||
tests := []struct {
|
||||
name string
|
||||
params QuorumParams
|
||||
wantErr bool
|
||||
}{
|
||||
{
|
||||
name: "valid 2/3",
|
||||
params: QuorumParams{Numerator: 2, Denominator: 3},
|
||||
wantErr: false,
|
||||
},
|
||||
{
|
||||
name: "valid 1/2",
|
||||
params: QuorumParams{Numerator: 1, Denominator: 2},
|
||||
wantErr: false,
|
||||
},
|
||||
{
|
||||
name: "valid 1/1 (unanimous)",
|
||||
params: QuorumParams{Numerator: 1, Denominator: 1},
|
||||
wantErr: false,
|
||||
},
|
||||
{
|
||||
name: "zero denominator",
|
||||
params: QuorumParams{Numerator: 2, Denominator: 0},
|
||||
wantErr: true,
|
||||
},
|
||||
{
|
||||
name: "numerator exceeds denominator",
|
||||
params: QuorumParams{Numerator: 4, Denominator: 3},
|
||||
wantErr: true,
|
||||
},
|
||||
}
|
||||
|
||||
for _, tt := range tests {
|
||||
t.Run(tt.name, func(t *testing.T) {
|
||||
err := tt.params.Validate()
|
||||
if (err != nil) != tt.wantErr {
|
||||
t.Errorf("Validate() error = %v, wantErr %v", err, tt.wantErr)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestQuorumIsMet(t *testing.T) {
|
||||
q := QuorumParams{Numerator: 2, Denominator: 3} // 2/3 = 66.67%
|
||||
|
||||
// Note: integer math: 100 * 2 / 3 = 66 (floor division)
|
||||
tests := []struct {
|
||||
signerWeight uint64
|
||||
totalWeight uint64
|
||||
want bool
|
||||
}{
|
||||
{signerWeight: 67, totalWeight: 100, want: true}, // 67 >= 66
|
||||
{signerWeight: 66, totalWeight: 100, want: true}, // 66 >= 66 (floor division)
|
||||
{signerWeight: 65, totalWeight: 100, want: false}, // 65 < 66
|
||||
{signerWeight: 100, totalWeight: 100, want: true}, // 100 >= 66
|
||||
{signerWeight: 0, totalWeight: 100, want: false}, // 0 < 66
|
||||
{signerWeight: 2, totalWeight: 3, want: true}, // 2 >= 2 (3*2/3=2)
|
||||
{signerWeight: 1, totalWeight: 3, want: false}, // 1 < 2
|
||||
}
|
||||
|
||||
for _, tt := range tests {
|
||||
got := q.IsMet(tt.signerWeight, tt.totalWeight)
|
||||
if got != tt.want {
|
||||
t.Errorf("IsMet(%d, %d) = %v, want %v", tt.signerWeight, tt.totalWeight, got, tt.want)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func TestRuntimeConfigValidation(t *testing.T) {
|
||||
tests := []struct {
|
||||
name string
|
||||
config RuntimeConfig
|
||||
wantErr bool
|
||||
}{
|
||||
{
|
||||
name: "valid defaults",
|
||||
config: DefaultRuntimeConfig(),
|
||||
wantErr: false,
|
||||
},
|
||||
{
|
||||
name: "zero poll interval",
|
||||
config: RuntimeConfig{
|
||||
PollInterval: 0,
|
||||
QueryTimeout: time.Second,
|
||||
},
|
||||
wantErr: true,
|
||||
},
|
||||
{
|
||||
name: "negative poll interval",
|
||||
config: RuntimeConfig{
|
||||
PollInterval: -time.Millisecond,
|
||||
QueryTimeout: time.Second,
|
||||
},
|
||||
wantErr: true,
|
||||
},
|
||||
{
|
||||
name: "query timeout less than poll interval",
|
||||
config: RuntimeConfig{
|
||||
PollInterval: time.Second,
|
||||
QueryTimeout: 100 * time.Millisecond,
|
||||
},
|
||||
wantErr: true,
|
||||
},
|
||||
{
|
||||
name: "negative channel size",
|
||||
config: RuntimeConfig{
|
||||
PollInterval: 250 * time.Millisecond,
|
||||
QueryTimeout: 2 * time.Second,
|
||||
FinalityChannelSize: -1,
|
||||
},
|
||||
wantErr: true,
|
||||
},
|
||||
}
|
||||
|
||||
for _, tt := range tests {
|
||||
t.Run(tt.name, func(t *testing.T) {
|
||||
err := tt.config.Validate()
|
||||
if (err != nil) != tt.wantErr {
|
||||
t.Errorf("Validate() error = %v, wantErr %v", err, tt.wantErr)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestConfigBuilder(t *testing.T) {
|
||||
// Test fluent API
|
||||
cfg, err := NewConfigBuilder().
|
||||
WithK(25).
|
||||
WithAlpha(0.75).
|
||||
WithBeta(18, 22).
|
||||
WithNumParties(10).
|
||||
WithQuorum(3, 4). // 75%
|
||||
WithPollInterval(500 * time.Millisecond).
|
||||
WithQueryTimeout(5 * time.Second).
|
||||
Build()
|
||||
|
||||
if err != nil {
|
||||
t.Fatalf("Build() error = %v", err)
|
||||
}
|
||||
|
||||
if cfg.Core.K != 25 {
|
||||
t.Errorf("K = %d, want 25", cfg.Core.K)
|
||||
}
|
||||
if cfg.Core.Alpha != 0.75 {
|
||||
t.Errorf("Alpha = %f, want 0.75", cfg.Core.Alpha)
|
||||
}
|
||||
if cfg.Core.BetaVirtuous != 18 {
|
||||
t.Errorf("BetaVirtuous = %d, want 18", cfg.Core.BetaVirtuous)
|
||||
}
|
||||
if cfg.Core.BetaRogue != 22 {
|
||||
t.Errorf("BetaRogue = %d, want 22", cfg.Core.BetaRogue)
|
||||
}
|
||||
if cfg.Threshold.NumParties != 10 {
|
||||
t.Errorf("NumParties = %d, want 10", cfg.Threshold.NumParties)
|
||||
}
|
||||
if cfg.Threshold.Threshold != 7 { // 2/3 of 10 + 1 = 7
|
||||
t.Errorf("Threshold = %d, want 7", cfg.Threshold.Threshold)
|
||||
}
|
||||
if cfg.Quorum.Numerator != 3 || cfg.Quorum.Denominator != 4 {
|
||||
t.Errorf("Quorum = %d/%d, want 3/4", cfg.Quorum.Numerator, cfg.Quorum.Denominator)
|
||||
}
|
||||
if cfg.Runtime.PollInterval != 500*time.Millisecond {
|
||||
t.Errorf("PollInterval = %v, want 500ms", cfg.Runtime.PollInterval)
|
||||
}
|
||||
}
|
||||
|
||||
func TestConfigBuilderWithExplicitThreshold(t *testing.T) {
|
||||
cfg, err := NewConfigBuilder().
|
||||
WithNumParties(10).
|
||||
WithThreshold(5). // Override default 7
|
||||
Build()
|
||||
|
||||
if err != nil {
|
||||
t.Fatalf("Build() error = %v", err)
|
||||
}
|
||||
|
||||
if cfg.Threshold.Threshold != 5 {
|
||||
t.Errorf("Threshold = %d, want 5", cfg.Threshold.Threshold)
|
||||
}
|
||||
}
|
||||
|
||||
func TestConfigBuilderValidationError(t *testing.T) {
|
||||
_, err := NewConfigBuilder().
|
||||
WithK(-1). // Invalid
|
||||
Build()
|
||||
|
||||
if err == nil {
|
||||
t.Error("Build() should return error for invalid K")
|
||||
}
|
||||
}
|
||||
|
||||
func TestConfigBuilderMustBuildPanics(t *testing.T) {
|
||||
defer func() {
|
||||
if r := recover(); r == nil {
|
||||
t.Error("MustBuild() should panic on invalid config")
|
||||
}
|
||||
}()
|
||||
|
||||
NewConfigBuilder().WithK(-1).MustBuild()
|
||||
}
|
||||
|
||||
func TestConfigBuilderMustBuildSuccess(t *testing.T) {
|
||||
// Should not panic
|
||||
cfg := NewConfigBuilder().MustBuild()
|
||||
if err := cfg.Validate(); err != nil {
|
||||
t.Errorf("MustBuild() produced invalid config: %v", err)
|
||||
}
|
||||
}
|
||||
|
||||
// TestConfigImmutability documents that Config values are immutable after creation.
|
||||
func TestConfigImmutability(t *testing.T) {
|
||||
cfg := DefaultConfig()
|
||||
|
||||
// These are value types, so modifications don't affect the original
|
||||
core := cfg.Core
|
||||
core.K = 999
|
||||
|
||||
if cfg.Core.K == 999 {
|
||||
t.Error("Config.Core should be immutable (value copy)")
|
||||
}
|
||||
}
|
||||
|
||||
// BenchmarkQuorumCheck benchmarks the quorum check operation.
|
||||
func BenchmarkQuorumCheck(b *testing.B) {
|
||||
q := DefaultQuorumParams()
|
||||
b.ResetTimer()
|
||||
for i := 0; i < b.N; i++ {
|
||||
_ = q.IsMet(70, 100)
|
||||
}
|
||||
}
|
||||
|
||||
// BenchmarkConfigValidation benchmarks config validation.
|
||||
func BenchmarkConfigValidation(b *testing.B) {
|
||||
cfg := DefaultConfig()
|
||||
b.ResetTimer()
|
||||
for i := 0; i < b.N; i++ {
|
||||
_ = cfg.Validate()
|
||||
}
|
||||
}
|
||||
@@ -1,306 +0,0 @@
|
||||
// Copyright (C) 2019-2025, Lux Industries Inc. All rights reserved.
|
||||
// See the file LICENSE for licensing terms.
|
||||
|
||||
package quasar
|
||||
|
||||
import (
|
||||
"testing"
|
||||
"time"
|
||||
|
||||
"github.com/luxfi/ids"
|
||||
"github.com/luxfi/log"
|
||||
)
|
||||
|
||||
// --- Config edge cases ---
|
||||
|
||||
func TestDefaultThresholdParamsSmall(t *testing.T) {
|
||||
// numParties=1: threshold clamped to numParties
|
||||
p := DefaultThresholdParams(1)
|
||||
if p.Threshold != 1 {
|
||||
t.Errorf("expected threshold 1 for 1 party, got %d", p.Threshold)
|
||||
}
|
||||
|
||||
// numParties=2: 2/3*2 + 1 = 2, min is 2
|
||||
p = DefaultThresholdParams(2)
|
||||
if p.Threshold != 2 {
|
||||
t.Errorf("expected threshold 2, got %d", p.Threshold)
|
||||
}
|
||||
|
||||
// numParties=3: 2/3*3 + 1 = 3
|
||||
p = DefaultThresholdParams(3)
|
||||
if p.Threshold != 3 {
|
||||
t.Errorf("expected threshold 3, got %d", p.Threshold)
|
||||
}
|
||||
}
|
||||
|
||||
func TestQuorumParamsValidate(t *testing.T) {
|
||||
p := DefaultQuorumParams()
|
||||
if err := p.Validate(); err != nil {
|
||||
t.Errorf("default quorum should be valid: %v", err)
|
||||
}
|
||||
|
||||
p = QuorumParams{Numerator: 1, Denominator: 0}
|
||||
if err := p.Validate(); err == nil {
|
||||
t.Error("zero denominator should be invalid")
|
||||
}
|
||||
|
||||
p = QuorumParams{Numerator: 4, Denominator: 3}
|
||||
if err := p.Validate(); err == nil {
|
||||
t.Error("num > denom should be invalid")
|
||||
}
|
||||
}
|
||||
|
||||
func TestQuorumParamsIsMet(t *testing.T) {
|
||||
p := QuorumParams{Numerator: 2, Denominator: 3}
|
||||
if !p.IsMet(200, 300) {
|
||||
t.Error("200/300 should meet 2/3 quorum")
|
||||
}
|
||||
if p.IsMet(199, 300) {
|
||||
t.Error("199/300 should not meet 2/3 quorum")
|
||||
}
|
||||
}
|
||||
|
||||
func TestQuorumParamsRequiredWeight(t *testing.T) {
|
||||
p := QuorumParams{Numerator: 2, Denominator: 3}
|
||||
if p.RequiredWeight(300) != 200 {
|
||||
t.Errorf("expected 200, got %d", p.RequiredWeight(300))
|
||||
}
|
||||
}
|
||||
|
||||
func TestConfigBuilderWithFinalityChannelSize(t *testing.T) {
|
||||
cfg, err := NewConfigBuilder().
|
||||
WithThreshold(3).
|
||||
WithFinalityChannelSize(256).
|
||||
Build()
|
||||
if err != nil {
|
||||
t.Fatalf("build failed: %v", err)
|
||||
}
|
||||
if cfg.Runtime.FinalityChannelSize != 256 {
|
||||
t.Errorf("expected 256, got %d", cfg.Runtime.FinalityChannelSize)
|
||||
}
|
||||
}
|
||||
|
||||
func TestThresholdParamsValidateEdge(t *testing.T) {
|
||||
p := ThresholdParams{NumParties: 3, Threshold: 5}
|
||||
if err := p.Validate(); err == nil {
|
||||
t.Error("threshold > parties should be invalid")
|
||||
}
|
||||
|
||||
p = ThresholdParams{NumParties: 3, Threshold: 0}
|
||||
if err := p.Validate(); err == nil {
|
||||
t.Error("threshold 0 should be invalid")
|
||||
}
|
||||
}
|
||||
|
||||
// --- Quasar lifecycle ---
|
||||
|
||||
func TestQuasarGetCoreGetCorona(t *testing.T) {
|
||||
q, err := NewQuasar(log.Noop(), 2, 2, 3)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
|
||||
if q.GetCore() == nil {
|
||||
t.Error("GetCore should not return nil")
|
||||
}
|
||||
if q.GetCorona() != nil {
|
||||
t.Error("Corona should be nil before initialization")
|
||||
}
|
||||
}
|
||||
|
||||
func TestQuasarSetGetFinalized(t *testing.T) {
|
||||
q, err := NewQuasar(log.Noop(), 2, 2, 3)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
|
||||
blockID := ids.GenerateTestID()
|
||||
finality := &QuantumFinality{
|
||||
BlockID: blockID,
|
||||
PChainHeight: 42,
|
||||
Timestamp: time.Now(),
|
||||
}
|
||||
|
||||
q.SetFinalized(blockID, finality)
|
||||
|
||||
got, ok := q.GetFinalized(blockID)
|
||||
if !ok {
|
||||
t.Fatal("should find finality record")
|
||||
}
|
||||
if got.PChainHeight != 42 {
|
||||
t.Errorf("height mismatch: %d", got.PChainHeight)
|
||||
}
|
||||
|
||||
_, ok = q.GetFinalized(ids.GenerateTestID())
|
||||
if ok {
|
||||
t.Error("should not find non-existent finality")
|
||||
}
|
||||
}
|
||||
|
||||
func TestQuasarGetConfig(t *testing.T) {
|
||||
q, err := NewQuasar(log.Noop(), 3, 2, 3)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
|
||||
threshold, qNum, qDen := q.GetConfig()
|
||||
if threshold != 3 {
|
||||
t.Errorf("expected threshold 3, got %d", threshold)
|
||||
}
|
||||
if qNum != 2 || qDen != 3 {
|
||||
t.Errorf("expected quorum 2/3, got %d/%d", qNum, qDen)
|
||||
}
|
||||
}
|
||||
|
||||
func TestQuasarIsRunning(t *testing.T) {
|
||||
q, err := NewQuasar(log.Noop(), 2, 2, 3)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
if q.IsRunning() {
|
||||
t.Error("should not be running before Start")
|
||||
}
|
||||
}
|
||||
|
||||
func TestQuasarCheckQuorum(t *testing.T) {
|
||||
q, err := NewQuasar(log.Noop(), 2, 2, 3)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
|
||||
if !q.CheckQuorum(200, 300) {
|
||||
t.Error("200/300 should meet 2/3 quorum")
|
||||
}
|
||||
if q.CheckQuorum(100, 300) {
|
||||
t.Error("100/300 should not meet 2/3 quorum")
|
||||
}
|
||||
}
|
||||
|
||||
func TestQuasarCreateMessage(t *testing.T) {
|
||||
q, err := NewQuasar(log.Noop(), 2, 2, 3)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
|
||||
event := FinalityEvent{
|
||||
BlockID: ids.GenerateTestID(),
|
||||
Height: 100,
|
||||
}
|
||||
|
||||
msg := q.CreateMessage(event)
|
||||
if len(msg) == 0 {
|
||||
t.Error("message should not be empty")
|
||||
}
|
||||
}
|
||||
|
||||
func TestQuasarTotalWeight(t *testing.T) {
|
||||
q, err := NewQuasar(log.Noop(), 2, 2, 3)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
|
||||
validators := []ValidatorState{
|
||||
{Weight: 100, Active: true},
|
||||
{Weight: 200, Active: true},
|
||||
{Weight: 50, Active: true},
|
||||
}
|
||||
|
||||
total := q.TotalWeight(validators)
|
||||
if total != 350 {
|
||||
t.Errorf("expected 350, got %d", total)
|
||||
}
|
||||
|
||||
// Inactive validators should not count
|
||||
validators[2].Active = false
|
||||
total = q.TotalWeight(validators)
|
||||
if total != 300 {
|
||||
t.Errorf("expected 300 without inactive, got %d", total)
|
||||
}
|
||||
}
|
||||
|
||||
// --- BLS Signature ---
|
||||
|
||||
func TestBLSSignature(t *testing.T) {
|
||||
signers := []ids.NodeID{ids.GenerateTestNodeID(), ids.GenerateTestNodeID()}
|
||||
sig := NewBLSSignature([]byte("aggregated-sig"), signers)
|
||||
|
||||
if sig.Type() != SignatureTypeBLS {
|
||||
t.Error("wrong type")
|
||||
}
|
||||
if len(sig.Bytes()) == 0 {
|
||||
t.Error("bytes should not be empty")
|
||||
}
|
||||
if len(sig.Signers()) != 2 {
|
||||
t.Errorf("expected 2 signers, got %d", len(sig.Signers()))
|
||||
}
|
||||
}
|
||||
|
||||
// --- CoronaCoordinator ---
|
||||
|
||||
func TestCoronaCoordinatorSignNotInitialized(t *testing.T) {
|
||||
rc, _ := NewCoronaCoordinator(log.Noop(), CoronaConfig{NumParties: 3, Threshold: 2})
|
||||
_, err := rc.Sign([]byte("msg"))
|
||||
if err == nil {
|
||||
t.Error("should fail when not initialized")
|
||||
}
|
||||
}
|
||||
|
||||
func TestCoronaCoordinatorVerifyNotInitialized(t *testing.T) {
|
||||
rc, _ := NewCoronaCoordinator(log.Noop(), CoronaConfig{NumParties: 3, Threshold: 2})
|
||||
if rc.Verify([]byte("msg"), nil) {
|
||||
t.Error("should return false when not initialized")
|
||||
}
|
||||
}
|
||||
|
||||
func TestCoronaCoordinatorTestMode(t *testing.T) {
|
||||
rc, _ := NewTestCoronaCoordinator(log.Noop(), CoronaConfig{NumParties: 3, Threshold: 2})
|
||||
validators := []ids.NodeID{ids.GenerateTestNodeID()}
|
||||
rc.Initialize(validators)
|
||||
|
||||
sig, err := rc.Sign([]byte("test-message"))
|
||||
if err != nil {
|
||||
t.Fatalf("sign failed: %v", err)
|
||||
}
|
||||
if sig == nil {
|
||||
t.Fatal("signature should not be nil")
|
||||
}
|
||||
if !rc.Verify([]byte("test-message"), sig) {
|
||||
t.Error("should verify in testing mode")
|
||||
}
|
||||
}
|
||||
|
||||
func TestCoronaCoordinatorNotTestMode(t *testing.T) {
|
||||
rc, _ := NewCoronaCoordinator(log.Noop(), CoronaConfig{NumParties: 3, Threshold: 2})
|
||||
rc.Initialize([]ids.NodeID{ids.GenerateTestNodeID()})
|
||||
|
||||
_, err := rc.Sign([]byte("msg"))
|
||||
if err == nil {
|
||||
t.Error("should fail when not in testing mode")
|
||||
}
|
||||
|
||||
sig := NewCoronaSignature([]byte("fake"), nil)
|
||||
if rc.Verify([]byte("msg"), sig) {
|
||||
t.Error("should return false when not in testing mode")
|
||||
}
|
||||
}
|
||||
|
||||
func TestCoronaCoordinatorStats(t *testing.T) {
|
||||
rc, _ := NewCoronaCoordinator(log.Noop(), CoronaConfig{NumParties: 3, Threshold: 2})
|
||||
s := rc.Stats()
|
||||
if s.NumParties != 3 {
|
||||
t.Errorf("expected 3 parties, got %d", s.NumParties)
|
||||
}
|
||||
if s.Initialized {
|
||||
t.Error("should not be initialized")
|
||||
}
|
||||
}
|
||||
|
||||
func TestCoronaCoordinatorThresholdNumParties(t *testing.T) {
|
||||
rc, _ := NewCoronaCoordinator(log.Noop(), CoronaConfig{NumParties: 5, Threshold: 3})
|
||||
if rc.Threshold() != 3 {
|
||||
t.Errorf("expected threshold 3, got %d", rc.Threshold())
|
||||
}
|
||||
if rc.NumParties() != 5 {
|
||||
t.Errorf("expected 5 parties, got %d", rc.NumParties())
|
||||
}
|
||||
}
|
||||
@@ -1,59 +0,0 @@
|
||||
// Copyright (C) 2019-2025, Lux Industries Inc. All rights reserved.
|
||||
// See the file LICENSE for licensing terms.
|
||||
|
||||
/*
|
||||
Package quasar provides hybrid quantum-safe consensus finality.
|
||||
|
||||
# Overview
|
||||
|
||||
Quasar is the gravitational center of Lux consensus, binding P-Chain
|
||||
(BLS signatures) and Q-Chain (Corona post-quantum threshold) into
|
||||
unified hybrid finality across all Lux networks.
|
||||
|
||||
# Architecture
|
||||
|
||||
All validators maintain both keypairs:
|
||||
- BLS keypair: Aggregate signatures (classical, fast)
|
||||
- Corona keypair: Threshold signatures (post-quantum, 2-round)
|
||||
|
||||
Both signature paths run in parallel:
|
||||
|
||||
Block arrives
|
||||
|
|
||||
+-- BLS PATH ----------+-- CORONA PATH --------+
|
||||
| All validators | Round 1: commitments |
|
||||
| sign with BLS | Round 2: partials |
|
||||
| Aggregate (96B) | Combine threshold sig |
|
||||
+----------------------+-------------------------+
|
||||
|
|
||||
HYBRID PROOF
|
||||
BLS + Corona combined
|
||||
|
|
||||
QUANTUM FINALITY
|
||||
|
||||
# Vote Flow
|
||||
|
||||
Validators cast votes (wire format: Chits) for proposed blocks. The
|
||||
Quasar engine collects these votes and produces finality proofs when:
|
||||
- 2/3+ validator weight signed via BLS
|
||||
- t-of-n validators completed Corona threshold signing
|
||||
|
||||
# Signature Types
|
||||
|
||||
The package defines several signature types:
|
||||
- SignatureTypeBLS: Classical BLS signatures
|
||||
- SignatureTypeCorona: Post-quantum threshold
|
||||
- SignatureTypeQuasar: Hybrid combining both
|
||||
- SignatureTypeMLDSA: ML-DSA fallback
|
||||
|
||||
# Components
|
||||
|
||||
Quasar: Main consensus hub coordinating both signature paths.
|
||||
|
||||
CoronaCoordinator: Manages the 2-round threshold signing protocol
|
||||
for post-quantum security.
|
||||
|
||||
QuantumFinality: Represents a block that achieved hybrid finality with
|
||||
both BLS and Corona proofs.
|
||||
*/
|
||||
package quasar
|
||||
@@ -1,627 +0,0 @@
|
||||
// Copyright (C) 2019-2025, Lux Industries Inc. All rights reserved.
|
||||
// See the file LICENSE for licensing terms.
|
||||
|
||||
package quasar
|
||||
|
||||
import (
|
||||
"errors"
|
||||
"fmt"
|
||||
"sync"
|
||||
"sync/atomic"
|
||||
"time"
|
||||
|
||||
"github.com/luxfi/accel"
|
||||
"github.com/luxfi/crypto/bls"
|
||||
"github.com/luxfi/crypto/mldsa"
|
||||
)
|
||||
|
||||
// GPUVerifyPipeline fuses multiple cryptographic verification operations into
|
||||
// a single GPU session, sharing GPU memory across all verification types.
|
||||
//
|
||||
// Instead of sequential: BLS verify -> Corona verify -> ZK verify -> ML-DSA verify
|
||||
// GPU pipeline: one session, parallel streams, shared memory allocation
|
||||
//
|
||||
// This is the ML-DSA rollup pattern:
|
||||
// - BLS aggregate signature (classical fast path)
|
||||
// - Corona threshold signature (PQ safe path)
|
||||
// - ZK rollup batch proof (state transition validity)
|
||||
// - N x ML-DSA signatures (per-tx PQ signatures)
|
||||
//
|
||||
// All execute on GPU in parallel using separate compute streams within one session.
|
||||
type GPUVerifyPipeline struct {
|
||||
// Stats (atomic for lock-free reads)
|
||||
gpuVerifies uint64
|
||||
cpuVerifies uint64
|
||||
gpuTimeNs uint64
|
||||
cpuTimeNs uint64
|
||||
}
|
||||
|
||||
// NewGPUVerifyPipeline creates a new fused GPU verification pipeline.
|
||||
func NewGPUVerifyPipeline() *GPUVerifyPipeline {
|
||||
return &GPUVerifyPipeline{}
|
||||
}
|
||||
|
||||
// BLSWork holds a batch of BLS signatures to verify.
|
||||
type BLSWork struct {
|
||||
Messages [][]byte // [N, msg_len]
|
||||
Signatures [][]byte // [N, 96] G2 points
|
||||
PubKeys [][]byte // [N, 48] G1 points
|
||||
}
|
||||
|
||||
// CoronaWork holds a batch of Corona threshold signatures to verify.
|
||||
type CoronaWork struct {
|
||||
Messages [][]byte // [N, msg_len]
|
||||
Signatures [][]byte // [N, sig_len] threshold sigs
|
||||
PubKeys [][]byte // [N, pk_len] ring public keys
|
||||
}
|
||||
|
||||
// ZKWork holds a batch of ZK proofs to verify.
|
||||
type ZKWork struct {
|
||||
Scalars [][]byte // [M, N, scalar_size]
|
||||
Bases [][]byte // [M, N, point_size]
|
||||
}
|
||||
|
||||
// MLDSAWork holds a batch of ML-DSA signatures to verify.
|
||||
type MLDSAWork struct {
|
||||
Messages [][]byte // [N, msg_len]
|
||||
Signatures [][]byte // [N, 3309] FIPS-204 ML-DSA-65 (3293 was the stale round-3 Dilithium3 size)
|
||||
PubKeys [][]byte // [N, 1952] FIPS-204 ML-DSA-65
|
||||
}
|
||||
|
||||
// BlockVerifyWork contains all verification batches for a single block.
|
||||
type BlockVerifyWork struct {
|
||||
BLS *BLSWork
|
||||
Corona *CoronaWork
|
||||
ZK *ZKWork
|
||||
MLDSA *MLDSAWork
|
||||
}
|
||||
|
||||
// BlockVerifyResult contains verification results for all batch types.
|
||||
type BlockVerifyResult struct {
|
||||
BLSValid []bool
|
||||
CoronaValid []bool
|
||||
ZKValid bool
|
||||
MLDSAValid []bool
|
||||
|
||||
GPUUsed bool
|
||||
BLSTime time.Duration
|
||||
CoronaTime time.Duration
|
||||
ZKTime time.Duration
|
||||
MLDSATime time.Duration
|
||||
TotalTime time.Duration
|
||||
}
|
||||
|
||||
var (
|
||||
ErrBLSSizeMismatch = errors.New("BLS batch size mismatch: messages, signatures, and pubkeys must have equal length")
|
||||
ErrCoronaSizeMismatch = errors.New("Corona batch size mismatch: messages, signatures, and pubkeys must have equal length")
|
||||
ErrZKSizeMismatch = errors.New("ZK batch size mismatch: scalars and bases must have equal length")
|
||||
ErrMLDSASizeMismatch = errors.New("ML-DSA batch size mismatch: messages, signatures, and pubkeys must have equal length")
|
||||
)
|
||||
|
||||
// VerifyBlock dispatches all verification work for a block through the GPU pipeline.
|
||||
// Falls back to CPU verification when no GPU is available.
|
||||
func (p *GPUVerifyPipeline) VerifyBlock(work *BlockVerifyWork) (*BlockVerifyResult, error) {
|
||||
if work == nil {
|
||||
return &BlockVerifyResult{}, nil
|
||||
}
|
||||
|
||||
if err := validateWork(work); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
start := time.Now()
|
||||
|
||||
if accel.Available() {
|
||||
result, err := p.verifyGPU(work)
|
||||
if err == nil {
|
||||
result.TotalTime = time.Since(start)
|
||||
result.GPUUsed = true
|
||||
atomic.AddUint64(&p.gpuVerifies, 1)
|
||||
atomic.AddUint64(&p.gpuTimeNs, uint64(result.TotalTime))
|
||||
return result, nil
|
||||
}
|
||||
// GPU failed, fall through to CPU
|
||||
}
|
||||
|
||||
result := p.verifyCPU(work)
|
||||
result.TotalTime = time.Since(start)
|
||||
result.GPUUsed = false
|
||||
atomic.AddUint64(&p.cpuVerifies, 1)
|
||||
atomic.AddUint64(&p.cpuTimeNs, uint64(result.TotalTime))
|
||||
return result, nil
|
||||
}
|
||||
|
||||
// verifyGPU dispatches all 4 verification types through a single GPU session.
|
||||
func (p *GPUVerifyPipeline) verifyGPU(work *BlockVerifyWork) (*BlockVerifyResult, error) {
|
||||
sess, err := accel.NewSession()
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("GPU session: %w", err)
|
||||
}
|
||||
defer sess.Close()
|
||||
|
||||
result := &BlockVerifyResult{}
|
||||
var mu sync.Mutex
|
||||
var wg sync.WaitGroup
|
||||
var firstErr atomic.Value
|
||||
|
||||
// BLS verification stream
|
||||
if work.BLS != nil && len(work.BLS.Messages) > 0 {
|
||||
wg.Add(1)
|
||||
go func() {
|
||||
defer wg.Done()
|
||||
start := time.Now()
|
||||
valid, err := gpuBLSVerify(sess, work.BLS)
|
||||
elapsed := time.Since(start)
|
||||
if err != nil {
|
||||
firstErr.CompareAndSwap(nil, err)
|
||||
return
|
||||
}
|
||||
mu.Lock()
|
||||
result.BLSValid = valid
|
||||
result.BLSTime = elapsed
|
||||
mu.Unlock()
|
||||
}()
|
||||
}
|
||||
|
||||
// Corona verification stream (uses DilithiumVerifyBatch on lattice ops)
|
||||
if work.Corona != nil && len(work.Corona.Messages) > 0 {
|
||||
wg.Add(1)
|
||||
go func() {
|
||||
defer wg.Done()
|
||||
start := time.Now()
|
||||
valid, err := gpuCoronaVerify(sess, work.Corona)
|
||||
elapsed := time.Since(start)
|
||||
if err != nil {
|
||||
firstErr.CompareAndSwap(nil, err)
|
||||
return
|
||||
}
|
||||
mu.Lock()
|
||||
result.CoronaValid = valid
|
||||
result.CoronaTime = elapsed
|
||||
mu.Unlock()
|
||||
}()
|
||||
}
|
||||
|
||||
// ZK rollup batch proof verification stream
|
||||
if work.ZK != nil && len(work.ZK.Scalars) > 0 {
|
||||
wg.Add(1)
|
||||
go func() {
|
||||
defer wg.Done()
|
||||
start := time.Now()
|
||||
valid, err := gpuZKVerify(sess, work.ZK)
|
||||
elapsed := time.Since(start)
|
||||
if err != nil {
|
||||
firstErr.CompareAndSwap(nil, err)
|
||||
return
|
||||
}
|
||||
mu.Lock()
|
||||
result.ZKValid = valid
|
||||
result.ZKTime = elapsed
|
||||
mu.Unlock()
|
||||
}()
|
||||
}
|
||||
|
||||
// ML-DSA per-tx signature verification stream
|
||||
if work.MLDSA != nil && len(work.MLDSA.Messages) > 0 {
|
||||
wg.Add(1)
|
||||
go func() {
|
||||
defer wg.Done()
|
||||
start := time.Now()
|
||||
valid, err := gpuMLDSAVerify(sess, work.MLDSA)
|
||||
elapsed := time.Since(start)
|
||||
if err != nil {
|
||||
firstErr.CompareAndSwap(nil, err)
|
||||
return
|
||||
}
|
||||
mu.Lock()
|
||||
result.MLDSAValid = valid
|
||||
result.MLDSATime = elapsed
|
||||
mu.Unlock()
|
||||
}()
|
||||
}
|
||||
|
||||
wg.Wait()
|
||||
|
||||
if v := firstErr.Load(); v != nil {
|
||||
return nil, v.(error)
|
||||
}
|
||||
|
||||
return result, nil
|
||||
}
|
||||
|
||||
// gpuBLSVerify dispatches BLS batch verification to the GPU crypto ops.
|
||||
func gpuBLSVerify(sess *accel.Session, work *BLSWork) ([]bool, error) {
|
||||
n := len(work.Messages)
|
||||
|
||||
// Determine uniform sizes for tensor packing
|
||||
msgLen := maxByteLen(work.Messages)
|
||||
sigLen := 96 // BLS G2 point
|
||||
pkLen := 48 // BLS G1 point
|
||||
|
||||
msgFlat := flattenPadded(work.Messages, n, msgLen)
|
||||
sigFlat := flattenPadded(work.Signatures, n, sigLen)
|
||||
pkFlat := flattenPadded(work.PubKeys, n, pkLen)
|
||||
|
||||
msgs, err := accel.NewTensorWithData[uint8](sess, []int{n, msgLen}, msgFlat)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
defer msgs.Close()
|
||||
|
||||
sigs, err := accel.NewTensorWithData[uint8](sess, []int{n, sigLen}, sigFlat)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
defer sigs.Close()
|
||||
|
||||
pks, err := accel.NewTensorWithData[uint8](sess, []int{n, pkLen}, pkFlat)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
defer pks.Close()
|
||||
|
||||
results, err := accel.NewTensor[uint8](sess, []int{n})
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
defer results.Close()
|
||||
|
||||
if err := sess.Crypto().BLSVerifyBatch(msgs.Untyped(), sigs.Untyped(), pks.Untyped(), results.Untyped()); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
raw, err := results.ToSlice()
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
valid := make([]bool, n)
|
||||
for i, v := range raw {
|
||||
valid[i] = v == 1
|
||||
}
|
||||
return valid, nil
|
||||
}
|
||||
|
||||
// gpuCoronaVerify dispatches Corona verification via DilithiumVerifyBatch
|
||||
// (Corona threshold signatures are lattice-based, same verification kernel).
|
||||
func gpuCoronaVerify(sess *accel.Session, work *CoronaWork) ([]bool, error) {
|
||||
n := len(work.Messages)
|
||||
|
||||
msgLen := maxByteLen(work.Messages)
|
||||
sigLen := maxByteLen(work.Signatures)
|
||||
pkLen := maxByteLen(work.PubKeys)
|
||||
|
||||
msgFlat := flattenPadded(work.Messages, n, msgLen)
|
||||
sigFlat := flattenPadded(work.Signatures, n, sigLen)
|
||||
pkFlat := flattenPadded(work.PubKeys, n, pkLen)
|
||||
|
||||
msgs, err := accel.NewTensorWithData[uint8](sess, []int{n, msgLen}, msgFlat)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
defer msgs.Close()
|
||||
|
||||
sigs, err := accel.NewTensorWithData[uint8](sess, []int{n, sigLen}, sigFlat)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
defer sigs.Close()
|
||||
|
||||
pks, err := accel.NewTensorWithData[uint8](sess, []int{n, pkLen}, pkFlat)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
defer pks.Close()
|
||||
|
||||
results, err := accel.NewTensor[uint8](sess, []int{n})
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
defer results.Close()
|
||||
|
||||
if err := sess.Lattice().DilithiumVerifyBatch(msgs.Untyped(), sigs.Untyped(), pks.Untyped(), results.Untyped()); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
raw, err := results.ToSlice()
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
valid := make([]bool, n)
|
||||
for i, v := range raw {
|
||||
valid[i] = v == 1
|
||||
}
|
||||
return valid, nil
|
||||
}
|
||||
|
||||
// gpuZKVerify dispatches ZK batch proof verification via MSMBatch on ZK ops.
|
||||
func gpuZKVerify(sess *accel.Session, work *ZKWork) (bool, error) {
|
||||
m := len(work.Scalars)
|
||||
|
||||
scalarLen := maxByteLen(work.Scalars)
|
||||
baseLen := maxByteLen(work.Bases)
|
||||
|
||||
scalarFlat := flattenPadded(work.Scalars, m, scalarLen)
|
||||
baseFlat := flattenPadded(work.Bases, m, baseLen)
|
||||
|
||||
scalars, err := accel.NewTensorWithData[uint8](sess, []int{m, scalarLen}, scalarFlat)
|
||||
if err != nil {
|
||||
return false, err
|
||||
}
|
||||
defer scalars.Close()
|
||||
|
||||
bases, err := accel.NewTensorWithData[uint8](sess, []int{m, baseLen}, baseFlat)
|
||||
if err != nil {
|
||||
return false, err
|
||||
}
|
||||
defer bases.Close()
|
||||
|
||||
// MSM result: single point per batch entry
|
||||
pointSize := baseLen
|
||||
results, err := accel.NewTensor[uint8](sess, []int{m, pointSize})
|
||||
if err != nil {
|
||||
return false, err
|
||||
}
|
||||
defer results.Close()
|
||||
|
||||
if err := sess.ZK().MSMBatch(scalars.Untyped(), bases.Untyped(), results.Untyped()); err != nil {
|
||||
return false, err
|
||||
}
|
||||
|
||||
// MSM completed without error means proof verification passed
|
||||
return true, nil
|
||||
}
|
||||
|
||||
// gpuMLDSAVerify dispatches ML-DSA (Dilithium) batch verification to the GPU.
|
||||
func gpuMLDSAVerify(sess *accel.Session, work *MLDSAWork) ([]bool, error) {
|
||||
n := len(work.Messages)
|
||||
|
||||
msgLen := maxByteLen(work.Messages)
|
||||
sigLen := 3309 // FIPS-204 ML-DSA-65 signature (3293 was the stale round-3 Dilithium3 size)
|
||||
pkLen := 1952 // FIPS-204 ML-DSA-65 public key (unchanged across the round-3 -> final transition)
|
||||
|
||||
msgFlat := flattenPadded(work.Messages, n, msgLen)
|
||||
sigFlat := flattenPadded(work.Signatures, n, sigLen)
|
||||
pkFlat := flattenPadded(work.PubKeys, n, pkLen)
|
||||
|
||||
msgs, err := accel.NewTensorWithData[uint8](sess, []int{n, msgLen}, msgFlat)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
defer msgs.Close()
|
||||
|
||||
sigs, err := accel.NewTensorWithData[uint8](sess, []int{n, sigLen}, sigFlat)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
defer sigs.Close()
|
||||
|
||||
pks, err := accel.NewTensorWithData[uint8](sess, []int{n, pkLen}, pkFlat)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
defer pks.Close()
|
||||
|
||||
results, err := accel.NewTensor[uint8](sess, []int{n})
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
defer results.Close()
|
||||
|
||||
if err := sess.Lattice().DilithiumVerifyBatch(msgs.Untyped(), sigs.Untyped(), pks.Untyped(), results.Untyped()); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
raw, err := results.ToSlice()
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
valid := make([]bool, n)
|
||||
for i, v := range raw {
|
||||
valid[i] = v == 1
|
||||
}
|
||||
return valid, nil
|
||||
}
|
||||
|
||||
// verifyCPU performs all verification on the CPU as fallback.
|
||||
func (p *GPUVerifyPipeline) verifyCPU(work *BlockVerifyWork) *BlockVerifyResult {
|
||||
result := &BlockVerifyResult{}
|
||||
var wg sync.WaitGroup
|
||||
var mu sync.Mutex
|
||||
|
||||
if work.BLS != nil && len(work.BLS.Messages) > 0 {
|
||||
wg.Add(1)
|
||||
go func() {
|
||||
defer wg.Done()
|
||||
start := time.Now()
|
||||
valid := cpuBLSVerify(work.BLS)
|
||||
mu.Lock()
|
||||
result.BLSValid = valid
|
||||
result.BLSTime = time.Since(start)
|
||||
mu.Unlock()
|
||||
}()
|
||||
}
|
||||
|
||||
if work.Corona != nil && len(work.Corona.Messages) > 0 {
|
||||
wg.Add(1)
|
||||
go func() {
|
||||
defer wg.Done()
|
||||
start := time.Now()
|
||||
valid := cpuCoronaVerify(work.Corona)
|
||||
mu.Lock()
|
||||
result.CoronaValid = valid
|
||||
result.CoronaTime = time.Since(start)
|
||||
mu.Unlock()
|
||||
}()
|
||||
}
|
||||
|
||||
if work.ZK != nil && len(work.ZK.Scalars) > 0 {
|
||||
wg.Add(1)
|
||||
go func() {
|
||||
defer wg.Done()
|
||||
start := time.Now()
|
||||
valid := cpuZKVerify(work.ZK)
|
||||
mu.Lock()
|
||||
result.ZKValid = valid
|
||||
result.ZKTime = time.Since(start)
|
||||
mu.Unlock()
|
||||
}()
|
||||
}
|
||||
|
||||
if work.MLDSA != nil && len(work.MLDSA.Messages) > 0 {
|
||||
wg.Add(1)
|
||||
go func() {
|
||||
defer wg.Done()
|
||||
start := time.Now()
|
||||
valid := cpuMLDSAVerify(work.MLDSA)
|
||||
mu.Lock()
|
||||
result.MLDSAValid = valid
|
||||
result.MLDSATime = time.Since(start)
|
||||
mu.Unlock()
|
||||
}()
|
||||
}
|
||||
|
||||
wg.Wait()
|
||||
return result
|
||||
}
|
||||
|
||||
// CPU fallback implementations — the pure-Go correctness oracle.
|
||||
//
|
||||
// These perform REAL per-element cryptographic verification using the
|
||||
// luxfi/crypto pure-Go primitives (CGO_ENABLED=0-clean). They interpret each
|
||||
// element's raw bytes with the SAME layout the GPU kernels use (see
|
||||
// gpuBLSVerify / gpuMLDSAVerify), so the CPU and GPU paths are a genuine
|
||||
// equivalence pair: a no-GPU node accepts exactly the signatures a GPU node
|
||||
// accepts, and never rubber-stamps a forged one.
|
||||
//
|
||||
// Corona and ZK have no pure-Go verifier in luxfi/crypto, so those paths
|
||||
// fail closed (return false) rather than format-check-and-accept. They MUST
|
||||
// be wired to a real verifier before block-accept depends on them.
|
||||
|
||||
func cpuBLSVerify(work *BLSWork) []bool {
|
||||
valid := make([]bool, len(work.Messages))
|
||||
for i := range work.Messages {
|
||||
// Mirror gpuBLSVerify's layout: pk is a 48-byte compressed G1 point,
|
||||
// sig is a 96-byte compressed G2 point, msg is the raw message.
|
||||
// PublicKeyFromCompressedBytes / SignatureFromBytes enforce the exact
|
||||
// length, on-curve and subgroup membership the blst/CGO path enforces;
|
||||
// any malformed input fails closed (constructor error => false).
|
||||
pk, err := bls.PublicKeyFromCompressedBytes(work.PubKeys[i])
|
||||
if err != nil {
|
||||
continue
|
||||
}
|
||||
sig, err := bls.SignatureFromBytes(work.Signatures[i])
|
||||
if err != nil {
|
||||
continue
|
||||
}
|
||||
valid[i] = bls.Verify(pk, sig, work.Messages[i])
|
||||
}
|
||||
return valid
|
||||
}
|
||||
|
||||
func cpuCoronaVerify(work *CoronaWork) []bool {
|
||||
// FAIL CLOSED: luxfi/crypto exposes no pure-Go Corona (lattice threshold)
|
||||
// signature verifier, and the GPU Corona kernel is the known-wrong-prime
|
||||
// BLOCKED kernel. There is no correct way to verify a Corona signature on
|
||||
// the CPU here, so every element is rejected. Never return true for an
|
||||
// unverified signature. Wire a real Corona verifier before block-accept
|
||||
// consumes this result.
|
||||
return make([]bool, len(work.Messages))
|
||||
}
|
||||
|
||||
func cpuZKVerify(work *ZKWork) bool {
|
||||
// FAIL CLOSED: luxfi/crypto exposes no standalone pure-Go ZK proof
|
||||
// verifier (the accel MSM path is a GPU primitive, not a proof check), so
|
||||
// CPU verification cannot establish proof validity. Reject rather than
|
||||
// rubber-stamp. Wire a real ZK verifier before block-accept consumes this.
|
||||
return false
|
||||
}
|
||||
|
||||
func cpuMLDSAVerify(work *MLDSAWork) []bool {
|
||||
valid := make([]bool, len(work.Messages))
|
||||
for i := range work.Messages {
|
||||
// Mirror gpuMLDSAVerify's layout: ML-DSA-65, pk 1952 bytes, msg raw.
|
||||
// PublicKeyFromBytes enforces the exact key length and decodes the
|
||||
// point; VerifySignature uses the FIPS 204 nil-context verify,
|
||||
// matching the kernel's contextless per-tx verification, and accepts
|
||||
// the signature at its true FIPS-204 length (3309 bytes). The GPU
|
||||
// flatten width (gpuMLDSAVerify's sigLen) now agrees at 3309, so the
|
||||
// CPU oracle and GPU path size the signature identically; see
|
||||
// TestMLDSA_WorkStructSizeIsCanonical. Malformed pk fails closed
|
||||
// (constructor error).
|
||||
pub, err := mldsa.PublicKeyFromBytes(work.PubKeys[i], mldsa.MLDSA65)
|
||||
if err != nil {
|
||||
continue
|
||||
}
|
||||
valid[i] = pub.VerifySignature(work.Messages[i], work.Signatures[i])
|
||||
}
|
||||
return valid
|
||||
}
|
||||
|
||||
// validateWork checks batch size consistency.
|
||||
func validateWork(work *BlockVerifyWork) error {
|
||||
if w := work.BLS; w != nil {
|
||||
n := len(w.Messages)
|
||||
if n > 0 && (len(w.Signatures) != n || len(w.PubKeys) != n) {
|
||||
return ErrBLSSizeMismatch
|
||||
}
|
||||
}
|
||||
if w := work.Corona; w != nil {
|
||||
n := len(w.Messages)
|
||||
if n > 0 && (len(w.Signatures) != n || len(w.PubKeys) != n) {
|
||||
return ErrCoronaSizeMismatch
|
||||
}
|
||||
}
|
||||
if w := work.ZK; w != nil {
|
||||
if len(w.Scalars) > 0 && len(w.Bases) != len(w.Scalars) {
|
||||
return ErrZKSizeMismatch
|
||||
}
|
||||
}
|
||||
if w := work.MLDSA; w != nil {
|
||||
n := len(w.Messages)
|
||||
if n > 0 && (len(w.Signatures) != n || len(w.PubKeys) != n) {
|
||||
return ErrMLDSASizeMismatch
|
||||
}
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
// PipelineStats contains pipeline verification statistics.
|
||||
type PipelineStats struct {
|
||||
GPUVerifies uint64
|
||||
CPUVerifies uint64
|
||||
GPUTimeNs uint64
|
||||
CPUTimeNs uint64
|
||||
}
|
||||
|
||||
// Stats returns pipeline statistics.
|
||||
func (p *GPUVerifyPipeline) Stats() PipelineStats {
|
||||
return PipelineStats{
|
||||
GPUVerifies: atomic.LoadUint64(&p.gpuVerifies),
|
||||
CPUVerifies: atomic.LoadUint64(&p.cpuVerifies),
|
||||
GPUTimeNs: atomic.LoadUint64(&p.gpuTimeNs),
|
||||
CPUTimeNs: atomic.LoadUint64(&p.cpuTimeNs),
|
||||
}
|
||||
}
|
||||
|
||||
// Helper: find max byte slice length in a batch.
|
||||
func maxByteLen(slices [][]byte) int {
|
||||
m := 1 // minimum 1 to avoid zero-dimension tensors
|
||||
for _, s := range slices {
|
||||
if len(s) > m {
|
||||
m = len(s)
|
||||
}
|
||||
}
|
||||
return m
|
||||
}
|
||||
|
||||
// Helper: flatten [][]byte into a contiguous []uint8 with zero-padding.
|
||||
func flattenPadded(slices [][]byte, n, elemLen int) []uint8 {
|
||||
flat := make([]uint8, n*elemLen)
|
||||
for i, s := range slices {
|
||||
copy(flat[i*elemLen:], s)
|
||||
}
|
||||
return flat
|
||||
}
|
||||
@@ -1,436 +0,0 @@
|
||||
// Copyright (C) 2019-2025, Lux Industries Inc. All rights reserved.
|
||||
// See the file LICENSE for licensing terms.
|
||||
|
||||
package quasar
|
||||
|
||||
import (
|
||||
"crypto/rand"
|
||||
"testing"
|
||||
|
||||
"github.com/luxfi/crypto/bls"
|
||||
"github.com/luxfi/crypto/mldsa"
|
||||
"github.com/stretchr/testify/require"
|
||||
)
|
||||
|
||||
// makeRandomBytes returns n random bytes.
|
||||
func makeRandomBytes(n int) []byte {
|
||||
b := make([]byte, n)
|
||||
_, _ = rand.Read(b)
|
||||
return b
|
||||
}
|
||||
|
||||
// makeValidBLSEntry returns (msg, sig, pk) for a REAL BLS signature over a
|
||||
// random 32-byte message: pk is the 48-byte compressed G1 key, sig is the
|
||||
// 96-byte compressed G2 signature. The CPU oracle must accept this.
|
||||
func makeValidBLSEntry(t testing.TB) (msg, sig, pk []byte) {
|
||||
t.Helper()
|
||||
sk, err := bls.NewSecretKey()
|
||||
require.NoError(t, err)
|
||||
msg = makeRandomBytes(32)
|
||||
s, err := sk.Sign(msg)
|
||||
require.NoError(t, err)
|
||||
sig = bls.SignatureToBytes(s)
|
||||
pk = bls.PublicKeyToCompressedBytes(sk.PublicKey())
|
||||
require.Len(t, sig, 96)
|
||||
require.Len(t, pk, 48)
|
||||
return msg, sig, pk
|
||||
}
|
||||
|
||||
// makeValidMLDSAEntry returns (msg, sig, pk) for a REAL ML-DSA-65 signature
|
||||
// over a random 64-byte message. The sizes are taken from the crypto package
|
||||
// constants (FIPS-204 ML-DSA-65: pk 1952 bytes, sig 3309 bytes) rather than
|
||||
// hard-coded — see TestMLDSA_WorkStructSizeIsCanonical, which holds the
|
||||
// MLDSAWork struct / gpuMLDSAVerify sig constant pinned at the FIPS-204 3309
|
||||
// (corrected from the stale round-3 Dilithium3 3293). The CPU oracle uses the
|
||||
// typed Verify, so it accepts the real signature regardless.
|
||||
func makeValidMLDSAEntry(t testing.TB) (msg, sig, pk []byte) {
|
||||
t.Helper()
|
||||
priv, err := mldsa.GenerateKey(rand.Reader, mldsa.MLDSA65)
|
||||
require.NoError(t, err)
|
||||
msg = makeRandomBytes(64)
|
||||
sig, err = priv.Sign(rand.Reader, msg, nil)
|
||||
require.NoError(t, err)
|
||||
pk = priv.PublicKey.Bytes()
|
||||
require.Len(t, sig, mldsa.MLDSA65SignatureSize)
|
||||
require.Len(t, pk, mldsa.MLDSA65PublicKeySize)
|
||||
return msg, sig, pk
|
||||
}
|
||||
|
||||
// makeBLSWork creates BLSWork with n entries carrying REAL valid BLS
|
||||
// signatures (the CPU oracle now performs real verification).
|
||||
func makeBLSWork(t testing.TB, n int) *BLSWork {
|
||||
t.Helper()
|
||||
w := &BLSWork{
|
||||
Messages: make([][]byte, n),
|
||||
Signatures: make([][]byte, n),
|
||||
PubKeys: make([][]byte, n),
|
||||
}
|
||||
for i := 0; i < n; i++ {
|
||||
w.Messages[i], w.Signatures[i], w.PubKeys[i] = makeValidBLSEntry(t)
|
||||
}
|
||||
return w
|
||||
}
|
||||
|
||||
// makeCoronaWork creates CoronaWork with n entries.
|
||||
func makeCoronaWork(n int) *CoronaWork {
|
||||
w := &CoronaWork{
|
||||
Messages: make([][]byte, n),
|
||||
Signatures: make([][]byte, n),
|
||||
PubKeys: make([][]byte, n),
|
||||
}
|
||||
for i := 0; i < n; i++ {
|
||||
w.Messages[i] = makeRandomBytes(48)
|
||||
w.Signatures[i] = makeRandomBytes(512)
|
||||
w.PubKeys[i] = makeRandomBytes(256)
|
||||
}
|
||||
return w
|
||||
}
|
||||
|
||||
// makeZKWork creates ZKWork with m entries.
|
||||
func makeZKWork(m int) *ZKWork {
|
||||
w := &ZKWork{
|
||||
Scalars: make([][]byte, m),
|
||||
Bases: make([][]byte, m),
|
||||
}
|
||||
for i := 0; i < m; i++ {
|
||||
w.Scalars[i] = makeRandomBytes(32)
|
||||
w.Bases[i] = makeRandomBytes(64)
|
||||
}
|
||||
return w
|
||||
}
|
||||
|
||||
// makeMLDSAWork creates MLDSAWork with n entries carrying REAL valid
|
||||
// ML-DSA-65 (Dilithium3) signatures (the CPU oracle now performs real
|
||||
// verification).
|
||||
func makeMLDSAWork(t testing.TB, n int) *MLDSAWork {
|
||||
t.Helper()
|
||||
w := &MLDSAWork{
|
||||
Messages: make([][]byte, n),
|
||||
Signatures: make([][]byte, n),
|
||||
PubKeys: make([][]byte, n),
|
||||
}
|
||||
for i := 0; i < n; i++ {
|
||||
w.Messages[i], w.Signatures[i], w.PubKeys[i] = makeValidMLDSAEntry(t)
|
||||
}
|
||||
return w
|
||||
}
|
||||
|
||||
func TestGPUPipeline_AllFourTypes(t *testing.T) {
|
||||
pipeline := NewGPUVerifyPipeline()
|
||||
|
||||
work := &BlockVerifyWork{
|
||||
BLS: makeBLSWork(t, 5),
|
||||
Corona: makeCoronaWork(3),
|
||||
ZK: makeZKWork(2),
|
||||
MLDSA: makeMLDSAWork(t, 10),
|
||||
}
|
||||
|
||||
result, err := pipeline.VerifyBlock(work)
|
||||
require.NoError(t, err)
|
||||
require.NotNil(t, result)
|
||||
|
||||
// BLS results: real valid signatures, CPU oracle accepts.
|
||||
require.Len(t, result.BLSValid, 5, "should have 5 BLS results")
|
||||
for i, v := range result.BLSValid {
|
||||
require.True(t, v, "BLS[%d] should be valid", i)
|
||||
}
|
||||
|
||||
// Corona results: no pure-Go Corona verifier exists, so the CPU oracle
|
||||
// fails closed — every element is rejected (never rubber-stamped).
|
||||
require.Len(t, result.CoronaValid, 3, "should have 3 Corona results")
|
||||
for i, v := range result.CoronaValid {
|
||||
require.False(t, v, "Corona[%d] must fail closed (no pure-Go verifier)", i)
|
||||
}
|
||||
|
||||
// ZK result: no pure-Go ZK verifier exists, so the CPU oracle fails closed.
|
||||
require.False(t, result.ZKValid, "ZK batch must fail closed (no pure-Go verifier)")
|
||||
|
||||
// ML-DSA results: real valid signatures, CPU oracle accepts.
|
||||
require.Len(t, result.MLDSAValid, 10, "should have 10 ML-DSA results")
|
||||
for i, v := range result.MLDSAValid {
|
||||
require.True(t, v, "MLDSA[%d] should be valid", i)
|
||||
}
|
||||
|
||||
// Timing: all durations should be non-negative
|
||||
require.GreaterOrEqual(t, result.TotalTime.Nanoseconds(), int64(0))
|
||||
require.GreaterOrEqual(t, result.BLSTime.Nanoseconds(), int64(0))
|
||||
require.GreaterOrEqual(t, result.CoronaTime.Nanoseconds(), int64(0))
|
||||
require.GreaterOrEqual(t, result.ZKTime.Nanoseconds(), int64(0))
|
||||
require.GreaterOrEqual(t, result.MLDSATime.Nanoseconds(), int64(0))
|
||||
|
||||
// Stats should reflect the verification
|
||||
stats := pipeline.Stats()
|
||||
require.Equal(t, uint64(1), stats.GPUVerifies+stats.CPUVerifies,
|
||||
"exactly one verify should have been recorded")
|
||||
}
|
||||
|
||||
func TestGPUPipeline_CPUFallback(t *testing.T) {
|
||||
// Without CGO/GPU, accel.Available() returns false.
|
||||
// Pipeline must fall back to CPU verification.
|
||||
pipeline := NewGPUVerifyPipeline()
|
||||
|
||||
work := &BlockVerifyWork{
|
||||
BLS: makeBLSWork(t, 3),
|
||||
MLDSA: makeMLDSAWork(t, 4),
|
||||
}
|
||||
|
||||
result, err := pipeline.VerifyBlock(work)
|
||||
require.NoError(t, err)
|
||||
require.NotNil(t, result)
|
||||
|
||||
// CPU fallback performs real verification; valid signatures are accepted.
|
||||
require.Len(t, result.BLSValid, 3)
|
||||
for i, v := range result.BLSValid {
|
||||
require.True(t, v, "CPU BLS[%d] should be valid", i)
|
||||
}
|
||||
|
||||
require.Len(t, result.MLDSAValid, 4)
|
||||
for i, v := range result.MLDSAValid {
|
||||
require.True(t, v, "CPU MLDSA[%d] should be valid", i)
|
||||
}
|
||||
|
||||
// GPU should not have been used (no CGO in test env)
|
||||
require.False(t, result.GPUUsed, "should use CPU fallback")
|
||||
|
||||
stats := pipeline.Stats()
|
||||
require.Equal(t, uint64(1), stats.CPUVerifies)
|
||||
}
|
||||
|
||||
// TestCPUVerify_RealOracle proves the CPU fallback is a real cryptographic
|
||||
// oracle, not a length-checking rubber stamp: a valid signature is accepted
|
||||
// and a well-formed-but-FORGED signature (correct lengths, wrong bytes) is
|
||||
// REJECTED. The forged-rejection case is the regression guard against the
|
||||
// old `return true for well-formed inputs` behavior.
|
||||
func TestCPUVerify_RealOracle(t *testing.T) {
|
||||
t.Run("BLS valid accepted, forged rejected", func(t *testing.T) {
|
||||
msg, sig, pk := makeValidBLSEntry(t)
|
||||
|
||||
// Valid signature => accepted.
|
||||
good := cpuBLSVerify(&BLSWork{
|
||||
Messages: [][]byte{msg},
|
||||
Signatures: [][]byte{sig},
|
||||
PubKeys: [][]byte{pk},
|
||||
})
|
||||
require.Equal(t, []bool{true}, good, "valid BLS signature must be accepted")
|
||||
|
||||
// Forged signature: correct 96-byte length, random bytes => rejected.
|
||||
forgedSig := makeRandomBytes(96)
|
||||
bad := cpuBLSVerify(&BLSWork{
|
||||
Messages: [][]byte{msg},
|
||||
Signatures: [][]byte{forgedSig},
|
||||
PubKeys: [][]byte{pk},
|
||||
})
|
||||
require.Equal(t, []bool{false}, bad, "forged BLS signature (right length, wrong bytes) must be REJECTED")
|
||||
|
||||
// Valid signature against the WRONG message => rejected.
|
||||
wrongMsg := cpuBLSVerify(&BLSWork{
|
||||
Messages: [][]byte{makeRandomBytes(32)},
|
||||
Signatures: [][]byte{sig},
|
||||
PubKeys: [][]byte{pk},
|
||||
})
|
||||
require.Equal(t, []bool{false}, wrongMsg, "BLS signature over a different message must be REJECTED")
|
||||
})
|
||||
|
||||
t.Run("MLDSA valid accepted, forged rejected", func(t *testing.T) {
|
||||
msg, sig, pk := makeValidMLDSAEntry(t)
|
||||
|
||||
// Valid signature => accepted.
|
||||
good := cpuMLDSAVerify(&MLDSAWork{
|
||||
Messages: [][]byte{msg},
|
||||
Signatures: [][]byte{sig},
|
||||
PubKeys: [][]byte{pk},
|
||||
})
|
||||
require.Equal(t, []bool{true}, good, "valid ML-DSA signature must be accepted")
|
||||
|
||||
// Forged signature: correct length, random bytes => rejected.
|
||||
forgedSig := makeRandomBytes(mldsa.MLDSA65SignatureSize)
|
||||
bad := cpuMLDSAVerify(&MLDSAWork{
|
||||
Messages: [][]byte{msg},
|
||||
Signatures: [][]byte{forgedSig},
|
||||
PubKeys: [][]byte{pk},
|
||||
})
|
||||
require.Equal(t, []bool{false}, bad, "forged ML-DSA signature (right length, wrong bytes) must be REJECTED")
|
||||
|
||||
// Valid signature against the WRONG message => rejected.
|
||||
wrongMsg := cpuMLDSAVerify(&MLDSAWork{
|
||||
Messages: [][]byte{makeRandomBytes(64)},
|
||||
Signatures: [][]byte{sig},
|
||||
PubKeys: [][]byte{pk},
|
||||
})
|
||||
require.Equal(t, []bool{false}, wrongMsg, "ML-DSA signature over a different message must be REJECTED")
|
||||
})
|
||||
|
||||
t.Run("Corona fails closed", func(t *testing.T) {
|
||||
// No pure-Go Corona verifier exists; every element must be rejected,
|
||||
// never rubber-stamped on length alone.
|
||||
got := cpuCoronaVerify(&CoronaWork{
|
||||
Messages: [][]byte{makeRandomBytes(48), makeRandomBytes(48)},
|
||||
Signatures: [][]byte{makeRandomBytes(512), makeRandomBytes(512)},
|
||||
PubKeys: [][]byte{makeRandomBytes(256), makeRandomBytes(256)},
|
||||
})
|
||||
require.Equal(t, []bool{false, false}, got, "Corona must fail closed for all elements")
|
||||
})
|
||||
|
||||
t.Run("ZK fails closed", func(t *testing.T) {
|
||||
// No pure-Go ZK proof verifier exists; the batch must be rejected.
|
||||
got := cpuZKVerify(&ZKWork{
|
||||
Scalars: [][]byte{makeRandomBytes(32)},
|
||||
Bases: [][]byte{makeRandomBytes(64)},
|
||||
})
|
||||
require.False(t, got, "ZK must fail closed")
|
||||
})
|
||||
}
|
||||
|
||||
// TestMLDSA_WorkStructSizeIsCanonical pins the FIPS-204 ML-DSA-65 signature and
|
||||
// public key sizes that the MLDSAWork struct comment and gpuMLDSAVerify's
|
||||
// fixed-size flatten now use. luxfi/crypto (circl v1.6.3, FIPS-204 final)
|
||||
// produces 3309-byte ML-DSA-65 signatures (5*640 + 55 + 6 + 48 = 3309); the
|
||||
// GPU flatten width was corrected from the stale round-3 Dilithium3 size 3293
|
||||
// to 3309 so the GPU path no longer clamps/corrupts a real signature. The
|
||||
// public key size (1952) is unchanged across the round-3 -> final transition.
|
||||
//
|
||||
// This is the equivalence-pair guard: the CPU oracle (cpuMLDSAVerify) parses
|
||||
// pk via the typed PublicKeyFromBytes and calls VerifySignature, accepting the
|
||||
// real 3309-byte signature; the GPU path sizes the signature identically. If
|
||||
// the crypto constant ever drifts, this test fails before any divergence
|
||||
// reaches the GPU ML-DSA kernel (not yet wired into block-accept).
|
||||
func TestMLDSA_WorkStructSizeIsCanonical(t *testing.T) {
|
||||
require.Equal(t, 1952, mldsa.MLDSA65PublicKeySize,
|
||||
"ML-DSA-65 public key is 1952 bytes (matches MLDSAWork / gpuMLDSAVerify pkLen)")
|
||||
require.Equal(t, 3309, mldsa.MLDSA65SignatureSize,
|
||||
"ML-DSA-65 signature is 3309 bytes (FIPS-204), matching the MLDSAWork struct / gpuMLDSAVerify sigLen")
|
||||
}
|
||||
|
||||
func TestGPUPipeline_EmptyBatches(t *testing.T) {
|
||||
pipeline := NewGPUVerifyPipeline()
|
||||
|
||||
tests := []struct {
|
||||
name string
|
||||
work *BlockVerifyWork
|
||||
}{
|
||||
{
|
||||
name: "nil work",
|
||||
work: nil,
|
||||
},
|
||||
{
|
||||
name: "all nil batches",
|
||||
work: &BlockVerifyWork{},
|
||||
},
|
||||
{
|
||||
name: "empty BLS only",
|
||||
work: &BlockVerifyWork{
|
||||
BLS: &BLSWork{},
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "BLS filled, rest nil",
|
||||
work: &BlockVerifyWork{
|
||||
BLS: makeBLSWork(t, 2),
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "ZK only",
|
||||
work: &BlockVerifyWork{
|
||||
ZK: makeZKWork(1),
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "MLDSA only",
|
||||
work: &BlockVerifyWork{
|
||||
MLDSA: makeMLDSAWork(t, 1),
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "Corona only",
|
||||
work: &BlockVerifyWork{
|
||||
Corona: makeCoronaWork(1),
|
||||
},
|
||||
},
|
||||
}
|
||||
|
||||
for _, tt := range tests {
|
||||
t.Run(tt.name, func(t *testing.T) {
|
||||
result, err := pipeline.VerifyBlock(tt.work)
|
||||
require.NoError(t, err)
|
||||
require.NotNil(t, result)
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestGPUPipeline_ValidationErrors(t *testing.T) {
|
||||
pipeline := NewGPUVerifyPipeline()
|
||||
|
||||
tests := []struct {
|
||||
name string
|
||||
work *BlockVerifyWork
|
||||
wantErr error
|
||||
}{
|
||||
{
|
||||
name: "BLS size mismatch",
|
||||
work: &BlockVerifyWork{
|
||||
BLS: &BLSWork{
|
||||
Messages: [][]byte{{1}},
|
||||
Signatures: [][]byte{{1}, {2}}, // 2 != 1
|
||||
PubKeys: [][]byte{{1}},
|
||||
},
|
||||
},
|
||||
wantErr: ErrBLSSizeMismatch,
|
||||
},
|
||||
{
|
||||
name: "Corona size mismatch",
|
||||
work: &BlockVerifyWork{
|
||||
Corona: &CoronaWork{
|
||||
Messages: [][]byte{{1}, {2}},
|
||||
Signatures: [][]byte{{1}}, // 1 != 2
|
||||
PubKeys: [][]byte{{1}, {2}},
|
||||
},
|
||||
},
|
||||
wantErr: ErrCoronaSizeMismatch,
|
||||
},
|
||||
{
|
||||
name: "ZK size mismatch",
|
||||
work: &BlockVerifyWork{
|
||||
ZK: &ZKWork{
|
||||
Scalars: [][]byte{{1}, {2}},
|
||||
Bases: [][]byte{{1}}, // 1 != 2
|
||||
},
|
||||
},
|
||||
wantErr: ErrZKSizeMismatch,
|
||||
},
|
||||
{
|
||||
name: "MLDSA size mismatch",
|
||||
work: &BlockVerifyWork{
|
||||
MLDSA: &MLDSAWork{
|
||||
Messages: [][]byte{{1}},
|
||||
Signatures: [][]byte{{1}},
|
||||
PubKeys: [][]byte{{1}, {2}}, // 2 != 1
|
||||
},
|
||||
},
|
||||
wantErr: ErrMLDSASizeMismatch,
|
||||
},
|
||||
}
|
||||
|
||||
for _, tt := range tests {
|
||||
t.Run(tt.name, func(t *testing.T) {
|
||||
_, err := pipeline.VerifyBlock(tt.work)
|
||||
require.ErrorIs(t, err, tt.wantErr)
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func BenchmarkGPUPipeline(b *testing.B) {
|
||||
pipeline := NewGPUVerifyPipeline()
|
||||
|
||||
work := &BlockVerifyWork{
|
||||
BLS: makeBLSWork(b, 100),
|
||||
Corona: makeCoronaWork(50),
|
||||
ZK: makeZKWork(10),
|
||||
MLDSA: makeMLDSAWork(b, 200),
|
||||
}
|
||||
|
||||
b.ResetTimer()
|
||||
b.ReportAllocs()
|
||||
for i := 0; i < b.N; i++ {
|
||||
_, _ = pipeline.VerifyBlock(work)
|
||||
}
|
||||
}
|
||||
@@ -1,970 +0,0 @@
|
||||
// Copyright (C) 2019-2025, Lux Industries Inc. All rights reserved.
|
||||
// See the file LICENSE for licensing terms.
|
||||
|
||||
// Package quasar integration tests.
|
||||
//
|
||||
// These tests exercise realistic end-to-end scenarios for Quasar consensus:
|
||||
// - Full component wiring and event processing
|
||||
// - Corona threshold signing flows (skipped if lattice lib unavailable)
|
||||
// - Concurrent operation safety
|
||||
// - Stop/start lifecycle management
|
||||
// - Memory behavior with many finality events
|
||||
//
|
||||
// Run with: go test -v -run "^Test.*Integration\|^TestQuasar" ./...
|
||||
// Skip long tests: go test -short ./...
|
||||
|
||||
package quasar
|
||||
|
||||
import (
|
||||
"context"
|
||||
"crypto/rand"
|
||||
"runtime"
|
||||
"strings"
|
||||
"sync"
|
||||
"sync/atomic"
|
||||
"testing"
|
||||
"time"
|
||||
|
||||
"github.com/luxfi/ids"
|
||||
"github.com/luxfi/log"
|
||||
"github.com/stretchr/testify/require"
|
||||
)
|
||||
|
||||
// ----------------------------------------------------------------------------
|
||||
// Mock implementations for integration tests
|
||||
// ----------------------------------------------------------------------------
|
||||
|
||||
// mockPChainProvider implements PChainProvider for tests
|
||||
type mockPChainProvider struct {
|
||||
mu sync.RWMutex
|
||||
height uint64
|
||||
validators []ValidatorState
|
||||
finalityCh chan FinalityEvent
|
||||
closed bool
|
||||
}
|
||||
|
||||
func newMockPChainProvider(validators []ValidatorState) *mockPChainProvider {
|
||||
return &mockPChainProvider{
|
||||
height: 0,
|
||||
validators: validators,
|
||||
finalityCh: make(chan FinalityEvent, 100),
|
||||
}
|
||||
}
|
||||
|
||||
func (m *mockPChainProvider) GetFinalizedHeight() uint64 {
|
||||
m.mu.RLock()
|
||||
defer m.mu.RUnlock()
|
||||
return m.height
|
||||
}
|
||||
|
||||
func (m *mockPChainProvider) GetValidators(height uint64) ([]ValidatorState, error) {
|
||||
m.mu.RLock()
|
||||
defer m.mu.RUnlock()
|
||||
return m.validators, nil
|
||||
}
|
||||
|
||||
func (m *mockPChainProvider) SubscribeFinality() <-chan FinalityEvent {
|
||||
return m.finalityCh
|
||||
}
|
||||
|
||||
func (m *mockPChainProvider) Validators() []ValidatorState {
|
||||
m.mu.RLock()
|
||||
defer m.mu.RUnlock()
|
||||
return append([]ValidatorState(nil), m.validators...)
|
||||
}
|
||||
|
||||
func (m *mockPChainProvider) EmitFinality(event FinalityEvent) {
|
||||
m.mu.Lock()
|
||||
defer m.mu.Unlock()
|
||||
if m.closed {
|
||||
return
|
||||
}
|
||||
m.height = event.Height
|
||||
select {
|
||||
case m.finalityCh <- event:
|
||||
default:
|
||||
}
|
||||
}
|
||||
|
||||
func (m *mockPChainProvider) Close() {
|
||||
m.mu.Lock()
|
||||
defer m.mu.Unlock()
|
||||
if !m.closed {
|
||||
m.closed = true
|
||||
close(m.finalityCh)
|
||||
}
|
||||
}
|
||||
|
||||
// mockQuantumSigner implements QuantumSignerFallback for tests
|
||||
type mockQuantumSigner struct{}
|
||||
|
||||
func (m *mockQuantumSigner) SignMessage(msg []byte) ([]byte, error) {
|
||||
return []byte("RT-MOCK-SIG"), nil
|
||||
}
|
||||
|
||||
// ----------------------------------------------------------------------------
|
||||
// Helper functions
|
||||
// ----------------------------------------------------------------------------
|
||||
|
||||
// generateValidatorStates creates n ValidatorState entries
|
||||
func generateValidatorStates(n int) []ValidatorState {
|
||||
states := make([]ValidatorState, n)
|
||||
for i := range states {
|
||||
blsKey := make([]byte, 48)
|
||||
rtKey := make([]byte, 32)
|
||||
_, _ = rand.Read(blsKey)
|
||||
_, _ = rand.Read(rtKey)
|
||||
|
||||
states[i] = ValidatorState{
|
||||
NodeID: ids.GenerateTestNodeID(),
|
||||
Weight: 1000,
|
||||
BLSPubKey: blsKey,
|
||||
CoronaKey: rtKey,
|
||||
Active: true,
|
||||
}
|
||||
}
|
||||
return states
|
||||
}
|
||||
|
||||
// createTestEvent creates a FinalityEvent for testing
|
||||
func createTestEvent(height uint64, validators []ValidatorState) FinalityEvent {
|
||||
var blockID ids.ID
|
||||
_, _ = rand.Read(blockID[:])
|
||||
|
||||
return FinalityEvent{
|
||||
Height: height,
|
||||
BlockID: blockID,
|
||||
Validators: validators,
|
||||
Timestamp: time.Now(),
|
||||
}
|
||||
}
|
||||
|
||||
// setupQuasarWithCorona creates a Quasar with a test Corona coordinator.
|
||||
// Returns nil for Quasar if Corona initialization fails (e.g., lattice lib constraint).
|
||||
func setupQuasarWithCorona(t *testing.T, numParties int) (*Quasar, *mockPChainProvider, []ids.NodeID, error) {
|
||||
t.Helper()
|
||||
|
||||
validatorStates := generateValidatorStates(numParties)
|
||||
pchain := newMockPChainProvider(validatorStates)
|
||||
|
||||
q, err := NewQuasar(log.NewNoOpLogger(), 3, 2, 3)
|
||||
if err != nil {
|
||||
return nil, nil, nil, err
|
||||
}
|
||||
|
||||
// Connect providers
|
||||
q.ConnectPChain(pchain)
|
||||
q.ConnectQuantumFallback(&mockQuantumSigner{})
|
||||
|
||||
// Create a test Corona coordinator (stub signatures, not production)
|
||||
threshold := (numParties * 2 / 3) + 1
|
||||
if threshold < 2 {
|
||||
threshold = 2
|
||||
}
|
||||
rc, err := NewTestCoronaCoordinator(log.NewNoOpLogger(), CoronaConfig{
|
||||
NumParties: numParties,
|
||||
Threshold: threshold,
|
||||
})
|
||||
if err != nil {
|
||||
pchain.Close()
|
||||
return nil, nil, nil, err
|
||||
}
|
||||
q.ConnectCorona(rc)
|
||||
|
||||
// Extract node IDs and initialize Corona
|
||||
nodeIDs := make([]ids.NodeID, len(validatorStates))
|
||||
for i, v := range validatorStates {
|
||||
nodeIDs[i] = v.NodeID
|
||||
}
|
||||
|
||||
err = q.InitializeCorona(nodeIDs)
|
||||
if err != nil {
|
||||
pchain.Close()
|
||||
return nil, nil, nil, err
|
||||
}
|
||||
|
||||
return q, pchain, nodeIDs, nil
|
||||
}
|
||||
|
||||
// isLatticeUnavailable checks if an error indicates lattice library constraints
|
||||
func isLatticeUnavailable(err error) bool {
|
||||
if err == nil {
|
||||
return false
|
||||
}
|
||||
msg := err.Error()
|
||||
return strings.Contains(msg, "ring") || strings.Contains(msg, "modulus") ||
|
||||
strings.Contains(msg, "prime") || strings.Contains(msg, "lattice")
|
||||
}
|
||||
|
||||
// ----------------------------------------------------------------------------
|
||||
// Integration Tests
|
||||
// ----------------------------------------------------------------------------
|
||||
|
||||
// TestQuasarFullFlow tests creating Quasar, connecting components, and processing events
|
||||
func TestQuasarFullFlow(t *testing.T) {
|
||||
const numValidators = 5
|
||||
|
||||
t.Run("create_and_connect", func(t *testing.T) {
|
||||
q, err := NewQuasar(log.NewNoOpLogger(), 3, 2, 3)
|
||||
require.NoError(t, err, "NewQuasar should succeed")
|
||||
require.NotNil(t, q, "Quasar should not be nil")
|
||||
|
||||
// Verify initial state
|
||||
stats := q.Stats()
|
||||
require.False(t, stats.Running, "should not be running initially")
|
||||
require.Equal(t, uint64(0), stats.PChainHeight)
|
||||
require.Equal(t, uint64(0), stats.QChainHeight)
|
||||
|
||||
// Connect components
|
||||
validatorStates := generateValidatorStates(numValidators)
|
||||
pchain := newMockPChainProvider(validatorStates)
|
||||
defer pchain.Close()
|
||||
|
||||
q.ConnectPChain(pchain)
|
||||
q.ConnectQuantumFallback(&mockQuantumSigner{})
|
||||
|
||||
// Verify configuration
|
||||
threshold, quorumNum, quorumDen := q.GetConfig()
|
||||
require.Equal(t, 3, threshold)
|
||||
require.Equal(t, uint64(2), quorumNum)
|
||||
require.Equal(t, uint64(3), quorumDen)
|
||||
})
|
||||
|
||||
t.Run("start_and_stop", func(t *testing.T) {
|
||||
validatorStates := generateValidatorStates(numValidators)
|
||||
pchain := newMockPChainProvider(validatorStates)
|
||||
defer pchain.Close()
|
||||
|
||||
q, err := NewQuasar(log.NewNoOpLogger(), 3, 2, 3)
|
||||
require.NoError(t, err)
|
||||
|
||||
q.ConnectPChain(pchain)
|
||||
q.ConnectQuantumFallback(&mockQuantumSigner{})
|
||||
|
||||
// Start
|
||||
ctx, cancel := context.WithCancel(context.Background())
|
||||
defer cancel()
|
||||
|
||||
err = q.Start(ctx)
|
||||
require.NoError(t, err, "Start should succeed")
|
||||
require.True(t, q.IsRunning(), "should be running after Start")
|
||||
|
||||
// Stop
|
||||
q.Stop()
|
||||
// Give goroutines time to shut down
|
||||
time.Sleep(50 * time.Millisecond)
|
||||
require.False(t, q.IsRunning(), "should not be running after Stop")
|
||||
})
|
||||
|
||||
t.Run("process_single_event", func(t *testing.T) {
|
||||
q, pchain, _, err := setupQuasarWithCorona(t, numValidators)
|
||||
if isLatticeUnavailable(err) {
|
||||
t.Skipf("Skipping: lattice library constraint: %v", err)
|
||||
}
|
||||
require.NoError(t, err)
|
||||
defer pchain.Close()
|
||||
|
||||
ctx, cancel := context.WithCancel(context.Background())
|
||||
defer cancel()
|
||||
|
||||
err = q.Start(ctx)
|
||||
require.NoError(t, err)
|
||||
defer q.Stop()
|
||||
|
||||
// Emit event
|
||||
event := createTestEvent(1, pchain.Validators())
|
||||
pchain.EmitFinality(event)
|
||||
|
||||
require.Eventually(t, func() bool {
|
||||
return q.Stats().PChainHeight >= 1
|
||||
}, time.Second, 10*time.Millisecond, "P-chain height should be 1")
|
||||
})
|
||||
|
||||
t.Run("verify_quorum_calculation", func(t *testing.T) {
|
||||
q, err := NewQuasar(log.NewNoOpLogger(), 3, 2, 3)
|
||||
require.NoError(t, err)
|
||||
|
||||
// Test quorum: 2/3 means 67% needed
|
||||
require.True(t, q.CheckQuorum(670, 1000), "67% should meet 2/3 quorum")
|
||||
require.True(t, q.CheckQuorum(667, 1000), "66.7% should meet 2/3 quorum")
|
||||
require.False(t, q.CheckQuorum(600, 1000), "60% should not meet 2/3 quorum")
|
||||
require.False(t, q.CheckQuorum(0, 1000), "0% should not meet quorum")
|
||||
// Note: zero total weight is an edge case - required becomes 0, so any signer weight passes
|
||||
// This is intentional: if there are no validators, there's nothing to check
|
||||
require.True(t, q.CheckQuorum(500, 0), "zero total is edge case (required=0)")
|
||||
})
|
||||
}
|
||||
|
||||
// TestQuasarWithCorona tests full threshold signing flow
|
||||
func TestQuasarWithCorona(t *testing.T) {
|
||||
// All Corona tests require the lattice library to work correctly.
|
||||
// Skip if the library has constraints (e.g., requires prime moduli).
|
||||
|
||||
t.Run("initialize_and_sign", func(t *testing.T) {
|
||||
q, pchain, _, err := setupQuasarWithCorona(t, 5)
|
||||
if isLatticeUnavailable(err) {
|
||||
t.Skipf("Skipping: lattice library constraint: %v", err)
|
||||
}
|
||||
require.NoError(t, err)
|
||||
defer pchain.Close()
|
||||
|
||||
// Verify Corona is connected
|
||||
require.NotNil(t, q.corona, "Corona should be connected")
|
||||
require.True(t, q.corona.IsInitialized(), "Corona should be initialized")
|
||||
})
|
||||
|
||||
t.Run("sign_and_verify", func(t *testing.T) {
|
||||
q, pchain, _, err := setupQuasarWithCorona(t, 5)
|
||||
if isLatticeUnavailable(err) {
|
||||
t.Skipf("Skipping: lattice library constraint: %v", err)
|
||||
}
|
||||
require.NoError(t, err)
|
||||
defer pchain.Close()
|
||||
|
||||
// Sign a message
|
||||
msg := []byte("test message for signing")
|
||||
sig, err := q.corona.Sign(msg)
|
||||
require.NoError(t, err, "Sign should succeed")
|
||||
require.NotNil(t, sig, "Signature should not be nil")
|
||||
|
||||
// Verify signature
|
||||
valid := q.corona.Verify(msg, sig)
|
||||
require.True(t, valid, "Signature should verify")
|
||||
})
|
||||
|
||||
t.Run("multiple_signing_sessions", func(t *testing.T) {
|
||||
q, pchain, _, err := setupQuasarWithCorona(t, 5)
|
||||
if isLatticeUnavailable(err) {
|
||||
t.Skipf("Skipping: lattice library constraint: %v", err)
|
||||
}
|
||||
require.NoError(t, err)
|
||||
defer pchain.Close()
|
||||
|
||||
// Sign multiple messages
|
||||
for i := 0; i < 3; i++ {
|
||||
msg := []byte("message " + string(rune('A'+i)))
|
||||
sig, err := q.corona.Sign(msg)
|
||||
require.NoError(t, err, "Sign %d should succeed", i)
|
||||
require.True(t, q.corona.Verify(msg, sig), "Signature %d should verify", i)
|
||||
}
|
||||
})
|
||||
|
||||
t.Run("threshold_parameter_check", func(t *testing.T) {
|
||||
q, pchain, _, err := setupQuasarWithCorona(t, 5)
|
||||
if isLatticeUnavailable(err) {
|
||||
t.Skipf("Skipping: lattice library constraint: %v", err)
|
||||
}
|
||||
require.NoError(t, err)
|
||||
defer pchain.Close()
|
||||
|
||||
// With 5 parties, threshold = (5 * 2 / 3) + 1 = 4
|
||||
require.Equal(t, 4, q.corona.Threshold(), "Threshold should be 4 for 5 parties")
|
||||
require.Equal(t, 5, q.corona.NumParties(), "NumParties should be 5")
|
||||
})
|
||||
}
|
||||
|
||||
// TestQuasarConcurrent tests concurrent finality processing
|
||||
func TestQuasarConcurrent(t *testing.T) {
|
||||
const numValidators = 5
|
||||
const numEvents = 50
|
||||
|
||||
q, pchain, _, err := setupQuasarWithCorona(t, numValidators)
|
||||
if isLatticeUnavailable(err) {
|
||||
t.Skipf("Skipping: lattice library constraint: %v", err)
|
||||
}
|
||||
require.NoError(t, err)
|
||||
defer pchain.Close()
|
||||
validatorStates := pchain.Validators()
|
||||
|
||||
ctx, cancel := context.WithCancel(context.Background())
|
||||
defer cancel()
|
||||
|
||||
err = q.Start(ctx)
|
||||
require.NoError(t, err)
|
||||
defer q.Stop()
|
||||
|
||||
// Send events concurrently
|
||||
var wg sync.WaitGroup
|
||||
for i := uint64(1); i <= numEvents; i++ {
|
||||
wg.Add(1)
|
||||
go func(height uint64) {
|
||||
defer wg.Done()
|
||||
event := createTestEvent(height, validatorStates)
|
||||
pchain.EmitFinality(event)
|
||||
}(i)
|
||||
}
|
||||
wg.Wait()
|
||||
|
||||
// Wait for processing
|
||||
time.Sleep(500 * time.Millisecond)
|
||||
|
||||
stats := q.Stats()
|
||||
t.Logf("Processed %d events, finalized blocks: %d", numEvents, stats.FinalizedBlocks)
|
||||
require.GreaterOrEqual(t, stats.FinalizedBlocks, 1, "should have finalized at least 1 block")
|
||||
}
|
||||
|
||||
// TestQuasarConcurrentCoronaSigning tests concurrent Corona signing
|
||||
func TestQuasarConcurrentCoronaSigning(t *testing.T) {
|
||||
q, pchain, _, err := setupQuasarWithCorona(t, 5)
|
||||
if isLatticeUnavailable(err) {
|
||||
t.Skipf("Skipping: lattice library constraint: %v", err)
|
||||
}
|
||||
require.NoError(t, err)
|
||||
defer pchain.Close()
|
||||
|
||||
const numSigners = 10
|
||||
var wg sync.WaitGroup
|
||||
var successCount atomic.Int32
|
||||
|
||||
for i := 0; i < numSigners; i++ {
|
||||
wg.Add(1)
|
||||
go func(idx int) {
|
||||
defer wg.Done()
|
||||
msg := []byte("concurrent message " + string(rune('0'+idx)))
|
||||
sig, err := q.corona.Sign(msg)
|
||||
if err == nil && q.corona.Verify(msg, sig) {
|
||||
successCount.Add(1)
|
||||
}
|
||||
}(i)
|
||||
}
|
||||
wg.Wait()
|
||||
|
||||
require.Equal(t, int32(numSigners), successCount.Load(), "all concurrent signs should succeed")
|
||||
}
|
||||
|
||||
// TestQuasarRestart tests stop/start cycles
|
||||
func TestQuasarRestart(t *testing.T) {
|
||||
const numValidators = 5
|
||||
|
||||
t.Run("basic_stop_start", func(t *testing.T) {
|
||||
validatorStates := generateValidatorStates(numValidators)
|
||||
pchain := newMockPChainProvider(validatorStates)
|
||||
defer pchain.Close()
|
||||
|
||||
// First cycle
|
||||
q1, err := NewQuasar(log.NewNoOpLogger(), 3, 2, 3)
|
||||
require.NoError(t, err)
|
||||
q1.ConnectPChain(pchain)
|
||||
q1.ConnectQuantumFallback(&mockQuantumSigner{})
|
||||
|
||||
ctx1, cancel1 := context.WithCancel(context.Background())
|
||||
err = q1.Start(ctx1)
|
||||
require.NoError(t, err)
|
||||
require.True(t, q1.IsRunning())
|
||||
cancel1()
|
||||
q1.Stop()
|
||||
time.Sleep(50 * time.Millisecond)
|
||||
require.False(t, q1.IsRunning())
|
||||
|
||||
// Second cycle with fresh Quasar instance
|
||||
// Note: The current implementation closes stopCh on Stop and doesn't recreate it,
|
||||
// so restart requires a new instance. This is a known limitation.
|
||||
q2, err := NewQuasar(log.NewNoOpLogger(), 3, 2, 3)
|
||||
require.NoError(t, err)
|
||||
q2.ConnectPChain(pchain)
|
||||
q2.ConnectQuantumFallback(&mockQuantumSigner{})
|
||||
|
||||
ctx2, cancel2 := context.WithCancel(context.Background())
|
||||
defer cancel2()
|
||||
err = q2.Start(ctx2)
|
||||
require.NoError(t, err)
|
||||
require.True(t, q2.IsRunning())
|
||||
q2.Stop()
|
||||
})
|
||||
|
||||
t.Run("stop_with_pending_events", func(t *testing.T) {
|
||||
validatorStates := generateValidatorStates(numValidators)
|
||||
pchain := newMockPChainProvider(validatorStates)
|
||||
defer pchain.Close()
|
||||
|
||||
q, err := NewQuasar(log.NewNoOpLogger(), 3, 2, 3)
|
||||
require.NoError(t, err)
|
||||
|
||||
q.ConnectPChain(pchain)
|
||||
q.ConnectQuantumFallback(&mockQuantumSigner{})
|
||||
|
||||
ctx, cancel := context.WithCancel(context.Background())
|
||||
defer cancel()
|
||||
|
||||
err = q.Start(ctx)
|
||||
require.NoError(t, err)
|
||||
|
||||
// Emit events
|
||||
for i := uint64(1); i <= 10; i++ {
|
||||
event := createTestEvent(i, validatorStates)
|
||||
pchain.EmitFinality(event)
|
||||
}
|
||||
|
||||
// Stop immediately
|
||||
q.Stop()
|
||||
time.Sleep(50 * time.Millisecond)
|
||||
require.False(t, q.IsRunning(), "should stop cleanly with pending events")
|
||||
})
|
||||
|
||||
t.Run("multiple_stop_calls", func(t *testing.T) {
|
||||
q, err := NewQuasar(log.NewNoOpLogger(), 3, 2, 3)
|
||||
require.NoError(t, err)
|
||||
|
||||
validatorStates := generateValidatorStates(numValidators)
|
||||
pchain := newMockPChainProvider(validatorStates)
|
||||
defer pchain.Close()
|
||||
|
||||
q.ConnectPChain(pchain)
|
||||
q.ConnectQuantumFallback(&mockQuantumSigner{})
|
||||
|
||||
ctx, cancel := context.WithCancel(context.Background())
|
||||
defer cancel()
|
||||
|
||||
err = q.Start(ctx)
|
||||
require.NoError(t, err)
|
||||
|
||||
// Multiple stops should not panic
|
||||
q.Stop()
|
||||
// Note: After first Stop, stopCh is closed. Subsequent Stop calls check running flag,
|
||||
// but since running=false, they won't try to close again. This tests idempotency.
|
||||
})
|
||||
|
||||
t.Run("stop_without_start", func(t *testing.T) {
|
||||
q, err := NewQuasar(log.NewNoOpLogger(), 3, 2, 3)
|
||||
require.NoError(t, err)
|
||||
|
||||
// Stop without start should not panic
|
||||
q.Stop()
|
||||
require.False(t, q.IsRunning())
|
||||
})
|
||||
}
|
||||
|
||||
// TestQuasarMemoryPressure tests with many finality events to verify no memory leaks
|
||||
func TestQuasarMemoryPressure(t *testing.T) {
|
||||
if testing.Short() {
|
||||
t.Skip("skipping memory pressure test in short mode")
|
||||
}
|
||||
|
||||
t.Run("many_finality_entries", func(t *testing.T) {
|
||||
q, err := NewQuasar(log.NewNoOpLogger(), 3, 2, 3)
|
||||
require.NoError(t, err)
|
||||
|
||||
// Add many finality entries
|
||||
const numEntries = 1000
|
||||
for i := 0; i < numEntries; i++ {
|
||||
var blockID ids.ID
|
||||
_, _ = rand.Read(blockID[:])
|
||||
|
||||
finality := &QuantumFinality{
|
||||
BlockID: blockID,
|
||||
PChainHeight: uint64(i),
|
||||
QChainHeight: uint64(i),
|
||||
TotalWeight: 1000,
|
||||
SignerWeight: 700,
|
||||
}
|
||||
q.SetFinalized(blockID, finality)
|
||||
}
|
||||
|
||||
stats := q.Stats()
|
||||
require.GreaterOrEqual(t, stats.FinalizedBlocks, numEntries-1)
|
||||
})
|
||||
|
||||
t.Run("memory_stability", func(t *testing.T) {
|
||||
q, err := NewQuasar(log.NewNoOpLogger(), 3, 2, 3)
|
||||
require.NoError(t, err)
|
||||
|
||||
// Add many entries
|
||||
const numEntries = 10000
|
||||
for i := 0; i < numEntries; i++ {
|
||||
var blockID ids.ID
|
||||
_, _ = rand.Read(blockID[:])
|
||||
|
||||
finality := &QuantumFinality{
|
||||
BlockID: blockID,
|
||||
PChainHeight: uint64(i),
|
||||
QChainHeight: uint64(i),
|
||||
BLSProof: make([]byte, 96),
|
||||
CoronaProof: make([]byte, 1024),
|
||||
TotalWeight: 1000,
|
||||
SignerWeight: 700,
|
||||
}
|
||||
q.SetFinalized(blockID, finality)
|
||||
}
|
||||
|
||||
// Force GC and check we don't crash
|
||||
runtime.GC()
|
||||
var m runtime.MemStats
|
||||
runtime.ReadMemStats(&m)
|
||||
|
||||
t.Logf("Heap after %d entries: %d bytes", numEntries, m.HeapAlloc)
|
||||
|
||||
// Just verify we completed without issues - memory testing is notoriously flaky
|
||||
stats := q.Stats()
|
||||
require.GreaterOrEqual(t, stats.FinalizedBlocks, numEntries-1)
|
||||
})
|
||||
|
||||
t.Run("concurrent_add_finality", func(t *testing.T) {
|
||||
q, err := NewQuasar(log.NewNoOpLogger(), 3, 2, 3)
|
||||
require.NoError(t, err)
|
||||
|
||||
const numGoroutines = 10
|
||||
const entriesPerGoroutine = 250
|
||||
|
||||
var wg sync.WaitGroup
|
||||
for g := 0; g < numGoroutines; g++ {
|
||||
wg.Add(1)
|
||||
go func(gid int) {
|
||||
defer wg.Done()
|
||||
for i := 0; i < entriesPerGoroutine; i++ {
|
||||
var blockID ids.ID
|
||||
_, _ = rand.Read(blockID[:])
|
||||
|
||||
finality := &QuantumFinality{
|
||||
BlockID: blockID,
|
||||
PChainHeight: uint64(gid*entriesPerGoroutine + i),
|
||||
QChainHeight: uint64(gid*entriesPerGoroutine + i),
|
||||
TotalWeight: 1000,
|
||||
SignerWeight: 700,
|
||||
}
|
||||
q.SetFinalized(blockID, finality)
|
||||
}
|
||||
}(g)
|
||||
}
|
||||
wg.Wait()
|
||||
|
||||
stats := q.Stats()
|
||||
t.Logf("Final entries after concurrent operations: %d", stats.FinalizedBlocks)
|
||||
// Some entries may share block IDs due to rand collision, so just verify we have many
|
||||
require.GreaterOrEqual(t, stats.FinalizedBlocks, numGoroutines*entriesPerGoroutine/2)
|
||||
})
|
||||
|
||||
t.Run("concurrent_read_write", func(t *testing.T) {
|
||||
q, err := NewQuasar(log.NewNoOpLogger(), 3, 2, 3)
|
||||
require.NoError(t, err)
|
||||
|
||||
// Pre-populate some entries
|
||||
blockIDs := make([]ids.ID, 100)
|
||||
for i := range blockIDs {
|
||||
_, _ = rand.Read(blockIDs[i][:])
|
||||
q.SetFinalized(blockIDs[i], &QuantumFinality{
|
||||
BlockID: blockIDs[i],
|
||||
PChainHeight: uint64(i),
|
||||
})
|
||||
}
|
||||
|
||||
// Concurrent reads and writes
|
||||
var wg sync.WaitGroup
|
||||
done := make(chan struct{})
|
||||
|
||||
// Writers
|
||||
for w := 0; w < 5; w++ {
|
||||
wg.Add(1)
|
||||
go func() {
|
||||
defer wg.Done()
|
||||
for {
|
||||
select {
|
||||
case <-done:
|
||||
return
|
||||
default:
|
||||
var blockID ids.ID
|
||||
_, _ = rand.Read(blockID[:])
|
||||
q.SetFinalized(blockID, &QuantumFinality{BlockID: blockID})
|
||||
}
|
||||
}
|
||||
}()
|
||||
}
|
||||
|
||||
// Readers
|
||||
for r := 0; r < 5; r++ {
|
||||
wg.Add(1)
|
||||
go func() {
|
||||
defer wg.Done()
|
||||
for {
|
||||
select {
|
||||
case <-done:
|
||||
return
|
||||
default:
|
||||
idx := int(time.Now().UnixNano()) % len(blockIDs)
|
||||
_, _ = q.GetFinality(blockIDs[idx])
|
||||
}
|
||||
}
|
||||
}()
|
||||
}
|
||||
|
||||
// Run for a short period
|
||||
time.Sleep(100 * time.Millisecond)
|
||||
close(done)
|
||||
wg.Wait()
|
||||
|
||||
t.Log("Concurrent read/write completed successfully")
|
||||
})
|
||||
}
|
||||
|
||||
// TestQuasarHealthStatus tests health status reporting
|
||||
func TestQuasarHealthStatus(t *testing.T) {
|
||||
t.Run("initial_state", func(t *testing.T) {
|
||||
q, err := NewQuasar(log.NewNoOpLogger(), 3, 2, 3)
|
||||
require.NoError(t, err)
|
||||
|
||||
stats := q.Stats()
|
||||
require.False(t, stats.Running)
|
||||
require.Equal(t, uint64(0), stats.PChainHeight)
|
||||
require.Equal(t, uint64(0), stats.QChainHeight)
|
||||
require.Equal(t, 0, stats.FinalizedBlocks)
|
||||
})
|
||||
|
||||
t.Run("after_corona_init", func(t *testing.T) {
|
||||
q, pchain, _, err := setupQuasarWithCorona(t, 5)
|
||||
if isLatticeUnavailable(err) {
|
||||
t.Skipf("Skipping: lattice library constraint: %v", err)
|
||||
}
|
||||
require.NoError(t, err)
|
||||
defer pchain.Close()
|
||||
|
||||
stats := q.Stats()
|
||||
require.True(t, stats.CoronaReady, "Corona should be ready")
|
||||
})
|
||||
|
||||
t.Run("running_state", func(t *testing.T) {
|
||||
validatorStates := generateValidatorStates(5)
|
||||
pchain := newMockPChainProvider(validatorStates)
|
||||
defer pchain.Close()
|
||||
|
||||
q, err := NewQuasar(log.NewNoOpLogger(), 3, 2, 3)
|
||||
require.NoError(t, err)
|
||||
|
||||
q.ConnectPChain(pchain)
|
||||
q.ConnectQuantumFallback(&mockQuantumSigner{})
|
||||
|
||||
ctx, cancel := context.WithCancel(context.Background())
|
||||
defer cancel()
|
||||
|
||||
err = q.Start(ctx)
|
||||
require.NoError(t, err)
|
||||
defer q.Stop()
|
||||
|
||||
stats := q.Stats()
|
||||
require.True(t, stats.Running, "should be running")
|
||||
})
|
||||
}
|
||||
|
||||
// TestQuasarShutdown tests graceful shutdown behavior
|
||||
func TestQuasarShutdown(t *testing.T) {
|
||||
t.Run("graceful_stop_with_timeout", func(t *testing.T) {
|
||||
validatorStates := generateValidatorStates(5)
|
||||
pchain := newMockPChainProvider(validatorStates)
|
||||
defer pchain.Close()
|
||||
|
||||
q, err := NewQuasar(log.NewNoOpLogger(), 3, 2, 3)
|
||||
require.NoError(t, err)
|
||||
|
||||
q.ConnectPChain(pchain)
|
||||
q.ConnectQuantumFallback(&mockQuantumSigner{})
|
||||
|
||||
ctx, cancel := context.WithCancel(context.Background())
|
||||
err = q.Start(ctx)
|
||||
require.NoError(t, err)
|
||||
|
||||
// Cancel context and stop
|
||||
cancel()
|
||||
q.Stop()
|
||||
|
||||
require.False(t, q.IsRunning())
|
||||
})
|
||||
|
||||
t.Run("stop_already_stopped", func(t *testing.T) {
|
||||
q, err := NewQuasar(log.NewNoOpLogger(), 3, 2, 3)
|
||||
require.NoError(t, err)
|
||||
|
||||
// Should not panic
|
||||
q.Stop()
|
||||
require.False(t, q.IsRunning())
|
||||
})
|
||||
|
||||
t.Run("start_after_stop", func(t *testing.T) {
|
||||
validatorStates := generateValidatorStates(5)
|
||||
pchain := newMockPChainProvider(validatorStates)
|
||||
defer pchain.Close()
|
||||
|
||||
// First run
|
||||
q1, err := NewQuasar(log.NewNoOpLogger(), 3, 2, 3)
|
||||
require.NoError(t, err)
|
||||
q1.ConnectPChain(pchain)
|
||||
q1.ConnectQuantumFallback(&mockQuantumSigner{})
|
||||
|
||||
ctx1, cancel1 := context.WithCancel(context.Background())
|
||||
err = q1.Start(ctx1)
|
||||
require.NoError(t, err)
|
||||
cancel1()
|
||||
q1.Stop()
|
||||
|
||||
// Second run with new instance (implementation limitation)
|
||||
q2, err := NewQuasar(log.NewNoOpLogger(), 3, 2, 3)
|
||||
require.NoError(t, err)
|
||||
q2.ConnectPChain(pchain)
|
||||
q2.ConnectQuantumFallback(&mockQuantumSigner{})
|
||||
|
||||
ctx2, cancel2 := context.WithCancel(context.Background())
|
||||
defer cancel2()
|
||||
err = q2.Start(ctx2)
|
||||
require.NoError(t, err)
|
||||
require.True(t, q2.IsRunning())
|
||||
q2.Stop()
|
||||
})
|
||||
|
||||
t.Run("health_status", func(t *testing.T) {
|
||||
q, err := NewQuasar(log.NewNoOpLogger(), 3, 2, 3)
|
||||
require.NoError(t, err)
|
||||
|
||||
// Check stats work without panic
|
||||
stats := q.Stats()
|
||||
require.NotNil(t, stats)
|
||||
})
|
||||
|
||||
t.Run("drain_finality_channel", func(t *testing.T) {
|
||||
validatorStates := generateValidatorStates(5)
|
||||
pchain := newMockPChainProvider(validatorStates)
|
||||
defer pchain.Close()
|
||||
|
||||
q, err := NewQuasar(log.NewNoOpLogger(), 3, 2, 3)
|
||||
require.NoError(t, err)
|
||||
|
||||
q.ConnectPChain(pchain)
|
||||
q.ConnectQuantumFallback(&mockQuantumSigner{})
|
||||
|
||||
ctx, cancel := context.WithCancel(context.Background())
|
||||
defer cancel()
|
||||
|
||||
err = q.Start(ctx)
|
||||
require.NoError(t, err)
|
||||
|
||||
// Get finality channel via Subscribe
|
||||
finCh := q.Subscribe()
|
||||
require.NotNil(t, finCh)
|
||||
|
||||
// Emit event
|
||||
event := createTestEvent(1, validatorStates)
|
||||
pchain.EmitFinality(event)
|
||||
|
||||
// Try to receive finality (with timeout)
|
||||
select {
|
||||
case finality := <-finCh:
|
||||
require.NotNil(t, finality)
|
||||
case <-time.After(100 * time.Millisecond):
|
||||
// May not receive if processing takes longer
|
||||
}
|
||||
|
||||
q.Stop()
|
||||
})
|
||||
}
|
||||
|
||||
// TestQuasarEdgeCases tests edge cases and error conditions
|
||||
func TestQuasarEdgeCases(t *testing.T) {
|
||||
t.Run("start_without_pchain", func(t *testing.T) {
|
||||
q, err := NewQuasar(log.NewNoOpLogger(), 3, 2, 3)
|
||||
require.NoError(t, err)
|
||||
|
||||
ctx, cancel := context.WithCancel(context.Background())
|
||||
defer cancel()
|
||||
|
||||
err = q.Start(ctx)
|
||||
// Should handle gracefully (either error or run without processing)
|
||||
if err == nil {
|
||||
q.Stop()
|
||||
}
|
||||
})
|
||||
|
||||
t.Run("start_without_quantum_fallback", func(t *testing.T) {
|
||||
validatorStates := generateValidatorStates(5)
|
||||
pchain := newMockPChainProvider(validatorStates)
|
||||
defer pchain.Close()
|
||||
|
||||
q, err := NewQuasar(log.NewNoOpLogger(), 3, 2, 3)
|
||||
require.NoError(t, err)
|
||||
|
||||
q.ConnectPChain(pchain)
|
||||
// No quantum fallback connected - Start requires it
|
||||
|
||||
ctx, cancel := context.WithCancel(context.Background())
|
||||
defer cancel()
|
||||
|
||||
err = q.Start(ctx)
|
||||
// Implementation requires Q-Chain (quantum fallback) to be connected
|
||||
require.Error(t, err, "Start should error without quantum fallback")
|
||||
})
|
||||
|
||||
t.Run("get_finality_nonexistent", func(t *testing.T) {
|
||||
q, err := NewQuasar(log.NewNoOpLogger(), 3, 2, 3)
|
||||
require.NoError(t, err)
|
||||
|
||||
var blockID ids.ID
|
||||
_, _ = rand.Read(blockID[:])
|
||||
|
||||
finality, found := q.GetFinality(blockID)
|
||||
require.False(t, found, "should not find nonexistent block")
|
||||
require.Nil(t, finality, "should return nil for nonexistent block")
|
||||
})
|
||||
|
||||
t.Run("verify_nil_finality", func(t *testing.T) {
|
||||
q, err := NewQuasar(log.NewNoOpLogger(), 3, 2, 3)
|
||||
require.NoError(t, err)
|
||||
|
||||
err = q.Verify(nil)
|
||||
require.Error(t, err, "should error on nil finality")
|
||||
})
|
||||
|
||||
t.Run("verify_empty_proofs", func(t *testing.T) {
|
||||
q, err := NewQuasar(log.NewNoOpLogger(), 3, 2, 3)
|
||||
require.NoError(t, err)
|
||||
|
||||
finality := &QuantumFinality{
|
||||
BLSProof: nil,
|
||||
CoronaProof: nil,
|
||||
TotalWeight: 1000,
|
||||
SignerWeight: 700,
|
||||
}
|
||||
|
||||
err = q.Verify(finality)
|
||||
require.Error(t, err, "should error on empty proofs")
|
||||
})
|
||||
|
||||
t.Run("verify_insufficient_weight", func(t *testing.T) {
|
||||
q, err := NewQuasar(log.NewNoOpLogger(), 3, 2, 3)
|
||||
require.NoError(t, err)
|
||||
|
||||
finality := &QuantumFinality{
|
||||
BLSProof: []byte("proof"),
|
||||
CoronaProof: []byte("proof"),
|
||||
TotalWeight: 1000,
|
||||
SignerWeight: 500, // Only 50%, needs 67%
|
||||
}
|
||||
|
||||
err = q.Verify(finality)
|
||||
require.Error(t, err, "should error on insufficient weight")
|
||||
})
|
||||
|
||||
t.Run("create_message_format", func(t *testing.T) {
|
||||
q, err := NewQuasar(log.NewNoOpLogger(), 3, 2, 3)
|
||||
require.NoError(t, err)
|
||||
|
||||
validatorStates := generateValidatorStates(3)
|
||||
event := createTestEvent(42, validatorStates)
|
||||
|
||||
msg := q.CreateMessage(event)
|
||||
require.NotEmpty(t, msg, "message should not be empty")
|
||||
// Message is binary format containing blockID and height
|
||||
// Just verify it's deterministic and non-empty
|
||||
msg2 := q.CreateMessage(event)
|
||||
require.Equal(t, msg, msg2, "message should be deterministic")
|
||||
})
|
||||
|
||||
t.Run("total_weight_calculation", func(t *testing.T) {
|
||||
q, err := NewQuasar(log.NewNoOpLogger(), 3, 2, 3)
|
||||
require.NoError(t, err)
|
||||
|
||||
validators := []ValidatorState{
|
||||
{Weight: 100, Active: true},
|
||||
{Weight: 200, Active: true},
|
||||
{Weight: 300, Active: false}, // Inactive
|
||||
{Weight: 400, Active: true},
|
||||
}
|
||||
|
||||
total := q.TotalWeight(validators)
|
||||
require.Equal(t, uint64(700), total, "should sum only active validator weights")
|
||||
})
|
||||
}
|
||||
@@ -1,195 +0,0 @@
|
||||
// Copyright (C) 2019-2025, Lux Industries Inc. All rights reserved.
|
||||
// See the file LICENSE for licensing terms.
|
||||
|
||||
//go:build !cgo
|
||||
|
||||
// Package quasar provides NTT operations for Corona consensus.
|
||||
// This file provides pure Go CPU implementation when CGO is not available.
|
||||
// All operations use the luxfi/lattice library which provides optimized
|
||||
// NTT implementations in pure Go.
|
||||
package quasar
|
||||
|
||||
import (
|
||||
"sync"
|
||||
"sync/atomic"
|
||||
|
||||
"github.com/luxfi/lattice/v7/ring"
|
||||
)
|
||||
|
||||
// NTTAccelerator provides NTT operations for Corona.
|
||||
// When CGO is disabled, this uses the pure Go lattice library
|
||||
// which provides optimized CPU-based NTT transforms.
|
||||
type NTTAccelerator struct {
|
||||
enabled bool
|
||||
stats NTTStats
|
||||
statsmu sync.RWMutex
|
||||
}
|
||||
|
||||
// NTTStats tracks NTT accelerator statistics.
|
||||
type NTTStats struct {
|
||||
Enabled bool
|
||||
Backend string
|
||||
TotalOps uint64
|
||||
GPUAvailable bool
|
||||
}
|
||||
|
||||
// NewNTTAccelerator creates a new NTT accelerator using pure Go lattice library.
|
||||
func NewNTTAccelerator() (*NTTAccelerator, error) {
|
||||
return &NTTAccelerator{
|
||||
enabled: true, // CPU implementation is always available
|
||||
stats: NTTStats{
|
||||
Enabled: true,
|
||||
Backend: "CPU (Pure Go)",
|
||||
},
|
||||
}, nil
|
||||
}
|
||||
|
||||
// IsEnabled returns true - CPU implementation is always available.
|
||||
func (g *NTTAccelerator) IsEnabled() bool {
|
||||
return true
|
||||
}
|
||||
|
||||
// Backend returns the backend name.
|
||||
func (g *NTTAccelerator) Backend() string {
|
||||
return "CPU (Pure Go lattice)"
|
||||
}
|
||||
|
||||
// NTTForward performs forward NTT on a polynomial using lattice library.
|
||||
func (g *NTTAccelerator) NTTForward(r *ring.Ring, poly ring.Poly) error {
|
||||
r.NTT(poly, poly)
|
||||
atomic.AddUint64(&g.stats.TotalOps, 1)
|
||||
return nil
|
||||
}
|
||||
|
||||
// NTTInverse performs inverse NTT on a polynomial using lattice library.
|
||||
func (g *NTTAccelerator) NTTInverse(r *ring.Ring, poly ring.Poly) error {
|
||||
r.INTT(poly, poly)
|
||||
atomic.AddUint64(&g.stats.TotalOps, 1)
|
||||
return nil
|
||||
}
|
||||
|
||||
// BatchNTTForward performs forward NTT on multiple polynomials.
|
||||
// Uses parallel processing for better performance on multi-core CPUs.
|
||||
func (g *NTTAccelerator) BatchNTTForward(r *ring.Ring, polys []ring.Poly) error {
|
||||
if len(polys) == 0 {
|
||||
return nil
|
||||
}
|
||||
|
||||
// For small batches, process sequentially
|
||||
if len(polys) < 8 {
|
||||
for _, poly := range polys {
|
||||
r.NTT(poly, poly)
|
||||
}
|
||||
atomic.AddUint64(&g.stats.TotalOps, uint64(len(polys)))
|
||||
return nil
|
||||
}
|
||||
|
||||
// For larger batches, use parallel processing
|
||||
var wg sync.WaitGroup
|
||||
numWorkers := 4
|
||||
chunkSize := (len(polys) + numWorkers - 1) / numWorkers
|
||||
|
||||
for i := 0; i < numWorkers; i++ {
|
||||
start := i * chunkSize
|
||||
end := start + chunkSize
|
||||
if end > len(polys) {
|
||||
end = len(polys)
|
||||
}
|
||||
if start >= end {
|
||||
break
|
||||
}
|
||||
|
||||
wg.Add(1)
|
||||
go func(batch []ring.Poly) {
|
||||
defer wg.Done()
|
||||
for _, poly := range batch {
|
||||
r.NTT(poly, poly)
|
||||
}
|
||||
}(polys[start:end])
|
||||
}
|
||||
wg.Wait()
|
||||
|
||||
atomic.AddUint64(&g.stats.TotalOps, uint64(len(polys)))
|
||||
return nil
|
||||
}
|
||||
|
||||
// BatchNTTInverse performs inverse NTT on multiple polynomials.
|
||||
// Uses parallel processing for better performance on multi-core CPUs.
|
||||
func (g *NTTAccelerator) BatchNTTInverse(r *ring.Ring, polys []ring.Poly) error {
|
||||
if len(polys) == 0 {
|
||||
return nil
|
||||
}
|
||||
|
||||
// For small batches, process sequentially
|
||||
if len(polys) < 8 {
|
||||
for _, poly := range polys {
|
||||
r.INTT(poly, poly)
|
||||
}
|
||||
atomic.AddUint64(&g.stats.TotalOps, uint64(len(polys)))
|
||||
return nil
|
||||
}
|
||||
|
||||
// For larger batches, use parallel processing
|
||||
var wg sync.WaitGroup
|
||||
numWorkers := 4
|
||||
chunkSize := (len(polys) + numWorkers - 1) / numWorkers
|
||||
|
||||
for i := 0; i < numWorkers; i++ {
|
||||
start := i * chunkSize
|
||||
end := start + chunkSize
|
||||
if end > len(polys) {
|
||||
end = len(polys)
|
||||
}
|
||||
if start >= end {
|
||||
break
|
||||
}
|
||||
|
||||
wg.Add(1)
|
||||
go func(batch []ring.Poly) {
|
||||
defer wg.Done()
|
||||
for _, poly := range batch {
|
||||
r.INTT(poly, poly)
|
||||
}
|
||||
}(polys[start:end])
|
||||
}
|
||||
wg.Wait()
|
||||
|
||||
atomic.AddUint64(&g.stats.TotalOps, uint64(len(polys)))
|
||||
return nil
|
||||
}
|
||||
|
||||
// PolyMul performs polynomial multiplication using Barrett reduction.
|
||||
func (g *NTTAccelerator) PolyMul(r *ring.Ring, a, b, out ring.Poly) error {
|
||||
r.MulCoeffsBarrett(a, b, out)
|
||||
atomic.AddUint64(&g.stats.TotalOps, 1)
|
||||
return nil
|
||||
}
|
||||
|
||||
// ClearCache is a no-op for CPU implementation (no GPU cache).
|
||||
func (g *NTTAccelerator) ClearCache() {}
|
||||
|
||||
// Stats returns current NTT accelerator statistics.
|
||||
func (g *NTTAccelerator) Stats() NTTStats {
|
||||
g.statsmu.RLock()
|
||||
defer g.statsmu.RUnlock()
|
||||
return NTTStats{
|
||||
Enabled: true,
|
||||
Backend: "CPU (Pure Go lattice)",
|
||||
TotalOps: atomic.LoadUint64(&g.stats.TotalOps),
|
||||
GPUAvailable: false, // CPU-only build
|
||||
}
|
||||
}
|
||||
|
||||
// Global accelerator instance
|
||||
var (
|
||||
globalNTTAccelerator *NTTAccelerator
|
||||
globalNTTAcceleratorOnce sync.Once
|
||||
)
|
||||
|
||||
// GetNTTAccelerator returns the global NTT accelerator instance.
|
||||
func GetNTTAccelerator() (*NTTAccelerator, error) {
|
||||
globalNTTAcceleratorOnce.Do(func() {
|
||||
globalNTTAccelerator, _ = NewNTTAccelerator()
|
||||
})
|
||||
return globalNTTAccelerator, nil
|
||||
}
|
||||
@@ -1,469 +0,0 @@
|
||||
// Copyright (C) 2019-2025, Lux Industries Inc. All rights reserved.
|
||||
// See the file LICENSE for licensing terms.
|
||||
|
||||
//go:build cgo
|
||||
|
||||
// Package quasar provides GPU-accelerated NTT operations for Corona consensus.
|
||||
// This uses the unified lux/accel package for GPU acceleration of lattice
|
||||
// operations in the Corona threshold signature protocol.
|
||||
//
|
||||
// GPU acceleration provides 40x+ speedup for NTT operations on Apple Silicon
|
||||
// and NVIDIA GPUs via the accel library (Metal/CUDA/CPU backends).
|
||||
//
|
||||
// Architecture:
|
||||
//
|
||||
// luxcpp/accel (C++ GPU) → lux/accel (Go CGO) → Quasar consensus
|
||||
//
|
||||
// This enables consistent GPU acceleration across:
|
||||
// - Corona threshold signatures
|
||||
// - ML-DSA post-quantum signatures
|
||||
// - FHE operations (via luxcpp/fhe which reuses luxcpp/lattice)
|
||||
package quasar
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"sync"
|
||||
"sync/atomic"
|
||||
|
||||
"github.com/luxfi/accel"
|
||||
"github.com/luxfi/lattice/v7/ring"
|
||||
"github.com/luxfi/node/config"
|
||||
)
|
||||
|
||||
// NTTAccelerator provides GPU-accelerated NTT operations for Corona.
|
||||
// It uses the unified lux/accel package for Metal/CUDA/CPU backends.
|
||||
type NTTAccelerator struct {
|
||||
mu sync.RWMutex
|
||||
session *accel.Session
|
||||
enabled bool
|
||||
totalOps uint64
|
||||
}
|
||||
|
||||
// NTTOptions holds options for creating an NTT accelerator.
|
||||
type NTTOptions struct {
|
||||
// Enabled controls whether GPU acceleration is used
|
||||
Enabled bool
|
||||
// Backend specifies which GPU backend to use: "auto", "metal", "cuda", "cpu"
|
||||
Backend string
|
||||
// DeviceIndex specifies which GPU device to use
|
||||
DeviceIndex int
|
||||
}
|
||||
|
||||
// NewNTTAccelerator creates a new NTT accelerator with GPU support.
|
||||
// It auto-detects available GPU backends (Metal on macOS, CUDA on Linux).
|
||||
func NewNTTAccelerator() (*NTTAccelerator, error) {
|
||||
return NewNTTAcceleratorWithOptions(NTTOptions{})
|
||||
}
|
||||
|
||||
// NewNTTAcceleratorWithOptions creates a new NTT accelerator with custom options.
|
||||
// If options are zero-valued, it uses the global GPU config.
|
||||
func NewNTTAcceleratorWithOptions(opts NTTOptions) (*NTTAccelerator, error) {
|
||||
// Get global config if options not specified
|
||||
gpuCfg := config.GetGlobalGPUConfig()
|
||||
|
||||
// Determine if GPU should be enabled
|
||||
enabled := gpuCfg.Enabled
|
||||
if opts.Backend == "cpu" {
|
||||
enabled = false
|
||||
}
|
||||
|
||||
// Check if GPU is available via accel library
|
||||
available := accel.Available() && enabled
|
||||
|
||||
var session *accel.Session
|
||||
if available {
|
||||
var err error
|
||||
session, err = accel.DefaultSession()
|
||||
if err != nil {
|
||||
// Fall back to CPU mode
|
||||
available = false
|
||||
}
|
||||
}
|
||||
|
||||
return &NTTAccelerator{
|
||||
session: session,
|
||||
enabled: available,
|
||||
}, nil
|
||||
}
|
||||
|
||||
// IsEnabled returns whether GPU acceleration is available.
|
||||
func (g *NTTAccelerator) IsEnabled() bool {
|
||||
g.mu.RLock()
|
||||
defer g.mu.RUnlock()
|
||||
return g.enabled
|
||||
}
|
||||
|
||||
// Backend returns the name of the active GPU backend.
|
||||
func (g *NTTAccelerator) Backend() string {
|
||||
g.mu.RLock()
|
||||
defer g.mu.RUnlock()
|
||||
|
||||
if !g.enabled || g.session == nil {
|
||||
return "CPU (GPU not available)"
|
||||
}
|
||||
return g.session.Backend().String()
|
||||
}
|
||||
|
||||
// getModulus extracts the first modulus from the ring.
|
||||
func (g *NTTAccelerator) getModulus(r *ring.Ring) (uint32, error) {
|
||||
if len(r.ModuliChain()) == 0 {
|
||||
return 0, fmt.Errorf("ring has no moduli")
|
||||
}
|
||||
return uint32(r.ModuliChain()[0]), nil
|
||||
}
|
||||
|
||||
// NTTForward performs forward NTT on a polynomial using GPU acceleration.
|
||||
// Falls back to CPU if GPU is not available.
|
||||
func (g *NTTAccelerator) NTTForward(r *ring.Ring, poly ring.Poly) error {
|
||||
if !g.enabled || g.session == nil {
|
||||
// Fall back to lattice library's NTT
|
||||
r.NTT(poly, poly)
|
||||
return nil
|
||||
}
|
||||
|
||||
N := r.N()
|
||||
coeffs := poly.Coeffs
|
||||
if len(coeffs) == 0 || len(coeffs[0]) < N {
|
||||
r.NTT(poly, poly)
|
||||
return nil
|
||||
}
|
||||
|
||||
Q, err := g.getModulus(r)
|
||||
if err != nil {
|
||||
r.NTT(poly, poly)
|
||||
return nil
|
||||
}
|
||||
|
||||
// Create input tensor from polynomial coefficients
|
||||
inputTensor, err := accel.NewTensorWithData[uint64](g.session, []int{N}, coeffs[0][:N])
|
||||
if err != nil {
|
||||
r.NTT(poly, poly)
|
||||
return nil
|
||||
}
|
||||
defer inputTensor.Close()
|
||||
|
||||
// Create output tensor
|
||||
outputTensor, err := accel.NewTensor[uint64](g.session, []int{N})
|
||||
if err != nil {
|
||||
r.NTT(poly, poly)
|
||||
return nil
|
||||
}
|
||||
defer outputTensor.Close()
|
||||
|
||||
// GPU NTT via accel Lattice ops
|
||||
if err := g.session.Lattice().PolynomialNTT(inputTensor.Untyped(), outputTensor.Untyped(), Q); err != nil {
|
||||
r.NTT(poly, poly)
|
||||
return nil
|
||||
}
|
||||
|
||||
// Copy result back
|
||||
result, err := outputTensor.ToSlice()
|
||||
if err != nil {
|
||||
r.NTT(poly, poly)
|
||||
return nil
|
||||
}
|
||||
copy(coeffs[0], result)
|
||||
atomic.AddUint64(&g.totalOps, 1)
|
||||
return nil
|
||||
}
|
||||
|
||||
// NTTInverse performs inverse NTT on a polynomial using GPU acceleration.
|
||||
// Falls back to CPU if GPU is not available.
|
||||
func (g *NTTAccelerator) NTTInverse(r *ring.Ring, poly ring.Poly) error {
|
||||
if !g.enabled || g.session == nil {
|
||||
// Fall back to lattice library's INTT
|
||||
r.INTT(poly, poly)
|
||||
return nil
|
||||
}
|
||||
|
||||
N := r.N()
|
||||
coeffs := poly.Coeffs
|
||||
if len(coeffs) == 0 || len(coeffs[0]) < N {
|
||||
r.INTT(poly, poly)
|
||||
return nil
|
||||
}
|
||||
|
||||
Q, err := g.getModulus(r)
|
||||
if err != nil {
|
||||
r.INTT(poly, poly)
|
||||
return nil
|
||||
}
|
||||
|
||||
// Create input tensor from polynomial coefficients
|
||||
inputTensor, err := accel.NewTensorWithData[uint64](g.session, []int{N}, coeffs[0][:N])
|
||||
if err != nil {
|
||||
r.INTT(poly, poly)
|
||||
return nil
|
||||
}
|
||||
defer inputTensor.Close()
|
||||
|
||||
// Create output tensor
|
||||
outputTensor, err := accel.NewTensor[uint64](g.session, []int{N})
|
||||
if err != nil {
|
||||
r.INTT(poly, poly)
|
||||
return nil
|
||||
}
|
||||
defer outputTensor.Close()
|
||||
|
||||
// GPU INTT via accel Lattice ops
|
||||
if err := g.session.Lattice().PolynomialINTT(inputTensor.Untyped(), outputTensor.Untyped(), Q); err != nil {
|
||||
r.INTT(poly, poly)
|
||||
return nil
|
||||
}
|
||||
|
||||
// Copy result back
|
||||
result, err := outputTensor.ToSlice()
|
||||
if err != nil {
|
||||
r.INTT(poly, poly)
|
||||
return nil
|
||||
}
|
||||
copy(coeffs[0], result)
|
||||
atomic.AddUint64(&g.totalOps, 1)
|
||||
return nil
|
||||
}
|
||||
|
||||
// BatchNTTForward performs forward NTT on multiple polynomials in parallel.
|
||||
// This is the primary use case for GPU acceleration - batch operations.
|
||||
func (g *NTTAccelerator) BatchNTTForward(r *ring.Ring, polys []ring.Poly) error {
|
||||
if len(polys) == 0 {
|
||||
return nil
|
||||
}
|
||||
|
||||
if !g.enabled || g.session == nil || len(polys) < 4 {
|
||||
// Fall back to CPU for small batches (GPU overhead not worth it)
|
||||
for i := range polys {
|
||||
r.NTT(polys[i], polys[i])
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
Q, err := g.getModulus(r)
|
||||
if err != nil {
|
||||
for i := range polys {
|
||||
r.NTT(polys[i], polys[i])
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
// Process each polynomial through GPU
|
||||
// Note: For true batch performance, we'd want batch tensor operations
|
||||
// but the current accel API operates on single polynomials
|
||||
N := r.N()
|
||||
for i := range polys {
|
||||
coeffs := polys[i].Coeffs
|
||||
if len(coeffs) == 0 || len(coeffs[0]) < N {
|
||||
r.NTT(polys[i], polys[i])
|
||||
continue
|
||||
}
|
||||
|
||||
inputTensor, err := accel.NewTensorWithData[uint64](g.session, []int{N}, coeffs[0][:N])
|
||||
if err != nil {
|
||||
r.NTT(polys[i], polys[i])
|
||||
continue
|
||||
}
|
||||
|
||||
outputTensor, err := accel.NewTensor[uint64](g.session, []int{N})
|
||||
if err != nil {
|
||||
inputTensor.Close()
|
||||
r.NTT(polys[i], polys[i])
|
||||
continue
|
||||
}
|
||||
|
||||
if err := g.session.Lattice().PolynomialNTT(inputTensor.Untyped(), outputTensor.Untyped(), Q); err != nil {
|
||||
inputTensor.Close()
|
||||
outputTensor.Close()
|
||||
r.NTT(polys[i], polys[i])
|
||||
continue
|
||||
}
|
||||
|
||||
result, err := outputTensor.ToSlice()
|
||||
if err != nil {
|
||||
inputTensor.Close()
|
||||
outputTensor.Close()
|
||||
r.NTT(polys[i], polys[i])
|
||||
continue
|
||||
}
|
||||
copy(coeffs[0], result)
|
||||
|
||||
inputTensor.Close()
|
||||
outputTensor.Close()
|
||||
}
|
||||
|
||||
atomic.AddUint64(&g.totalOps, uint64(len(polys)))
|
||||
return nil
|
||||
}
|
||||
|
||||
// BatchNTTInverse performs inverse NTT on multiple polynomials in parallel.
|
||||
func (g *NTTAccelerator) BatchNTTInverse(r *ring.Ring, polys []ring.Poly) error {
|
||||
if len(polys) == 0 {
|
||||
return nil
|
||||
}
|
||||
|
||||
if !g.enabled || g.session == nil || len(polys) < 4 {
|
||||
// Fall back to CPU for small batches
|
||||
for i := range polys {
|
||||
r.INTT(polys[i], polys[i])
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
Q, err := g.getModulus(r)
|
||||
if err != nil {
|
||||
for i := range polys {
|
||||
r.INTT(polys[i], polys[i])
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
// Process each polynomial through GPU
|
||||
N := r.N()
|
||||
for i := range polys {
|
||||
coeffs := polys[i].Coeffs
|
||||
if len(coeffs) == 0 || len(coeffs[0]) < N {
|
||||
r.INTT(polys[i], polys[i])
|
||||
continue
|
||||
}
|
||||
|
||||
inputTensor, err := accel.NewTensorWithData[uint64](g.session, []int{N}, coeffs[0][:N])
|
||||
if err != nil {
|
||||
r.INTT(polys[i], polys[i])
|
||||
continue
|
||||
}
|
||||
|
||||
outputTensor, err := accel.NewTensor[uint64](g.session, []int{N})
|
||||
if err != nil {
|
||||
inputTensor.Close()
|
||||
r.INTT(polys[i], polys[i])
|
||||
continue
|
||||
}
|
||||
|
||||
if err := g.session.Lattice().PolynomialINTT(inputTensor.Untyped(), outputTensor.Untyped(), Q); err != nil {
|
||||
inputTensor.Close()
|
||||
outputTensor.Close()
|
||||
r.INTT(polys[i], polys[i])
|
||||
continue
|
||||
}
|
||||
|
||||
result, err := outputTensor.ToSlice()
|
||||
if err != nil {
|
||||
inputTensor.Close()
|
||||
outputTensor.Close()
|
||||
r.INTT(polys[i], polys[i])
|
||||
continue
|
||||
}
|
||||
copy(coeffs[0], result)
|
||||
|
||||
inputTensor.Close()
|
||||
outputTensor.Close()
|
||||
}
|
||||
|
||||
atomic.AddUint64(&g.totalOps, uint64(len(polys)))
|
||||
return nil
|
||||
}
|
||||
|
||||
// PolyMul performs polynomial multiplication using GPU-accelerated NTT.
|
||||
// This multiplies polynomials a and b, storing result in out.
|
||||
func (g *NTTAccelerator) PolyMul(r *ring.Ring, a, b, out ring.Poly) error {
|
||||
if !g.enabled || g.session == nil {
|
||||
// Fall back to CPU
|
||||
r.MulCoeffsBarrett(a, b, out)
|
||||
return nil
|
||||
}
|
||||
|
||||
Q, err := g.getModulus(r)
|
||||
if err != nil {
|
||||
r.MulCoeffsBarrett(a, b, out)
|
||||
return nil
|
||||
}
|
||||
|
||||
N := r.N()
|
||||
|
||||
// Extract coefficients
|
||||
if len(a.Coeffs) == 0 || len(a.Coeffs[0]) < N ||
|
||||
len(b.Coeffs) == 0 || len(b.Coeffs[0]) < N ||
|
||||
len(out.Coeffs) == 0 || len(out.Coeffs[0]) < N {
|
||||
r.MulCoeffsBarrett(a, b, out)
|
||||
return nil
|
||||
}
|
||||
|
||||
// Create tensors for a, b, and output
|
||||
aTensor, err := accel.NewTensorWithData[uint64](g.session, []int{N}, a.Coeffs[0][:N])
|
||||
if err != nil {
|
||||
r.MulCoeffsBarrett(a, b, out)
|
||||
return nil
|
||||
}
|
||||
defer aTensor.Close()
|
||||
|
||||
bTensor, err := accel.NewTensorWithData[uint64](g.session, []int{N}, b.Coeffs[0][:N])
|
||||
if err != nil {
|
||||
r.MulCoeffsBarrett(a, b, out)
|
||||
return nil
|
||||
}
|
||||
defer bTensor.Close()
|
||||
|
||||
outTensor, err := accel.NewTensor[uint64](g.session, []int{N})
|
||||
if err != nil {
|
||||
r.MulCoeffsBarrett(a, b, out)
|
||||
return nil
|
||||
}
|
||||
defer outTensor.Close()
|
||||
|
||||
// GPU polynomial multiplication
|
||||
if err := g.session.Lattice().PolynomialMul(aTensor.Untyped(), bTensor.Untyped(), outTensor.Untyped(), Q); err != nil {
|
||||
r.MulCoeffsBarrett(a, b, out)
|
||||
return nil
|
||||
}
|
||||
|
||||
// Copy result back
|
||||
result, err := outTensor.ToSlice()
|
||||
if err != nil {
|
||||
r.MulCoeffsBarrett(a, b, out)
|
||||
return nil
|
||||
}
|
||||
copy(out.Coeffs[0], result)
|
||||
atomic.AddUint64(&g.totalOps, 1)
|
||||
return nil
|
||||
}
|
||||
|
||||
// ClearCache is a no-op in the accel-based implementation.
|
||||
// The accel library manages its own caching internally.
|
||||
func (g *NTTAccelerator) ClearCache() {
|
||||
// No-op: accel library manages caching internally
|
||||
}
|
||||
|
||||
// NTTStats returns NTT accelerator statistics.
|
||||
type NTTStats struct {
|
||||
Enabled bool
|
||||
Backend string
|
||||
TotalOps uint64
|
||||
GPUAvailable bool
|
||||
}
|
||||
|
||||
// Stats returns current NTT accelerator statistics.
|
||||
func (g *NTTAccelerator) Stats() NTTStats {
|
||||
g.mu.RLock()
|
||||
defer g.mu.RUnlock()
|
||||
|
||||
return NTTStats{
|
||||
Enabled: g.enabled,
|
||||
Backend: g.Backend(),
|
||||
TotalOps: atomic.LoadUint64(&g.totalOps),
|
||||
GPUAvailable: accel.Available(),
|
||||
}
|
||||
}
|
||||
|
||||
// Global NTT accelerator instance (lazily initialized)
|
||||
var (
|
||||
globalNTTAccelerator *NTTAccelerator
|
||||
globalNTTAcceleratorOnce sync.Once
|
||||
globalNTTAcceleratorErr error
|
||||
)
|
||||
|
||||
// GetNTTAccelerator returns the global NTT accelerator instance.
|
||||
// The accelerator is lazily initialized on first call.
|
||||
func GetNTTAccelerator() (*NTTAccelerator, error) {
|
||||
globalNTTAcceleratorOnce.Do(func() {
|
||||
globalNTTAccelerator, globalNTTAcceleratorErr = NewNTTAccelerator()
|
||||
})
|
||||
return globalNTTAccelerator, globalNTTAcceleratorErr
|
||||
}
|
||||
@@ -1,448 +0,0 @@
|
||||
// Copyright (C) 2019-2025, Lux Industries Inc. All rights reserved.
|
||||
// See the file LICENSE for licensing terms.
|
||||
|
||||
package quasar
|
||||
|
||||
import (
|
||||
"crypto/rand"
|
||||
"runtime"
|
||||
"sync"
|
||||
"testing"
|
||||
"time"
|
||||
|
||||
"github.com/luxfi/ids"
|
||||
"github.com/luxfi/log"
|
||||
"github.com/stretchr/testify/require"
|
||||
)
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// Test 1: Finalized map pruning
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
// TestFinalizedMapPruning simulates 100,000 finality events and verifies:
|
||||
// - The finalized map never exceeds maxFinalized + buffer
|
||||
// - Old entries are actually pruned
|
||||
// - After 100K events, map size <= 10,000
|
||||
func TestFinalizedMapPruning(t *testing.T) {
|
||||
q, err := NewQuasar(log.NewNoOpLogger(), 3, 2, 3)
|
||||
require.NoError(t, err)
|
||||
|
||||
// maxFinalized is 10,000 by default (set in NewQuasar)
|
||||
const totalEvents = 100_000
|
||||
|
||||
// We simulate processFinality's pruning logic directly by
|
||||
// inserting entries and triggering the prune path.
|
||||
// processFinality increments qHeight and prunes when len > maxFinalized.
|
||||
var peakSize int
|
||||
for i := 0; i < totalEvents; i++ {
|
||||
var blockID ids.ID
|
||||
_, _ = rand.Read(blockID[:])
|
||||
|
||||
q.mu.Lock()
|
||||
q.qHeight++
|
||||
q.finalized[blockID] = &QuantumFinality{
|
||||
BlockID: blockID,
|
||||
QChainHeight: q.qHeight,
|
||||
PChainHeight: uint64(i),
|
||||
TotalWeight: 1000,
|
||||
SignerWeight: 700,
|
||||
}
|
||||
|
||||
// Replicate the pruning logic from processFinality
|
||||
if q.maxFinalized > 0 && len(q.finalized) > q.maxFinalized {
|
||||
cutoff := q.qHeight - uint64(q.maxFinalized)
|
||||
for id, f := range q.finalized {
|
||||
if f.QChainHeight < cutoff {
|
||||
delete(q.finalized, id)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
size := len(q.finalized)
|
||||
if size > peakSize {
|
||||
peakSize = size
|
||||
}
|
||||
q.mu.Unlock()
|
||||
}
|
||||
|
||||
q.mu.RLock()
|
||||
finalSize := len(q.finalized)
|
||||
finalQHeight := q.qHeight
|
||||
q.mu.RUnlock()
|
||||
|
||||
t.Logf("totalEvents=%d peakSize=%d finalSize=%d qHeight=%d",
|
||||
totalEvents, peakSize, finalSize, finalQHeight)
|
||||
|
||||
// The pruning logic fires when len > maxFinalized, then deletes entries
|
||||
// with QChainHeight < cutoff (strict <). The cutoff is qHeight - maxFinalized.
|
||||
// After pruning, entries at exactly cutoff remain, so the steady-state
|
||||
// size is maxFinalized + 1. This is correct and bounded.
|
||||
require.LessOrEqual(t, peakSize, q.maxFinalized+1,
|
||||
"peak map size should not exceed maxFinalized+1")
|
||||
|
||||
require.LessOrEqual(t, finalSize, q.maxFinalized+1,
|
||||
"final map size should be <= maxFinalized+1 (10,001)")
|
||||
|
||||
// Verify old entries are actually gone: the oldest remaining entry
|
||||
// should have QChainHeight >= qHeight - maxFinalized.
|
||||
q.mu.RLock()
|
||||
minHeight := uint64(^uint64(0))
|
||||
for _, f := range q.finalized {
|
||||
if f.QChainHeight < minHeight {
|
||||
minHeight = f.QChainHeight
|
||||
}
|
||||
}
|
||||
q.mu.RUnlock()
|
||||
|
||||
expectedMinHeight := finalQHeight - uint64(q.maxFinalized)
|
||||
require.GreaterOrEqual(t, minHeight, expectedMinHeight,
|
||||
"oldest entry should be pruned: minHeight=%d expected>=%d", minHeight, expectedMinHeight)
|
||||
}
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// Test 2: Channel backpressure -- no goroutine leak or deadlock
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
// TestChannelBackpressure creates a pChainProvider with a 64-buffer channel,
|
||||
// sends 1000 events, and verifies no goroutine leak or deadlock.
|
||||
func TestChannelBackpressure(t *testing.T) {
|
||||
const (
|
||||
channelSize = 64
|
||||
totalEvents = 1000
|
||||
)
|
||||
|
||||
validators := generateValidatorStates(5)
|
||||
pchain := &mockPChainProvider{
|
||||
height: 0,
|
||||
validators: validators,
|
||||
finalityCh: make(chan FinalityEvent, channelSize),
|
||||
}
|
||||
|
||||
goroutinesBefore := runtime.NumGoroutine()
|
||||
|
||||
// Send events -- channel will fill up, excess events are dropped (select default)
|
||||
sent := 0
|
||||
for i := 0; i < totalEvents; i++ {
|
||||
event := createTestEvent(uint64(i+1), validators)
|
||||
select {
|
||||
case pchain.finalityCh <- event:
|
||||
sent++
|
||||
default:
|
||||
// Channel full -- expected backpressure behavior
|
||||
}
|
||||
}
|
||||
|
||||
t.Logf("sent %d/%d events (channel capacity %d)", sent, totalEvents, channelSize)
|
||||
require.GreaterOrEqual(t, sent, channelSize,
|
||||
"should have sent at least channelSize events")
|
||||
|
||||
// Drain the channel
|
||||
drained := 0
|
||||
for {
|
||||
select {
|
||||
case <-pchain.finalityCh:
|
||||
drained++
|
||||
default:
|
||||
goto done
|
||||
}
|
||||
}
|
||||
done:
|
||||
t.Logf("drained %d events", drained)
|
||||
|
||||
// Check goroutine count -- should not have leaked
|
||||
runtime.GC()
|
||||
goroutinesAfter := runtime.NumGoroutine()
|
||||
// Allow a delta of 5 for GC/runtime goroutines
|
||||
require.InDelta(t, goroutinesBefore, goroutinesAfter, 5,
|
||||
"goroutine count should not grow significantly: before=%d after=%d",
|
||||
goroutinesBefore, goroutinesAfter)
|
||||
}
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// Test 3: Long-running benchmark -- 1M simulated finality cycles
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
// BenchmarkQuasarLongRun runs 1M simulated finality cycles and reports
|
||||
// allocs/op, bytes/op, ns/op. Verifies no unbounded memory growth.
|
||||
func BenchmarkQuasarLongRun(b *testing.B) {
|
||||
q, err := NewQuasar(log.NewNoOpLogger(), 3, 2, 3)
|
||||
if err != nil {
|
||||
b.Fatal(err)
|
||||
}
|
||||
|
||||
// Snapshot heap before
|
||||
runtime.GC()
|
||||
var memBefore runtime.MemStats
|
||||
runtime.ReadMemStats(&memBefore)
|
||||
|
||||
b.ResetTimer()
|
||||
b.ReportAllocs()
|
||||
|
||||
for i := 0; i < b.N; i++ {
|
||||
var blockID ids.ID
|
||||
// Use deterministic IDs to avoid crypto/rand overhead in benchmark
|
||||
blockID[0] = byte(i)
|
||||
blockID[1] = byte(i >> 8)
|
||||
blockID[2] = byte(i >> 16)
|
||||
blockID[3] = byte(i >> 24)
|
||||
|
||||
q.mu.Lock()
|
||||
q.qHeight++
|
||||
q.finalized[blockID] = &QuantumFinality{
|
||||
BlockID: blockID,
|
||||
QChainHeight: q.qHeight,
|
||||
PChainHeight: uint64(i),
|
||||
TotalWeight: 1000,
|
||||
SignerWeight: 700,
|
||||
BLSProof: make([]byte, 96),
|
||||
Timestamp: time.Now(),
|
||||
}
|
||||
|
||||
// Prune (same logic as processFinality)
|
||||
if q.maxFinalized > 0 && len(q.finalized) > q.maxFinalized {
|
||||
cutoff := q.qHeight - uint64(q.maxFinalized)
|
||||
for id, f := range q.finalized {
|
||||
if f.QChainHeight < cutoff {
|
||||
delete(q.finalized, id)
|
||||
}
|
||||
}
|
||||
}
|
||||
q.mu.Unlock()
|
||||
}
|
||||
|
||||
b.StopTimer()
|
||||
|
||||
// Snapshot heap after
|
||||
runtime.GC()
|
||||
var memAfter runtime.MemStats
|
||||
runtime.ReadMemStats(&memAfter)
|
||||
|
||||
q.mu.RLock()
|
||||
finalSize := len(q.finalized)
|
||||
q.mu.RUnlock()
|
||||
|
||||
heapBefore := memBefore.HeapAlloc
|
||||
heapAfter := memAfter.HeapAlloc
|
||||
|
||||
var heapDelta int64
|
||||
if heapAfter >= heapBefore {
|
||||
heapDelta = int64(heapAfter - heapBefore)
|
||||
} else {
|
||||
heapDelta = -int64(heapBefore - heapAfter)
|
||||
}
|
||||
b.Logf("N=%d finalMapSize=%d heapBefore=%d heapAfter=%d heapDelta=%d",
|
||||
b.N, finalSize, heapBefore, heapAfter, heapDelta)
|
||||
|
||||
// Map should be bounded regardless of N
|
||||
if finalSize > q.maxFinalized+1 {
|
||||
b.Fatalf("unbounded growth: map size %d exceeds maxFinalized %d",
|
||||
finalSize, q.maxFinalized)
|
||||
}
|
||||
|
||||
// Heap should not grow linearly with N. After pruning, the heap should
|
||||
// be bounded by ~maxFinalized entries worth of allocations.
|
||||
// We allow 100MB as a generous upper bound for 10K entries with 96-byte proofs.
|
||||
const maxHeapGrowth = 100 * 1024 * 1024 // 100MB
|
||||
if heapAfter > heapBefore+maxHeapGrowth {
|
||||
b.Fatalf("unbounded heap growth: before=%d after=%d delta=%d (limit=%d)",
|
||||
heapBefore, heapAfter, heapAfter-heapBefore, maxHeapGrowth)
|
||||
}
|
||||
}
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// Test 4: Quorum math verification -- property test
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
// TestQuorumMath is a property test that for all validator counts 1-100
|
||||
// and all weight distributions:
|
||||
// - Cross-multiplication quorum never accepts < 2/3 weight
|
||||
// - Cross-multiplication quorum always accepts >= 2/3 weight (no false negatives)
|
||||
func TestQuorumMath(t *testing.T) {
|
||||
q, err := NewQuasar(log.NewNoOpLogger(), 3, 2, 3)
|
||||
require.NoError(t, err)
|
||||
|
||||
// The quorum check is: signerWeight * quorumDen >= totalWeight * quorumNum
|
||||
// With quorumNum=2, quorumDen=3: signerWeight * 3 >= totalWeight * 2
|
||||
|
||||
t.Run("no_false_positives", func(t *testing.T) {
|
||||
// For all validator counts and weight distributions, if the quorum
|
||||
// check passes, then signerWeight/totalWeight >= 2/3.
|
||||
for numValidators := 1; numValidators <= 100; numValidators++ {
|
||||
// Test with equal weights
|
||||
weightPerValidator := uint64(1000)
|
||||
totalWeight := uint64(numValidators) * weightPerValidator
|
||||
|
||||
for numSigners := 0; numSigners <= numValidators; numSigners++ {
|
||||
signerWeight := uint64(numSigners) * weightPerValidator
|
||||
result := q.CheckQuorum(signerWeight, totalWeight)
|
||||
|
||||
// Verify: if result is true, then signerWeight/totalWeight >= 2/3
|
||||
// Using cross-multiplication: signerWeight * 3 >= totalWeight * 2
|
||||
actualMeetsThreshold := signerWeight*3 >= totalWeight*2
|
||||
if result && !actualMeetsThreshold {
|
||||
t.Fatalf("FALSE POSITIVE: n=%d signers=%d sW=%d tW=%d: "+
|
||||
"quorum accepted but %d*3=%d < %d*2=%d",
|
||||
numValidators, numSigners, signerWeight, totalWeight,
|
||||
signerWeight, signerWeight*3, totalWeight, totalWeight*2)
|
||||
}
|
||||
}
|
||||
}
|
||||
})
|
||||
|
||||
t.Run("no_false_negatives", func(t *testing.T) {
|
||||
// For all validator counts and weight distributions, if
|
||||
// signerWeight/totalWeight >= 2/3, the quorum check must pass.
|
||||
for numValidators := 1; numValidators <= 100; numValidators++ {
|
||||
weightPerValidator := uint64(1000)
|
||||
totalWeight := uint64(numValidators) * weightPerValidator
|
||||
|
||||
for numSigners := 0; numSigners <= numValidators; numSigners++ {
|
||||
signerWeight := uint64(numSigners) * weightPerValidator
|
||||
result := q.CheckQuorum(signerWeight, totalWeight)
|
||||
|
||||
actualMeetsThreshold := signerWeight*3 >= totalWeight*2
|
||||
if actualMeetsThreshold && !result {
|
||||
t.Fatalf("FALSE NEGATIVE: n=%d signers=%d sW=%d tW=%d: "+
|
||||
"threshold met but quorum rejected",
|
||||
numValidators, numSigners, signerWeight, totalWeight)
|
||||
}
|
||||
}
|
||||
}
|
||||
})
|
||||
|
||||
t.Run("varied_weight_distributions", func(t *testing.T) {
|
||||
// Test with non-uniform weights: validators have weights 1..n
|
||||
for numValidators := 1; numValidators <= 100; numValidators++ {
|
||||
var totalWeight uint64
|
||||
weights := make([]uint64, numValidators)
|
||||
for i := 0; i < numValidators; i++ {
|
||||
weights[i] = uint64(i + 1)
|
||||
totalWeight += weights[i]
|
||||
}
|
||||
|
||||
// Test subsets: first k validators sign
|
||||
var signerWeight uint64
|
||||
for k := 0; k <= numValidators; k++ {
|
||||
if k > 0 {
|
||||
signerWeight += weights[k-1]
|
||||
}
|
||||
result := q.CheckQuorum(signerWeight, totalWeight)
|
||||
expected := signerWeight*3 >= totalWeight*2
|
||||
|
||||
if result != expected {
|
||||
t.Fatalf("MISMATCH: n=%d signers=%d sW=%d tW=%d: "+
|
||||
"got %v expected %v",
|
||||
numValidators, k, signerWeight, totalWeight,
|
||||
result, expected)
|
||||
}
|
||||
}
|
||||
}
|
||||
})
|
||||
|
||||
t.Run("edge_cases", func(t *testing.T) {
|
||||
// Zero total weight: any signer weight passes (vacuously true)
|
||||
require.True(t, q.CheckQuorum(0, 0), "0/0 should pass (vacuous)")
|
||||
require.True(t, q.CheckQuorum(1, 0), "1/0 should pass (vacuous)")
|
||||
|
||||
// Exact boundary: 2/3 of various totals
|
||||
// For totalWeight=3: need signerWeight >= 2
|
||||
require.True(t, q.CheckQuorum(2, 3), "2/3 should pass")
|
||||
require.False(t, q.CheckQuorum(1, 3), "1/3 should fail")
|
||||
|
||||
// For totalWeight=6: need signerWeight >= 4
|
||||
require.True(t, q.CheckQuorum(4, 6), "4/6 should pass")
|
||||
require.False(t, q.CheckQuorum(3, 6), "3/6 should fail")
|
||||
|
||||
// For totalWeight=9: need signerWeight >= 6
|
||||
require.True(t, q.CheckQuorum(6, 9), "6/9 should pass")
|
||||
require.False(t, q.CheckQuorum(5, 9), "5/9 should fail")
|
||||
|
||||
// For totalWeight=100: 2*100/3 = 66 (floor), so need 67 to be strictly >= 2/3
|
||||
// But cross-mult: sW*3 >= tW*2 → sW*3 >= 200 → sW >= 67 (ceil)
|
||||
// Actually: 66*3=198 < 200 → fail; 67*3=201 >= 200 → pass
|
||||
require.True(t, q.CheckQuorum(67, 100), "67/100 should pass")
|
||||
require.False(t, q.CheckQuorum(66, 100), "66/100 should fail")
|
||||
|
||||
// Large weights (near overflow boundary for uint64)
|
||||
// Safe for totalWeight < 2^62 with quorumNum=2 (per checkQuorum doc)
|
||||
largeTotal := uint64(1) << 61
|
||||
largeSigner := largeTotal*2/3 + 1
|
||||
require.True(t, q.CheckQuorum(largeSigner, largeTotal),
|
||||
"large weight should pass when above 2/3")
|
||||
})
|
||||
|
||||
t.Run("bft_threshold_exact", func(t *testing.T) {
|
||||
// BFT requires > 2/3 of total weight.
|
||||
// With the cross-multiplication check (>=), signerWeight*3 >= totalWeight*2.
|
||||
// This means exactly 2/3 PASSES (which matches the formal spec:
|
||||
// "quorum is met when SignerWeight/TotalWeight >= Numerator/Denominator").
|
||||
//
|
||||
// For totalWeight divisible by 3:
|
||||
// signerWeight = totalWeight * 2 / 3 → passes (exact 2/3)
|
||||
// signerWeight = totalWeight * 2 / 3 - 1 → fails (below 2/3)
|
||||
for total := uint64(3); total <= 300; total += 3 {
|
||||
threshold := total * 2 / 3
|
||||
require.True(t, q.CheckQuorum(threshold, total),
|
||||
"exact 2/3 (%d/%d) should pass", threshold, total)
|
||||
require.False(t, q.CheckQuorum(threshold-1, total),
|
||||
"below 2/3 (%d/%d) should fail", threshold-1, total)
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// Test 5: Concurrent map pruning stress test
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
// TestConcurrentPruningStress verifies that concurrent writes + pruning
|
||||
// do not corrupt the finalized map or deadlock.
|
||||
func TestConcurrentPruningStress(t *testing.T) {
|
||||
q, err := NewQuasar(log.NewNoOpLogger(), 3, 2, 3)
|
||||
require.NoError(t, err)
|
||||
|
||||
const (
|
||||
numWriters = 8
|
||||
opsPerWriter = 5000
|
||||
)
|
||||
|
||||
var wg sync.WaitGroup
|
||||
for w := 0; w < numWriters; w++ {
|
||||
wg.Add(1)
|
||||
go func(writerID int) {
|
||||
defer wg.Done()
|
||||
for i := 0; i < opsPerWriter; i++ {
|
||||
var blockID ids.ID
|
||||
blockID[0] = byte(writerID)
|
||||
blockID[1] = byte(i)
|
||||
blockID[2] = byte(i >> 8)
|
||||
|
||||
q.mu.Lock()
|
||||
q.qHeight++
|
||||
q.finalized[blockID] = &QuantumFinality{
|
||||
BlockID: blockID,
|
||||
QChainHeight: q.qHeight,
|
||||
}
|
||||
if q.maxFinalized > 0 && len(q.finalized) > q.maxFinalized {
|
||||
cutoff := q.qHeight - uint64(q.maxFinalized)
|
||||
for id, f := range q.finalized {
|
||||
if f.QChainHeight < cutoff {
|
||||
delete(q.finalized, id)
|
||||
}
|
||||
}
|
||||
}
|
||||
q.mu.Unlock()
|
||||
}
|
||||
}(w)
|
||||
}
|
||||
wg.Wait()
|
||||
|
||||
q.mu.RLock()
|
||||
finalSize := len(q.finalized)
|
||||
q.mu.RUnlock()
|
||||
|
||||
t.Logf("writers=%d opsEach=%d totalOps=%d finalSize=%d",
|
||||
numWriters, opsPerWriter, numWriters*opsPerWriter, finalSize)
|
||||
|
||||
require.LessOrEqual(t, finalSize, q.maxFinalized+1,
|
||||
"map size should be bounded after concurrent stress")
|
||||
}
|
||||
@@ -1,681 +0,0 @@
|
||||
// Copyright (C) 2019-2025, Lux Industries Inc. All rights reserved.
|
||||
// See the file LICENSE for licensing terms.
|
||||
|
||||
package quasar
|
||||
|
||||
import (
|
||||
"context"
|
||||
"errors"
|
||||
"fmt"
|
||||
"sync"
|
||||
"time"
|
||||
|
||||
"github.com/luxfi/consensus/protocol/quasar"
|
||||
"github.com/luxfi/ids"
|
||||
"github.com/luxfi/log"
|
||||
)
|
||||
|
||||
// Quasar is the gravitational center of Lux consensus.
|
||||
// It binds P-Chain (BLS signatures) and Q-Chain (Corona post-quantum threshold)
|
||||
// into unified hybrid finality across all Lux networks.
|
||||
//
|
||||
// Architecture:
|
||||
// ALL validators have BOTH keypairs:
|
||||
// - BLS keypair → aggregate signatures (classical, fast)
|
||||
// - Corona keypair → threshold signatures (post-quantum, 2-round)
|
||||
//
|
||||
// Both signature paths run IN PARALLEL:
|
||||
//
|
||||
// Block arrives
|
||||
// │
|
||||
// ├─────────────────────────────────────────┐
|
||||
// │ │
|
||||
// ▼ ▼
|
||||
// BLS PATH (fast) CORONA PATH (quantum-safe)
|
||||
// ──────────────── ─────────────────────────────
|
||||
// All validators sign Round 1: All validators
|
||||
// with BLS keys generate commitments
|
||||
// │ │
|
||||
// ▼ ▼
|
||||
// Aggregate into Round 2: All validators
|
||||
// single 96-byte sig compute partial signatures
|
||||
// │ │
|
||||
// └─────────────────┬───────────────────────┘
|
||||
// │
|
||||
// ▼
|
||||
// Finalize: combine into
|
||||
// threshold signature
|
||||
// │
|
||||
// ▼
|
||||
// ┌─────────────────┐
|
||||
// │ HYBRID PROOF │
|
||||
// │ BLS Aggregate │ ← 96 bytes (2/3+ validators)
|
||||
// │ Corona Thresh │ ← ~KB (t-of-n threshold)
|
||||
// └─────────────────┘
|
||||
// │
|
||||
// ▼
|
||||
// QUANTUM FINALITY
|
||||
//
|
||||
// The quasar ensures blocks achieve finality only when BOTH complete:
|
||||
// 1. 2/3+ validator weight signed via BLS (fast, classical)
|
||||
// 2. t-of-n validators completed Corona threshold (post-quantum secure)
|
||||
|
||||
var (
|
||||
ErrQuasarNotStarted = errors.New("quasar not started")
|
||||
ErrPChainNotConnected = errors.New("P-Chain not connected")
|
||||
ErrQChainNotConnected = errors.New("Q-Chain not connected")
|
||||
ErrCoronaNotConnected = errors.New("Corona coordinator not connected")
|
||||
ErrInsufficientWeight = errors.New("insufficient validator weight")
|
||||
ErrInsufficientSigners = errors.New("insufficient Corona signers")
|
||||
ErrFinalityFailed = errors.New("hybrid finality verification failed")
|
||||
ErrBLSFailed = errors.New("BLS aggregation failed")
|
||||
ErrCoronaFailed = errors.New("Corona threshold signing failed")
|
||||
)
|
||||
|
||||
// PChainProvider provides P-Chain state and finality events
|
||||
type PChainProvider interface {
|
||||
GetFinalizedHeight() uint64
|
||||
GetValidators(height uint64) ([]ValidatorState, error)
|
||||
SubscribeFinality() <-chan FinalityEvent
|
||||
}
|
||||
|
||||
// QuantumSignerFallback provides fallback single-signer quantum signatures
|
||||
type QuantumSignerFallback interface {
|
||||
SignMessage(msg []byte) ([]byte, error)
|
||||
}
|
||||
|
||||
// ValidatorState represents a validator's current state
|
||||
// Each validator has BOTH BLS and Corona keys
|
||||
type ValidatorState struct {
|
||||
NodeID ids.NodeID
|
||||
Weight uint64
|
||||
BLSPubKey []byte // BLS public key for aggregate signatures
|
||||
CoronaKey []byte // Corona public key share for threshold sigs
|
||||
Active bool
|
||||
}
|
||||
|
||||
// FinalityEvent represents a P-Chain finality event
|
||||
type FinalityEvent struct {
|
||||
Height uint64
|
||||
BlockID ids.ID
|
||||
Validators []ValidatorState
|
||||
Timestamp time.Time
|
||||
}
|
||||
|
||||
// QuantumFinality represents a block that achieved hybrid quantum finality
|
||||
type QuantumFinality struct {
|
||||
BlockID ids.ID
|
||||
PChainHeight uint64
|
||||
QChainHeight uint64
|
||||
BLSProof []byte // Aggregated BLS signature (96 bytes)
|
||||
CoronaProof []byte // Serialized Corona threshold signature
|
||||
SignerBitset []byte // Which validators signed BLS
|
||||
CoronaSigners []ids.NodeID // Which validators participated in Corona
|
||||
TotalWeight uint64
|
||||
SignerWeight uint64
|
||||
BLSLatency time.Duration
|
||||
CoronaLatency time.Duration
|
||||
Timestamp time.Time
|
||||
}
|
||||
|
||||
// Quasar binds P-Chain and Q-Chain consensus into hybrid quantum finality
|
||||
type Quasar struct {
|
||||
mu sync.RWMutex
|
||||
|
||||
log log.Logger
|
||||
core *quasar.Quasar
|
||||
|
||||
// Chain connections
|
||||
pChain PChainProvider
|
||||
quantumFallback QuantumSignerFallback
|
||||
|
||||
// Corona threshold coordinator
|
||||
corona *CoronaCoordinator
|
||||
|
||||
// State
|
||||
pHeight uint64
|
||||
qHeight uint64
|
||||
finalized map[ids.ID]*QuantumFinality
|
||||
|
||||
// Configuration
|
||||
threshold int // Corona threshold (t in t-of-n)
|
||||
quorumNum uint64 // BLS quorum numerator
|
||||
quorumDen uint64 // BLS quorum denominator
|
||||
maxFinalized int // max finalized entries before pruning
|
||||
|
||||
// Channels
|
||||
finalityCh chan *QuantumFinality
|
||||
stopCh chan struct{}
|
||||
running bool
|
||||
}
|
||||
|
||||
// NewQuasar creates a new Quasar consensus hub
|
||||
func NewQuasar(log log.Logger, threshold int, quorumNum, quorumDen uint64) (*Quasar, error) {
|
||||
core, err := quasar.NewQuasar(threshold)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("failed to create quasar core: %w", err)
|
||||
}
|
||||
|
||||
return &Quasar{
|
||||
log: log,
|
||||
core: core,
|
||||
threshold: threshold,
|
||||
quorumNum: quorumNum,
|
||||
quorumDen: quorumDen,
|
||||
finalized: make(map[ids.ID]*QuantumFinality),
|
||||
maxFinalized: 10000,
|
||||
finalityCh: make(chan *QuantumFinality, 100),
|
||||
stopCh: make(chan struct{}),
|
||||
}, nil
|
||||
}
|
||||
|
||||
// ConnectPChain connects the P-Chain finality provider
|
||||
func (q *Quasar) ConnectPChain(p PChainProvider) {
|
||||
q.mu.Lock()
|
||||
defer q.mu.Unlock()
|
||||
|
||||
q.pChain = p
|
||||
if p != nil {
|
||||
q.pHeight = p.GetFinalizedHeight()
|
||||
}
|
||||
|
||||
q.log.Info("quasar: P-Chain connected", "height", q.pHeight)
|
||||
}
|
||||
|
||||
// ConnectQuantumFallback connects the quantum signer fallback
|
||||
func (q *Quasar) ConnectQuantumFallback(f QuantumSignerFallback) {
|
||||
q.mu.Lock()
|
||||
defer q.mu.Unlock()
|
||||
|
||||
q.quantumFallback = f
|
||||
q.log.Info("quasar: quantum fallback connected")
|
||||
}
|
||||
|
||||
// ConnectCorona connects the Corona threshold coordinator
|
||||
func (q *Quasar) ConnectCorona(rc *CoronaCoordinator) {
|
||||
q.mu.Lock()
|
||||
defer q.mu.Unlock()
|
||||
|
||||
q.corona = rc
|
||||
q.log.Info("quasar: Corona coordinator connected")
|
||||
}
|
||||
|
||||
// InitializeCorona initializes the Corona coordinator with validators
|
||||
func (q *Quasar) InitializeCorona(validators []ids.NodeID) error {
|
||||
q.mu.Lock()
|
||||
defer q.mu.Unlock()
|
||||
|
||||
if q.corona == nil {
|
||||
// Create coordinator if not provided
|
||||
numParties := len(validators)
|
||||
threshold := (numParties * 2 / 3) + 1 // 2/3 + 1 threshold
|
||||
if threshold < 2 {
|
||||
threshold = 2
|
||||
}
|
||||
|
||||
rc, err := NewCoronaCoordinator(q.log, CoronaConfig{
|
||||
NumParties: numParties,
|
||||
Threshold: threshold,
|
||||
})
|
||||
if err != nil {
|
||||
return fmt.Errorf("failed to create Corona coordinator: %w", err)
|
||||
}
|
||||
q.corona = rc
|
||||
}
|
||||
|
||||
if err := q.corona.Initialize(validators); err != nil {
|
||||
return fmt.Errorf("failed to initialize Corona: %w", err)
|
||||
}
|
||||
|
||||
q.log.Info("quasar: Corona initialized",
|
||||
"validators", len(validators),
|
||||
"threshold", q.corona.Stats().Threshold,
|
||||
)
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
// Start begins the quasar consensus loop
|
||||
func (q *Quasar) Start(ctx context.Context) error {
|
||||
q.mu.Lock()
|
||||
if q.pChain == nil {
|
||||
q.mu.Unlock()
|
||||
return ErrPChainNotConnected
|
||||
}
|
||||
if q.quantumFallback == nil {
|
||||
q.mu.Unlock()
|
||||
return ErrQChainNotConnected
|
||||
}
|
||||
q.running = true
|
||||
q.mu.Unlock()
|
||||
|
||||
// Subscribe to P-Chain finality
|
||||
sub := q.pChain.SubscribeFinality()
|
||||
go q.run(ctx, sub)
|
||||
|
||||
q.log.Info("quasar: started")
|
||||
return nil
|
||||
}
|
||||
|
||||
// Stop halts the quasar
|
||||
func (q *Quasar) Stop() {
|
||||
q.mu.Lock()
|
||||
if q.running {
|
||||
close(q.stopCh)
|
||||
q.running = false
|
||||
}
|
||||
q.mu.Unlock()
|
||||
q.log.Info("quasar: stopped")
|
||||
}
|
||||
|
||||
// run is the main finality loop.
|
||||
// Bounded by ctx.Done() and q.stopCh — exits when either fires.
|
||||
// The goroutine is started in Start() and guaranteed to terminate
|
||||
// when Stop() closes stopCh or the parent context is cancelled.
|
||||
func (q *Quasar) run(ctx context.Context, sub <-chan FinalityEvent) {
|
||||
for {
|
||||
select {
|
||||
case <-ctx.Done():
|
||||
return
|
||||
case <-q.stopCh:
|
||||
return
|
||||
case event := <-sub:
|
||||
if err := q.processFinality(ctx, event); err != nil {
|
||||
q.log.Error("quasar: finality failed",
|
||||
"height", event.Height,
|
||||
"error", err,
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// processFinality processes a P-Chain finality event into hybrid finality
|
||||
// Both BLS and Corona paths run IN PARALLEL
|
||||
func (q *Quasar) processFinality(ctx context.Context, event FinalityEvent) error {
|
||||
q.mu.Lock()
|
||||
defer q.mu.Unlock()
|
||||
|
||||
// Sync validators to quasar core
|
||||
for _, v := range event.Validators {
|
||||
if v.Active {
|
||||
_, _ = q.core.AddValidator(v.NodeID.String(), v.Weight)
|
||||
}
|
||||
}
|
||||
|
||||
// Create finality message
|
||||
msg := q.createMessage(event)
|
||||
msgStr := string(msg) // Corona uses string message
|
||||
|
||||
// Run BLS and Corona IN PARALLEL
|
||||
var blsProof, signerBitset []byte
|
||||
var signerWeight uint64
|
||||
var coronaSig Signature
|
||||
var blsLatency, coronaLatency time.Duration
|
||||
var blsErr, coronaErr error
|
||||
var wg sync.WaitGroup
|
||||
|
||||
// BLS path
|
||||
wg.Add(1)
|
||||
go func() {
|
||||
defer wg.Done()
|
||||
start := time.Now()
|
||||
blsProof, signerBitset, signerWeight, blsErr = q.collectBLS(event, msg)
|
||||
blsLatency = time.Since(start)
|
||||
}()
|
||||
|
||||
// Corona path - REQUIRED for Q-Chain validator consensus.
|
||||
// No fallback mode: if Corona coordinator is not initialized,
|
||||
// finality MUST fail to prevent accepting BLS-only proofs.
|
||||
wg.Add(1)
|
||||
go func() {
|
||||
defer wg.Done()
|
||||
if q.corona == nil || !q.corona.IsInitialized() {
|
||||
// STRICT: No fallback allowed for validators.
|
||||
// RT signatures are REQUIRED for quantum-safe consensus.
|
||||
coronaErr = ErrCoronaNotConnected
|
||||
return
|
||||
}
|
||||
// Full threshold signing
|
||||
start := time.Now()
|
||||
coronaSig, coronaErr = q.collectCorona(msgStr)
|
||||
coronaLatency = time.Since(start)
|
||||
}()
|
||||
|
||||
wg.Wait()
|
||||
|
||||
// Check BLS result
|
||||
if blsErr != nil {
|
||||
return fmt.Errorf("BLS collection: %w", blsErr)
|
||||
}
|
||||
|
||||
// Check Corona result
|
||||
if coronaErr != nil {
|
||||
return fmt.Errorf("Corona threshold: %w", coronaErr)
|
||||
}
|
||||
|
||||
// Check quorum
|
||||
totalWeight := q.totalWeight(event.Validators)
|
||||
if !q.checkQuorum(signerWeight, totalWeight) {
|
||||
return ErrInsufficientWeight
|
||||
}
|
||||
|
||||
// Record finality
|
||||
q.qHeight++
|
||||
var coronaSigners []ids.NodeID
|
||||
var coronaProof []byte
|
||||
if coronaSig != nil {
|
||||
coronaSigners = coronaSig.Signers()
|
||||
coronaProof = coronaSig.Bytes()
|
||||
}
|
||||
finality := &QuantumFinality{
|
||||
BlockID: event.BlockID,
|
||||
PChainHeight: event.Height,
|
||||
QChainHeight: q.qHeight,
|
||||
BLSProof: blsProof,
|
||||
CoronaProof: coronaProof,
|
||||
SignerBitset: signerBitset,
|
||||
CoronaSigners: coronaSigners,
|
||||
TotalWeight: totalWeight,
|
||||
SignerWeight: signerWeight,
|
||||
BLSLatency: blsLatency,
|
||||
CoronaLatency: coronaLatency,
|
||||
Timestamp: time.Now(),
|
||||
}
|
||||
|
||||
q.finalized[event.BlockID] = finality
|
||||
q.pHeight = event.Height
|
||||
|
||||
// Prune old finality entries to bound memory
|
||||
if q.maxFinalized > 0 && len(q.finalized) > q.maxFinalized {
|
||||
cutoff := q.qHeight - uint64(q.maxFinalized)
|
||||
for id, f := range q.finalized {
|
||||
if f.QChainHeight < cutoff {
|
||||
delete(q.finalized, id)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Emit
|
||||
select {
|
||||
case q.finalityCh <- finality:
|
||||
default:
|
||||
}
|
||||
|
||||
q.log.Info("quasar: hybrid finality achieved",
|
||||
"block", event.BlockID,
|
||||
"pHeight", event.Height,
|
||||
"qHeight", q.qHeight,
|
||||
"weight", fmt.Sprintf("%d/%d", signerWeight, totalWeight),
|
||||
"blsLatency", blsLatency,
|
||||
"coronaLatency", coronaLatency,
|
||||
"coronaSigners", len(coronaSigners),
|
||||
)
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
// createMessage creates the finality message to sign
|
||||
func (q *Quasar) createMessage(event FinalityEvent) []byte {
|
||||
msg := make([]byte, 48) // 32 (blockID) + 8 (height) + 8 (timestamp)
|
||||
copy(msg[:32], event.BlockID[:])
|
||||
putUint64BE(msg[32:40], event.Height)
|
||||
putUint64BE(msg[40:48], uint64(event.Timestamp.UnixNano()))
|
||||
return msg
|
||||
}
|
||||
|
||||
// collectBLS collects BLS signatures from validators and aggregates them
|
||||
func (q *Quasar) collectBLS(event FinalityEvent, msg []byte) ([]byte, []byte, uint64, error) {
|
||||
var signerBitset []byte
|
||||
var signerWeight uint64
|
||||
signatures := make([]*quasar.QuasarSig, 0, len(event.Validators))
|
||||
|
||||
for i, v := range event.Validators {
|
||||
if !v.Active {
|
||||
continue
|
||||
}
|
||||
|
||||
sig, err := q.core.SignMessage(v.NodeID.String(), msg)
|
||||
if err != nil {
|
||||
continue // Skip failed signers
|
||||
}
|
||||
|
||||
signatures = append(signatures, sig)
|
||||
signerWeight += v.Weight
|
||||
|
||||
// Set bit
|
||||
byteIdx := i / 8
|
||||
for len(signerBitset) <= byteIdx {
|
||||
signerBitset = append(signerBitset, 0)
|
||||
}
|
||||
signerBitset[byteIdx] |= 1 << uint(i%8)
|
||||
}
|
||||
|
||||
if len(signatures) == 0 {
|
||||
return nil, nil, 0, errors.New("no BLS signatures")
|
||||
}
|
||||
|
||||
agg, err := q.core.AggregateSignatures(msg, signatures)
|
||||
if err != nil {
|
||||
return nil, nil, 0, err
|
||||
}
|
||||
|
||||
return agg.BLSAggregated, signerBitset, signerWeight, nil
|
||||
}
|
||||
|
||||
// collectCorona runs the 2-round Corona threshold protocol in parallel
|
||||
func (q *Quasar) collectCorona(message string) (Signature, error) {
|
||||
if q.corona == nil {
|
||||
return nil, ErrCoronaNotConnected
|
||||
}
|
||||
|
||||
// Use the high-level Sign API which handles all rounds internally
|
||||
sig, err := q.corona.Sign([]byte(message))
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("corona signing failed: %w", err)
|
||||
}
|
||||
|
||||
// Verify the signature
|
||||
if !q.corona.Verify([]byte(message), sig) {
|
||||
return nil, ErrCoronaFailed
|
||||
}
|
||||
|
||||
q.log.Debug("corona signature complete",
|
||||
"signers", len(sig.Signers()),
|
||||
"type", sig.Type(),
|
||||
)
|
||||
|
||||
return sig, nil
|
||||
}
|
||||
|
||||
// totalWeight calculates total validator weight
|
||||
func (q *Quasar) totalWeight(validators []ValidatorState) uint64 {
|
||||
var total uint64
|
||||
for _, v := range validators {
|
||||
if v.Active {
|
||||
total += v.Weight
|
||||
}
|
||||
}
|
||||
return total
|
||||
}
|
||||
|
||||
// checkQuorum verifies quorum is met using cross-multiplication to avoid
|
||||
// integer division truncation.
|
||||
//
|
||||
// signerWeight / totalWeight >= quorumNum / quorumDen
|
||||
// is equivalent to:
|
||||
// signerWeight * quorumDen >= totalWeight * quorumNum
|
||||
//
|
||||
// Overflow bound: safe for totalWeight < 2^62 with quorumNum <= 3.
|
||||
// Production values: totalWeight is sum of validator weights (well under 2^60),
|
||||
// quorumNum=2, quorumDen=3.
|
||||
func (q *Quasar) checkQuorum(signerWeight, totalWeight uint64) bool {
|
||||
return signerWeight*q.quorumDen >= totalWeight*q.quorumNum
|
||||
}
|
||||
|
||||
// GetFinality returns finality for a block
|
||||
func (q *Quasar) GetFinality(blockID ids.ID) (*QuantumFinality, bool) {
|
||||
q.mu.RLock()
|
||||
defer q.mu.RUnlock()
|
||||
f, ok := q.finalized[blockID]
|
||||
return f, ok
|
||||
}
|
||||
|
||||
// Subscribe returns channel for finality events
|
||||
func (q *Quasar) Subscribe() <-chan *QuantumFinality {
|
||||
return q.finalityCh
|
||||
}
|
||||
|
||||
// Verify verifies a hybrid finality proof.
|
||||
// Both BLS and Corona proofs are REQUIRED - no fallback mode.
|
||||
// This ensures quantum-safe consensus for Q-Chain validators.
|
||||
func (q *Quasar) Verify(finality *QuantumFinality) error {
|
||||
if finality == nil {
|
||||
return ErrFinalityFailed
|
||||
}
|
||||
|
||||
// STRICT: Both proofs are REQUIRED
|
||||
if len(finality.BLSProof) == 0 {
|
||||
return fmt.Errorf("%w: BLS proof missing", ErrBLSFailed)
|
||||
}
|
||||
if len(finality.CoronaProof) == 0 {
|
||||
return fmt.Errorf("%w: RT proof missing - required for Q-Chain validators", ErrCoronaFailed)
|
||||
}
|
||||
|
||||
if !q.checkQuorum(finality.SignerWeight, finality.TotalWeight) {
|
||||
return ErrInsufficientWeight
|
||||
}
|
||||
|
||||
// Verify BLS via hybrid engine
|
||||
agg := &quasar.AggregatedSignature{
|
||||
BLSAggregated: finality.BLSProof,
|
||||
}
|
||||
|
||||
// Reconstruct message for verification
|
||||
msg := make([]byte, 48)
|
||||
copy(msg[:32], finality.BlockID[:])
|
||||
putUint64BE(msg[32:40], finality.PChainHeight)
|
||||
putUint64BE(msg[40:48], uint64(finality.Timestamp.UnixNano()))
|
||||
|
||||
if !q.core.VerifyAggregatedSignature(msg, agg) {
|
||||
return ErrBLSFailed
|
||||
}
|
||||
|
||||
// Verify Corona threshold signature
|
||||
// RT signatures MUST have the "RT" prefix marker followed by threshold data
|
||||
if len(finality.CoronaProof) < 3 {
|
||||
return fmt.Errorf("%w: RT proof too short", ErrCoronaFailed)
|
||||
}
|
||||
if finality.CoronaProof[0] != 'R' || finality.CoronaProof[1] != 'T' {
|
||||
return fmt.Errorf("%w: invalid RT proof marker", ErrCoronaFailed)
|
||||
}
|
||||
|
||||
// Verify threshold signers meet minimum requirement
|
||||
if len(finality.CoronaSigners) < q.threshold {
|
||||
return fmt.Errorf("%w: need %d signers, have %d",
|
||||
ErrInsufficientSigners, q.threshold, len(finality.CoronaSigners))
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
// Stats returns quasar statistics
|
||||
func (q *Quasar) Stats() QuasarStats {
|
||||
q.mu.RLock()
|
||||
defer q.mu.RUnlock()
|
||||
|
||||
var coronaStats CoronaStats
|
||||
if q.corona != nil {
|
||||
coronaStats = q.corona.Stats()
|
||||
}
|
||||
|
||||
return QuasarStats{
|
||||
PChainHeight: q.pHeight,
|
||||
QChainHeight: q.qHeight,
|
||||
FinalizedBlocks: len(q.finalized),
|
||||
Threshold: q.threshold,
|
||||
QuorumNum: q.quorumNum,
|
||||
QuorumDen: q.quorumDen,
|
||||
Running: q.running,
|
||||
CoronaParties: coronaStats.NumParties,
|
||||
CoronaThreshold: coronaStats.Threshold,
|
||||
CoronaReady: coronaStats.Initialized,
|
||||
}
|
||||
}
|
||||
|
||||
// QuasarStats contains quasar statistics
|
||||
type QuasarStats struct {
|
||||
PChainHeight uint64
|
||||
QChainHeight uint64
|
||||
FinalizedBlocks int
|
||||
Threshold int
|
||||
QuorumNum uint64
|
||||
QuorumDen uint64
|
||||
Running bool
|
||||
CoronaParties int
|
||||
CoronaThreshold int
|
||||
CoronaReady bool
|
||||
}
|
||||
|
||||
// GetCore returns the underlying quasar core for testing
|
||||
func (q *Quasar) GetCore() *quasar.Quasar {
|
||||
return q.core
|
||||
}
|
||||
|
||||
// GetCorona returns the Corona coordinator
|
||||
func (q *Quasar) GetCorona() *CoronaCoordinator {
|
||||
q.mu.RLock()
|
||||
defer q.mu.RUnlock()
|
||||
return q.corona
|
||||
}
|
||||
|
||||
// CheckQuorum verifies quorum is met (exported for testing)
|
||||
func (q *Quasar) CheckQuorum(signerWeight, totalWeight uint64) bool {
|
||||
return q.checkQuorum(signerWeight, totalWeight)
|
||||
}
|
||||
|
||||
// CreateMessage creates the finality message to sign (exported for testing)
|
||||
func (q *Quasar) CreateMessage(event FinalityEvent) []byte {
|
||||
return q.createMessage(event)
|
||||
}
|
||||
|
||||
// TotalWeight calculates total validator weight (exported for testing)
|
||||
func (q *Quasar) TotalWeight(validators []ValidatorState) uint64 {
|
||||
return q.totalWeight(validators)
|
||||
}
|
||||
|
||||
// GetConfig returns quorum configuration (exported for testing)
|
||||
func (q *Quasar) GetConfig() (threshold int, quorumNum, quorumDen uint64) {
|
||||
q.mu.RLock()
|
||||
defer q.mu.RUnlock()
|
||||
return q.threshold, q.quorumNum, q.quorumDen
|
||||
}
|
||||
|
||||
// IsRunning returns whether the Quasar is currently running (exported for testing)
|
||||
func (q *Quasar) IsRunning() bool {
|
||||
q.mu.RLock()
|
||||
defer q.mu.RUnlock()
|
||||
return q.running
|
||||
}
|
||||
|
||||
// SetFinalized adds a finality record (exported for testing/benchmarking)
|
||||
func (q *Quasar) SetFinalized(blockID ids.ID, finality *QuantumFinality) {
|
||||
q.mu.Lock()
|
||||
defer q.mu.Unlock()
|
||||
q.finalized[blockID] = finality
|
||||
}
|
||||
|
||||
// GetFinalized retrieves a finality record (exported for testing)
|
||||
func (q *Quasar) GetFinalized(blockID ids.ID) (*QuantumFinality, bool) {
|
||||
q.mu.RLock()
|
||||
defer q.mu.RUnlock()
|
||||
f, ok := q.finalized[blockID]
|
||||
return f, ok
|
||||
}
|
||||
|
||||
// Helper: big-endian uint64
|
||||
func putUint64BE(b []byte, v uint64) {
|
||||
for i := 0; i < 8; i++ {
|
||||
b[i] = byte(v >> (56 - i*8))
|
||||
}
|
||||
}
|
||||
@@ -1,240 +0,0 @@
|
||||
// Copyright (C) 2019-2025, Lux Industries Inc. All rights reserved.
|
||||
// See the file LICENSE for licensing terms.
|
||||
|
||||
package quasar
|
||||
|
||||
import (
|
||||
"errors"
|
||||
|
||||
"github.com/luxfi/ids"
|
||||
"github.com/luxfi/log"
|
||||
)
|
||||
|
||||
// SignatureType identifies the signature algorithm used
|
||||
type SignatureType uint8
|
||||
|
||||
const (
|
||||
SignatureTypeBLS SignatureType = iota
|
||||
SignatureTypeCorona
|
||||
SignatureTypeQuasar // Hybrid BLS + Corona
|
||||
SignatureTypeMLDSA
|
||||
)
|
||||
|
||||
// Signature is the interface for all signature types
|
||||
type Signature interface {
|
||||
Bytes() []byte
|
||||
Type() SignatureType
|
||||
Signers() []ids.NodeID
|
||||
}
|
||||
|
||||
// Signer is the interface for signing operations
|
||||
type Signer interface {
|
||||
Sign(msg []byte) (Signature, error)
|
||||
PublicKey() []byte
|
||||
}
|
||||
|
||||
// Verifier is the interface for signature verification
|
||||
type Verifier interface {
|
||||
Verify(msg []byte, sig Signature) bool
|
||||
}
|
||||
|
||||
// ThresholdSigner extends Signer for threshold signature schemes
|
||||
type ThresholdSigner interface {
|
||||
Signer
|
||||
Index() int
|
||||
Threshold() int
|
||||
}
|
||||
|
||||
// CoronaConfig holds configuration for Corona threshold signatures
|
||||
type CoronaConfig struct {
|
||||
NumParties int
|
||||
Threshold int
|
||||
PartyIndex int
|
||||
}
|
||||
|
||||
// CoronaStats contains statistics about the Corona coordinator
|
||||
type CoronaStats struct {
|
||||
NumParties int
|
||||
Threshold int
|
||||
Initialized bool
|
||||
}
|
||||
|
||||
// CoronaSignature represents a threshold Corona signature
|
||||
type CoronaSignature struct {
|
||||
sig []byte
|
||||
signers []ids.NodeID
|
||||
}
|
||||
|
||||
// NewCoronaSignature creates a new Corona signature
|
||||
func NewCoronaSignature(sig []byte, signers []ids.NodeID) *CoronaSignature {
|
||||
return &CoronaSignature{sig: sig, signers: signers}
|
||||
}
|
||||
|
||||
func (s *CoronaSignature) Bytes() []byte { return s.sig }
|
||||
func (s *CoronaSignature) Type() SignatureType { return SignatureTypeCorona }
|
||||
func (s *CoronaSignature) Signers() []ids.NodeID { return s.signers }
|
||||
|
||||
// CoronaCoordinator manages the threshold signing protocol.
|
||||
//
|
||||
// Sign/Verify are fail-closed without initialized lattice keys.
|
||||
// Operations return errors unless properly initialized with key
|
||||
// material, or explicitly created via NewTestCoronaCoordinator
|
||||
// for tests.
|
||||
type CoronaCoordinator struct {
|
||||
log log.Logger
|
||||
config CoronaConfig
|
||||
initialized bool
|
||||
testing bool // only true via NewTestCoronaCoordinator
|
||||
validators []ids.NodeID
|
||||
}
|
||||
|
||||
// NewCoronaCoordinator creates a new Corona coordinator.
|
||||
// Sign and Verify will fail until real lattice key material is loaded.
|
||||
func NewCoronaCoordinator(log log.Logger, config CoronaConfig) (*CoronaCoordinator, error) {
|
||||
return &CoronaCoordinator{
|
||||
log: log,
|
||||
config: config,
|
||||
}, nil
|
||||
}
|
||||
|
||||
// NewTestCoronaCoordinator creates a Corona coordinator for testing.
|
||||
// The test coordinator uses deterministic stub signatures that are NOT
|
||||
// cryptographically secure.
|
||||
func NewTestCoronaCoordinator(log log.Logger, config CoronaConfig) (*CoronaCoordinator, error) {
|
||||
return &CoronaCoordinator{
|
||||
log: log,
|
||||
config: config,
|
||||
testing: true,
|
||||
}, nil
|
||||
}
|
||||
|
||||
func (rc *CoronaCoordinator) Initialize(validators []ids.NodeID) error {
|
||||
rc.validators = validators
|
||||
rc.initialized = true
|
||||
return nil
|
||||
}
|
||||
|
||||
func (rc *CoronaCoordinator) IsInitialized() bool {
|
||||
return rc.initialized
|
||||
}
|
||||
|
||||
func (rc *CoronaCoordinator) Sign(msg []byte) (Signature, error) {
|
||||
if !rc.initialized {
|
||||
return nil, errors.New("corona: threshold signing not initialized — requires real lattice key material")
|
||||
}
|
||||
if rc.testing {
|
||||
// Test-only stub: deterministic RT-prefixed signature
|
||||
sig := append([]byte("RT"), msg[:min(32, len(msg))]...)
|
||||
return NewCoronaSignature(sig, rc.validators), nil
|
||||
}
|
||||
return nil, errors.New("corona: threshold signing not initialized — requires real lattice key material")
|
||||
}
|
||||
|
||||
func (rc *CoronaCoordinator) Verify(msg []byte, sig Signature) bool {
|
||||
if !rc.initialized {
|
||||
return false
|
||||
}
|
||||
if rc.testing {
|
||||
return sig != nil && len(sig.Bytes()) > 0
|
||||
}
|
||||
return false
|
||||
}
|
||||
|
||||
func (rc *CoronaCoordinator) Stats() CoronaStats {
|
||||
return CoronaStats{
|
||||
NumParties: rc.config.NumParties,
|
||||
Threshold: rc.config.Threshold,
|
||||
Initialized: rc.initialized,
|
||||
}
|
||||
}
|
||||
|
||||
// Threshold returns the threshold required for signing
|
||||
func (rc *CoronaCoordinator) Threshold() int {
|
||||
return rc.config.Threshold
|
||||
}
|
||||
|
||||
// NumParties returns the number of parties in the threshold scheme
|
||||
func (rc *CoronaCoordinator) NumParties() int {
|
||||
return rc.config.NumParties
|
||||
}
|
||||
|
||||
func min(a, b int) int {
|
||||
if a < b {
|
||||
return a
|
||||
}
|
||||
return b
|
||||
}
|
||||
|
||||
// BLSSignature represents an aggregated BLS signature (node-specific)
|
||||
type BLSSignature struct {
|
||||
sig []byte
|
||||
signers []ids.NodeID
|
||||
}
|
||||
|
||||
func NewBLSSignature(sig []byte, signers []ids.NodeID) *BLSSignature {
|
||||
return &BLSSignature{sig: sig, signers: signers}
|
||||
}
|
||||
|
||||
func (s *BLSSignature) Bytes() []byte { return s.sig }
|
||||
func (s *BLSSignature) Type() SignatureType { return SignatureTypeBLS }
|
||||
func (s *BLSSignature) Signers() []ids.NodeID { return s.signers }
|
||||
|
||||
// QuasarSignature combines BLS and Corona signatures for P/Q security
|
||||
type QuasarSignature struct {
|
||||
bls *BLSSignature
|
||||
corona *CoronaSignature
|
||||
}
|
||||
|
||||
func NewQuasarSignature(bls *BLSSignature, corona *CoronaSignature) *QuasarSignature {
|
||||
return &QuasarSignature{bls: bls, corona: corona}
|
||||
}
|
||||
|
||||
func (s *QuasarSignature) Bytes() []byte {
|
||||
// Concatenate BLS + Corona bytes with length prefix
|
||||
blsBytes := s.bls.Bytes()
|
||||
rtBytes := s.corona.Bytes()
|
||||
result := make([]byte, 4+len(blsBytes)+len(rtBytes))
|
||||
// Length of BLS signature (big endian)
|
||||
result[0] = byte(len(blsBytes) >> 24)
|
||||
result[1] = byte(len(blsBytes) >> 16)
|
||||
result[2] = byte(len(blsBytes) >> 8)
|
||||
result[3] = byte(len(blsBytes))
|
||||
copy(result[4:], blsBytes)
|
||||
copy(result[4+len(blsBytes):], rtBytes)
|
||||
return result
|
||||
}
|
||||
|
||||
func (s *QuasarSignature) Type() SignatureType { return SignatureTypeQuasar }
|
||||
|
||||
func (s *QuasarSignature) Signers() []ids.NodeID {
|
||||
// Return intersection of signers (both must sign)
|
||||
return s.bls.Signers()
|
||||
}
|
||||
|
||||
func (s *QuasarSignature) BLS() *BLSSignature { return s.bls }
|
||||
func (s *QuasarSignature) Corona() *CoronaSignature { return s.corona }
|
||||
|
||||
// QuasarSigner combines classical and post-quantum signers
|
||||
type QuasarSigner interface {
|
||||
Signer
|
||||
// SignQuasar signs with both BLS and Corona in parallel
|
||||
SignQuasar(msg []byte) (*QuasarSignature, error)
|
||||
// VerifyQuasar verifies both BLS and Corona signatures
|
||||
VerifyQuasar(msg []byte, sig *QuasarSignature) bool
|
||||
}
|
||||
|
||||
// FinalityProof represents proof of block finality
|
||||
type FinalityProof struct {
|
||||
BlockID ids.ID
|
||||
Height uint64
|
||||
Signature Signature
|
||||
TotalWeight uint64
|
||||
SignerWeight uint64
|
||||
}
|
||||
|
||||
// ValidatorInfo contains validator information for consensus
|
||||
type ValidatorInfo struct {
|
||||
NodeID ids.NodeID
|
||||
Weight uint64
|
||||
Active bool
|
||||
}
|
||||
@@ -1,378 +0,0 @@
|
||||
// Copyright (C) 2019-2025, Lux Industries Inc. All rights reserved.
|
||||
// See the file LICENSE for licensing terms.
|
||||
|
||||
// Package zap provides ZAP (Zero-copy Agent Protocol) integration for Lux consensus.
|
||||
//
|
||||
// This package bridges ZAP's agentic consensus with Lux's Quasar threshold signatures,
|
||||
// enabling W3C DID-based validator identity and post-quantum secure finality.
|
||||
package zap
|
||||
|
||||
import (
|
||||
"context"
|
||||
"errors"
|
||||
"sync"
|
||||
|
||||
"github.com/luxfi/ids"
|
||||
"github.com/luxfi/log"
|
||||
"github.com/luxfi/node/consensus/quasar"
|
||||
)
|
||||
|
||||
var (
|
||||
// ErrNotInitialized is returned when the bridge is used before initialization
|
||||
ErrNotInitialized = errors.New("zap bridge not initialized")
|
||||
// ErrValidatorNotFound is returned when a validator is not in the set
|
||||
ErrValidatorNotFound = errors.New("validator not found")
|
||||
// ErrQueryNotFound is returned when a query ID is not known
|
||||
ErrQueryNotFound = errors.New("query not found")
|
||||
// ErrAlreadyVoted is returned when a validator tries to vote twice
|
||||
ErrAlreadyVoted = errors.New("already voted on this query")
|
||||
// ErrInvalidDID is returned when a DID is malformed
|
||||
ErrInvalidDID = errors.New("invalid DID format")
|
||||
)
|
||||
|
||||
// BridgeConfig configures the ZAP-Lux consensus bridge
|
||||
type BridgeConfig struct {
|
||||
// ConsensusThreshold is the fraction of votes needed (0.5 = majority)
|
||||
ConsensusThreshold float64
|
||||
// MinResponses is the minimum responses before checking consensus
|
||||
MinResponses int
|
||||
// MinVotes is the minimum votes before checking consensus
|
||||
MinVotes int
|
||||
// EnablePQCrypto enables post-quantum signatures (ML-DSA-65)
|
||||
EnablePQCrypto bool
|
||||
}
|
||||
|
||||
// DefaultBridgeConfig returns sensible defaults for the bridge
|
||||
func DefaultBridgeConfig() BridgeConfig {
|
||||
return BridgeConfig{
|
||||
ConsensusThreshold: 0.5,
|
||||
MinResponses: 1,
|
||||
MinVotes: 3,
|
||||
EnablePQCrypto: true,
|
||||
}
|
||||
}
|
||||
|
||||
// Bridge connects ZAP agentic consensus to Lux's Quasar finality
|
||||
type Bridge struct {
|
||||
log log.Logger
|
||||
config BridgeConfig
|
||||
quasar *quasar.CoronaCoordinator
|
||||
mu sync.RWMutex
|
||||
queries map[string]*QueryState // QueryID -> QueryState
|
||||
dids map[ids.NodeID]*DID // NodeID -> DID
|
||||
}
|
||||
|
||||
// NewBridge creates a new ZAP-Lux consensus bridge
|
||||
func NewBridge(log log.Logger, config BridgeConfig) *Bridge {
|
||||
return &Bridge{
|
||||
log: log,
|
||||
config: config,
|
||||
queries: make(map[string]*QueryState),
|
||||
dids: make(map[ids.NodeID]*DID),
|
||||
}
|
||||
}
|
||||
|
||||
// Initialize sets up the bridge with a Quasar coordinator
|
||||
func (b *Bridge) Initialize(coordinator *quasar.CoronaCoordinator) error {
|
||||
b.mu.Lock()
|
||||
defer b.mu.Unlock()
|
||||
|
||||
if coordinator == nil {
|
||||
return ErrNotInitialized
|
||||
}
|
||||
|
||||
b.quasar = coordinator
|
||||
b.log.Info("ZAP bridge initialized",
|
||||
log.Int("threshold", coordinator.Threshold()),
|
||||
log.Int("parties", coordinator.NumParties()),
|
||||
log.Bool("pqCrypto", b.config.EnablePQCrypto),
|
||||
)
|
||||
return nil
|
||||
}
|
||||
|
||||
// RegisterValidator associates a DID with a Lux NodeID
|
||||
func (b *Bridge) RegisterValidator(nodeID ids.NodeID, did *DID) error {
|
||||
if did == nil || !did.Valid() {
|
||||
return ErrInvalidDID
|
||||
}
|
||||
|
||||
b.mu.Lock()
|
||||
defer b.mu.Unlock()
|
||||
|
||||
b.dids[nodeID] = did
|
||||
b.log.Debug("Registered validator DID",
|
||||
log.Stringer("nodeID", nodeID),
|
||||
log.String("did", did.String()),
|
||||
)
|
||||
return nil
|
||||
}
|
||||
|
||||
// GetValidatorDID returns the DID for a NodeID
|
||||
func (b *Bridge) GetValidatorDID(nodeID ids.NodeID) (*DID, error) {
|
||||
b.mu.RLock()
|
||||
defer b.mu.RUnlock()
|
||||
|
||||
did, ok := b.dids[nodeID]
|
||||
if !ok {
|
||||
return nil, ErrValidatorNotFound
|
||||
}
|
||||
return did, nil
|
||||
}
|
||||
|
||||
// SubmitQuery creates a new agentic consensus query
|
||||
func (b *Bridge) SubmitQuery(ctx context.Context, queryID string, content []byte, submitter ids.NodeID) error {
|
||||
b.mu.Lock()
|
||||
defer b.mu.Unlock()
|
||||
|
||||
if b.quasar == nil {
|
||||
return ErrNotInitialized
|
||||
}
|
||||
|
||||
submitterDID, ok := b.dids[submitter]
|
||||
if !ok {
|
||||
return ErrValidatorNotFound
|
||||
}
|
||||
|
||||
b.queries[queryID] = &QueryState{
|
||||
ID: queryID,
|
||||
Content: content,
|
||||
Submitter: submitterDID,
|
||||
Responses: make(map[string]*Response),
|
||||
Votes: make(map[string][]*DID),
|
||||
}
|
||||
|
||||
b.log.Debug("Query submitted",
|
||||
log.String("queryID", queryID),
|
||||
log.String("submitter", submitterDID.String()),
|
||||
)
|
||||
return nil
|
||||
}
|
||||
|
||||
// SubmitResponse adds a response to a query
|
||||
func (b *Bridge) SubmitResponse(ctx context.Context, queryID, responseID string, content []byte, responder ids.NodeID) error {
|
||||
b.mu.Lock()
|
||||
defer b.mu.Unlock()
|
||||
|
||||
state, ok := b.queries[queryID]
|
||||
if !ok {
|
||||
return ErrQueryNotFound
|
||||
}
|
||||
|
||||
responderDID, ok := b.dids[responder]
|
||||
if !ok {
|
||||
return ErrValidatorNotFound
|
||||
}
|
||||
|
||||
if state.Finalized != "" {
|
||||
return errors.New("query already finalized")
|
||||
}
|
||||
|
||||
state.Responses[responseID] = &Response{
|
||||
ID: responseID,
|
||||
QueryID: queryID,
|
||||
Content: content,
|
||||
Responder: responderDID,
|
||||
}
|
||||
state.Votes[responseID] = []*DID{}
|
||||
|
||||
b.log.Debug("Response submitted",
|
||||
log.String("queryID", queryID),
|
||||
log.String("responseID", responseID),
|
||||
log.String("responder", responderDID.String()),
|
||||
)
|
||||
return nil
|
||||
}
|
||||
|
||||
// Vote casts a vote for a response
|
||||
func (b *Bridge) Vote(ctx context.Context, queryID, responseID string, voter ids.NodeID) error {
|
||||
b.mu.Lock()
|
||||
defer b.mu.Unlock()
|
||||
|
||||
state, ok := b.queries[queryID]
|
||||
if !ok {
|
||||
return ErrQueryNotFound
|
||||
}
|
||||
|
||||
if state.Finalized != "" {
|
||||
return errors.New("query already finalized")
|
||||
}
|
||||
|
||||
if _, ok := state.Responses[responseID]; !ok {
|
||||
return errors.New("response not found")
|
||||
}
|
||||
|
||||
voterDID, ok := b.dids[voter]
|
||||
if !ok {
|
||||
return ErrValidatorNotFound
|
||||
}
|
||||
|
||||
// Check for double voting (by DID URI)
|
||||
voterURI := voterDID.String()
|
||||
for _, voters := range state.Votes {
|
||||
for _, v := range voters {
|
||||
if v.String() == voterURI {
|
||||
return ErrAlreadyVoted
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
state.Votes[responseID] = append(state.Votes[responseID], voterDID)
|
||||
|
||||
// Check consensus
|
||||
b.checkConsensus(state)
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func (b *Bridge) checkConsensus(state *QueryState) {
|
||||
if state.Finalized != "" {
|
||||
return
|
||||
}
|
||||
|
||||
if len(state.Responses) < b.config.MinResponses {
|
||||
return
|
||||
}
|
||||
|
||||
totalVotes := 0
|
||||
for _, voters := range state.Votes {
|
||||
totalVotes += len(voters)
|
||||
}
|
||||
|
||||
if totalVotes < b.config.MinVotes {
|
||||
return
|
||||
}
|
||||
|
||||
// Find best response
|
||||
var best struct {
|
||||
id string
|
||||
count int
|
||||
}
|
||||
|
||||
for responseID, voters := range state.Votes {
|
||||
count := len(voters)
|
||||
confidence := float64(count) / float64(totalVotes)
|
||||
|
||||
if confidence >= b.config.ConsensusThreshold {
|
||||
if best.id == "" || count > best.count {
|
||||
best.id = responseID
|
||||
best.count = count
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if best.id != "" {
|
||||
state.Finalized = best.id
|
||||
b.log.Info("Query finalized",
|
||||
log.String("queryID", state.ID),
|
||||
log.String("responseID", best.id),
|
||||
log.Int("votes", best.count),
|
||||
log.Int("total", totalVotes),
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
// GetResult returns the consensus result for a query
|
||||
func (b *Bridge) GetResult(queryID string) (*ConsensusResult, error) {
|
||||
b.mu.RLock()
|
||||
defer b.mu.RUnlock()
|
||||
|
||||
state, ok := b.queries[queryID]
|
||||
if !ok {
|
||||
return nil, ErrQueryNotFound
|
||||
}
|
||||
|
||||
if state.Finalized == "" {
|
||||
return nil, nil
|
||||
}
|
||||
|
||||
response := state.Responses[state.Finalized]
|
||||
votes := len(state.Votes[state.Finalized])
|
||||
|
||||
totalVoters := 0
|
||||
for _, v := range state.Votes {
|
||||
totalVoters += len(v)
|
||||
}
|
||||
|
||||
confidence := 0.0
|
||||
if totalVoters > 0 {
|
||||
confidence = float64(votes) / float64(totalVoters)
|
||||
}
|
||||
|
||||
return &ConsensusResult{
|
||||
Response: response,
|
||||
Votes: votes,
|
||||
TotalVoters: totalVoters,
|
||||
Confidence: confidence,
|
||||
}, nil
|
||||
}
|
||||
|
||||
// IsFinalized checks if a query has reached consensus
|
||||
func (b *Bridge) IsFinalized(queryID string) bool {
|
||||
b.mu.RLock()
|
||||
defer b.mu.RUnlock()
|
||||
|
||||
state, ok := b.queries[queryID]
|
||||
return ok && state.Finalized != ""
|
||||
}
|
||||
|
||||
// SignWithQuasar signs a message using Quasar hybrid signatures
|
||||
func (b *Bridge) SignWithQuasar(msg []byte) (quasar.Signature, error) {
|
||||
b.mu.RLock()
|
||||
defer b.mu.RUnlock()
|
||||
|
||||
if b.quasar == nil {
|
||||
return nil, ErrNotInitialized
|
||||
}
|
||||
|
||||
return b.quasar.Sign(msg)
|
||||
}
|
||||
|
||||
// VerifyQuasar verifies a Quasar signature
|
||||
func (b *Bridge) VerifyQuasar(msg []byte, sig quasar.Signature) bool {
|
||||
b.mu.RLock()
|
||||
defer b.mu.RUnlock()
|
||||
|
||||
if b.quasar == nil {
|
||||
return false
|
||||
}
|
||||
|
||||
return b.quasar.Verify(msg, sig)
|
||||
}
|
||||
|
||||
// Stats returns bridge statistics
|
||||
func (b *Bridge) Stats() BridgeStats {
|
||||
b.mu.RLock()
|
||||
defer b.mu.RUnlock()
|
||||
|
||||
active := 0
|
||||
finalized := 0
|
||||
for _, state := range b.queries {
|
||||
if state.Finalized != "" {
|
||||
finalized++
|
||||
} else {
|
||||
active++
|
||||
}
|
||||
}
|
||||
|
||||
var quasarStats quasar.CoronaStats
|
||||
if b.quasar != nil {
|
||||
quasarStats = b.quasar.Stats()
|
||||
}
|
||||
|
||||
return BridgeStats{
|
||||
RegisteredValidators: len(b.dids),
|
||||
ActiveQueries: active,
|
||||
FinalizedQueries: finalized,
|
||||
QuasarInitialized: b.quasar != nil && b.quasar.IsInitialized(),
|
||||
QuasarStats: quasarStats,
|
||||
}
|
||||
}
|
||||
|
||||
// BridgeStats contains statistics about the bridge
|
||||
type BridgeStats struct {
|
||||
RegisteredValidators int
|
||||
ActiveQueries int
|
||||
FinalizedQueries int
|
||||
QuasarInitialized bool
|
||||
QuasarStats quasar.CoronaStats
|
||||
}
|
||||
@@ -1,459 +0,0 @@
|
||||
// Copyright (C) 2019-2025, Lux Industries Inc. All rights reserved.
|
||||
// See the file LICENSE for licensing terms.
|
||||
|
||||
package zap
|
||||
|
||||
import (
|
||||
"context"
|
||||
"testing"
|
||||
|
||||
"github.com/luxfi/ids"
|
||||
"github.com/luxfi/log"
|
||||
"github.com/luxfi/node/consensus/quasar"
|
||||
"github.com/stretchr/testify/require"
|
||||
)
|
||||
|
||||
func TestParseDID(t *testing.T) {
|
||||
tests := []struct {
|
||||
name string
|
||||
input string
|
||||
want *DID
|
||||
wantErr bool
|
||||
}{
|
||||
{
|
||||
name: "valid did:lux",
|
||||
input: "did:lux:z6MkhaXgBZDvotDkL5257faiztiGiC2QtKLGpbnnEGta2doK",
|
||||
want: &DID{
|
||||
Method: DIDMethodLux,
|
||||
ID: "z6MkhaXgBZDvotDkL5257faiztiGiC2QtKLGpbnnEGta2doK",
|
||||
},
|
||||
wantErr: false,
|
||||
},
|
||||
{
|
||||
name: "valid did:key",
|
||||
input: "did:key:z6MkhaXgBZDvotDkL5257faiztiGiC2QtKLGpbnnEGta2doK",
|
||||
want: &DID{
|
||||
Method: DIDMethodKey,
|
||||
ID: "z6MkhaXgBZDvotDkL5257faiztiGiC2QtKLGpbnnEGta2doK",
|
||||
},
|
||||
wantErr: false,
|
||||
},
|
||||
{
|
||||
name: "valid did:web",
|
||||
input: "did:web:example.com:users:alice",
|
||||
want: &DID{
|
||||
Method: DIDMethodWeb,
|
||||
ID: "example.com:users:alice",
|
||||
},
|
||||
wantErr: false,
|
||||
},
|
||||
{
|
||||
name: "invalid - no did prefix",
|
||||
input: "lux:z6MkhaXgBZDvotDkL5257faiztiGiC2QtKLGpbnnEGta2doK",
|
||||
wantErr: true,
|
||||
},
|
||||
{
|
||||
name: "invalid - unknown method",
|
||||
input: "did:unknown:abc123",
|
||||
wantErr: true,
|
||||
},
|
||||
{
|
||||
name: "invalid - empty id",
|
||||
input: "did:lux:",
|
||||
wantErr: true,
|
||||
},
|
||||
}
|
||||
|
||||
for _, tt := range tests {
|
||||
t.Run(tt.name, func(t *testing.T) {
|
||||
got, err := ParseDID(tt.input)
|
||||
if tt.wantErr {
|
||||
require.Error(t, err)
|
||||
return
|
||||
}
|
||||
require.NoError(t, err)
|
||||
require.Equal(t, tt.want.Method, got.Method)
|
||||
require.Equal(t, tt.want.ID, got.ID)
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestDIDString(t *testing.T) {
|
||||
did := &DID{Method: DIDMethodLux, ID: "z6MkTest"}
|
||||
require.Equal(t, "did:lux:z6MkTest", did.String())
|
||||
}
|
||||
|
||||
func TestDIDValid(t *testing.T) {
|
||||
tests := []struct {
|
||||
name string
|
||||
did *DID
|
||||
valid bool
|
||||
}{
|
||||
{
|
||||
name: "valid lux",
|
||||
did: &DID{Method: DIDMethodLux, ID: "z6MkTest"},
|
||||
valid: true,
|
||||
},
|
||||
{
|
||||
name: "valid key",
|
||||
did: &DID{Method: DIDMethodKey, ID: "z6MkTest"},
|
||||
valid: true,
|
||||
},
|
||||
{
|
||||
name: "valid web",
|
||||
did: &DID{Method: DIDMethodWeb, ID: "example.com"},
|
||||
valid: true,
|
||||
},
|
||||
{
|
||||
name: "nil did",
|
||||
did: nil,
|
||||
valid: false,
|
||||
},
|
||||
{
|
||||
name: "empty id",
|
||||
did: &DID{Method: DIDMethodLux, ID: ""},
|
||||
valid: false,
|
||||
},
|
||||
{
|
||||
name: "unknown method",
|
||||
did: &DID{Method: "unknown", ID: "test"},
|
||||
valid: false,
|
||||
},
|
||||
}
|
||||
|
||||
for _, tt := range tests {
|
||||
t.Run(tt.name, func(t *testing.T) {
|
||||
require.Equal(t, tt.valid, tt.did.Valid())
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestDIDFromNodeID(t *testing.T) {
|
||||
nodeID := ids.GenerateTestNodeID()
|
||||
did := DIDFromNodeID(nodeID)
|
||||
|
||||
require.NotNil(t, did)
|
||||
require.Equal(t, DIDMethodLux, did.Method)
|
||||
require.True(t, did.Valid())
|
||||
require.Contains(t, did.String(), "did:lux:")
|
||||
}
|
||||
|
||||
func TestDIDFromWeb(t *testing.T) {
|
||||
tests := []struct {
|
||||
name string
|
||||
domain string
|
||||
path string
|
||||
wantID string
|
||||
wantErr bool
|
||||
}{
|
||||
{
|
||||
name: "domain only",
|
||||
domain: "example.com",
|
||||
path: "",
|
||||
wantID: "example.com",
|
||||
wantErr: false,
|
||||
},
|
||||
{
|
||||
name: "domain with path",
|
||||
domain: "example.com",
|
||||
path: "users/alice",
|
||||
wantID: "example.com:users:alice",
|
||||
wantErr: false,
|
||||
},
|
||||
{
|
||||
name: "empty domain",
|
||||
domain: "",
|
||||
path: "",
|
||||
wantErr: true,
|
||||
},
|
||||
{
|
||||
name: "domain with slash",
|
||||
domain: "example/com",
|
||||
path: "",
|
||||
wantErr: true,
|
||||
},
|
||||
}
|
||||
|
||||
for _, tt := range tests {
|
||||
t.Run(tt.name, func(t *testing.T) {
|
||||
got, err := DIDFromWeb(tt.domain, tt.path)
|
||||
if tt.wantErr {
|
||||
require.Error(t, err)
|
||||
return
|
||||
}
|
||||
require.NoError(t, err)
|
||||
require.Equal(t, DIDMethodWeb, got.Method)
|
||||
require.Equal(t, tt.wantID, got.ID)
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestGenerateDocument(t *testing.T) {
|
||||
did := &DID{Method: DIDMethodLux, ID: "z6MkTest"}
|
||||
doc, err := did.GenerateDocument()
|
||||
|
||||
require.NoError(t, err)
|
||||
require.NotNil(t, doc)
|
||||
require.Equal(t, "did:lux:z6MkTest", doc.ID)
|
||||
require.Len(t, doc.Context, 2)
|
||||
require.Len(t, doc.VerificationMethod, 1)
|
||||
require.Len(t, doc.Authentication, 1)
|
||||
require.Len(t, doc.Service, 1)
|
||||
require.Equal(t, "ZapAgent", doc.Service[0].Type)
|
||||
}
|
||||
|
||||
func TestBridgeNew(t *testing.T) {
|
||||
logger := log.Noop()
|
||||
config := DefaultBridgeConfig()
|
||||
bridge := NewBridge(logger, config)
|
||||
|
||||
require.NotNil(t, bridge)
|
||||
require.Equal(t, 0.5, bridge.config.ConsensusThreshold)
|
||||
}
|
||||
|
||||
func TestBridgeInitialize(t *testing.T) {
|
||||
logger := log.Noop()
|
||||
bridge := NewBridge(logger, DefaultBridgeConfig())
|
||||
|
||||
// Test with nil coordinator
|
||||
err := bridge.Initialize(nil)
|
||||
require.ErrorIs(t, err, ErrNotInitialized)
|
||||
|
||||
// Test with valid coordinator
|
||||
coordinator, err := quasar.NewTestCoronaCoordinator(logger, quasar.CoronaConfig{
|
||||
NumParties: 4,
|
||||
Threshold: 3,
|
||||
PartyIndex: 0,
|
||||
})
|
||||
require.NoError(t, err)
|
||||
|
||||
err = bridge.Initialize(coordinator)
|
||||
require.NoError(t, err)
|
||||
}
|
||||
|
||||
func TestBridgeRegisterValidator(t *testing.T) {
|
||||
logger := log.Noop()
|
||||
bridge := NewBridge(logger, DefaultBridgeConfig())
|
||||
|
||||
nodeID := ids.GenerateTestNodeID()
|
||||
did := DIDFromNodeID(nodeID)
|
||||
|
||||
// Register validator
|
||||
err := bridge.RegisterValidator(nodeID, did)
|
||||
require.NoError(t, err)
|
||||
|
||||
// Get validator DID
|
||||
got, err := bridge.GetValidatorDID(nodeID)
|
||||
require.NoError(t, err)
|
||||
require.Equal(t, did.String(), got.String())
|
||||
|
||||
// Unknown validator
|
||||
_, err = bridge.GetValidatorDID(ids.GenerateTestNodeID())
|
||||
require.ErrorIs(t, err, ErrValidatorNotFound)
|
||||
|
||||
// Invalid DID
|
||||
err = bridge.RegisterValidator(nodeID, nil)
|
||||
require.ErrorIs(t, err, ErrInvalidDID)
|
||||
}
|
||||
|
||||
func TestBridgeConsensusFlow(t *testing.T) {
|
||||
logger := log.Noop()
|
||||
bridge := NewBridge(logger, BridgeConfig{
|
||||
ConsensusThreshold: 0.5,
|
||||
MinResponses: 1,
|
||||
MinVotes: 2,
|
||||
EnablePQCrypto: true,
|
||||
})
|
||||
|
||||
// Initialize with coordinator
|
||||
coordinator, err := quasar.NewTestCoronaCoordinator(logger, quasar.CoronaConfig{
|
||||
NumParties: 4,
|
||||
Threshold: 3,
|
||||
PartyIndex: 0,
|
||||
})
|
||||
require.NoError(t, err)
|
||||
require.NoError(t, bridge.Initialize(coordinator))
|
||||
|
||||
// Register validators
|
||||
submitter := ids.GenerateTestNodeID()
|
||||
responder := ids.GenerateTestNodeID()
|
||||
voter1 := ids.GenerateTestNodeID()
|
||||
voter2 := ids.GenerateTestNodeID()
|
||||
|
||||
require.NoError(t, bridge.RegisterValidator(submitter, DIDFromNodeID(submitter)))
|
||||
require.NoError(t, bridge.RegisterValidator(responder, DIDFromNodeID(responder)))
|
||||
require.NoError(t, bridge.RegisterValidator(voter1, DIDFromNodeID(voter1)))
|
||||
require.NoError(t, bridge.RegisterValidator(voter2, DIDFromNodeID(voter2)))
|
||||
|
||||
ctx := context.Background()
|
||||
|
||||
// Submit query
|
||||
queryID := "query123"
|
||||
err = bridge.SubmitQuery(ctx, queryID, []byte("What is 2+2?"), submitter)
|
||||
require.NoError(t, err)
|
||||
|
||||
// Submit response
|
||||
responseID := "response456"
|
||||
err = bridge.SubmitResponse(ctx, queryID, responseID, []byte("4"), responder)
|
||||
require.NoError(t, err)
|
||||
|
||||
// Vote
|
||||
require.NoError(t, bridge.Vote(ctx, queryID, responseID, voter1))
|
||||
require.False(t, bridge.IsFinalized(queryID)) // Not enough votes yet
|
||||
|
||||
require.NoError(t, bridge.Vote(ctx, queryID, responseID, voter2))
|
||||
require.True(t, bridge.IsFinalized(queryID)) // Now finalized
|
||||
|
||||
// Get result
|
||||
result, err := bridge.GetResult(queryID)
|
||||
require.NoError(t, err)
|
||||
require.NotNil(t, result)
|
||||
require.Equal(t, responseID, result.Response.ID)
|
||||
require.Equal(t, 2, result.Votes)
|
||||
require.Equal(t, 2, result.TotalVoters)
|
||||
require.Equal(t, 1.0, result.Confidence)
|
||||
}
|
||||
|
||||
func TestBridgeDoubleVote(t *testing.T) {
|
||||
logger := log.Noop()
|
||||
bridge := NewBridge(logger, DefaultBridgeConfig())
|
||||
|
||||
coordinator, _ := quasar.NewTestCoronaCoordinator(logger, quasar.CoronaConfig{
|
||||
NumParties: 2,
|
||||
Threshold: 1,
|
||||
})
|
||||
bridge.Initialize(coordinator)
|
||||
|
||||
submitter := ids.GenerateTestNodeID()
|
||||
voter := ids.GenerateTestNodeID()
|
||||
|
||||
bridge.RegisterValidator(submitter, DIDFromNodeID(submitter))
|
||||
bridge.RegisterValidator(voter, DIDFromNodeID(voter))
|
||||
|
||||
ctx := context.Background()
|
||||
queryID := "q1"
|
||||
responseID := "r1"
|
||||
|
||||
bridge.SubmitQuery(ctx, queryID, []byte("test"), submitter)
|
||||
bridge.SubmitResponse(ctx, queryID, responseID, []byte("answer"), submitter)
|
||||
|
||||
// First vote succeeds
|
||||
require.NoError(t, bridge.Vote(ctx, queryID, responseID, voter))
|
||||
|
||||
// Second vote fails
|
||||
err := bridge.Vote(ctx, queryID, responseID, voter)
|
||||
require.ErrorIs(t, err, ErrAlreadyVoted)
|
||||
}
|
||||
|
||||
func TestBridgeStats(t *testing.T) {
|
||||
logger := log.Noop()
|
||||
bridge := NewBridge(logger, DefaultBridgeConfig())
|
||||
|
||||
coordinator, _ := quasar.NewTestCoronaCoordinator(logger, quasar.CoronaConfig{
|
||||
NumParties: 4,
|
||||
Threshold: 3,
|
||||
})
|
||||
bridge.Initialize(coordinator)
|
||||
|
||||
nodeID := ids.GenerateTestNodeID()
|
||||
bridge.RegisterValidator(nodeID, DIDFromNodeID(nodeID))
|
||||
|
||||
stats := bridge.Stats()
|
||||
require.Equal(t, 1, stats.RegisteredValidators)
|
||||
require.Equal(t, 0, stats.ActiveQueries)
|
||||
require.Equal(t, 0, stats.FinalizedQueries)
|
||||
require.False(t, stats.QuasarInitialized) // Not initialized with validators
|
||||
}
|
||||
|
||||
func TestNewQuery(t *testing.T) {
|
||||
did := &DID{Method: DIDMethodLux, ID: "z6MkTest"}
|
||||
query := NewQuery([]byte("What is 2+2?"), did)
|
||||
|
||||
require.NotEmpty(t, query.ID)
|
||||
require.Equal(t, 64, len(query.ID)) // SHA-256 hex
|
||||
require.Equal(t, []byte("What is 2+2?"), query.Content)
|
||||
require.Equal(t, did, query.Submitter)
|
||||
require.Greater(t, query.Timestamp, int64(0))
|
||||
}
|
||||
|
||||
func TestNewResponse(t *testing.T) {
|
||||
did := &DID{Method: DIDMethodLux, ID: "z6MkTest"}
|
||||
queryID := "0000000000000000000000000000000000000000000000000000000000000000"
|
||||
response := NewResponse(queryID, []byte("4"), did)
|
||||
|
||||
require.NotEmpty(t, response.ID)
|
||||
require.Equal(t, 64, len(response.ID)) // SHA-256 hex
|
||||
require.Equal(t, queryID, response.QueryID)
|
||||
require.Equal(t, []byte("4"), response.Content)
|
||||
require.Equal(t, did, response.Responder)
|
||||
}
|
||||
|
||||
func TestInMemoryStakeRegistry(t *testing.T) {
|
||||
registry := NewInMemoryStakeRegistry()
|
||||
did := &DID{Method: DIDMethodLux, ID: "z6MkTest"}
|
||||
|
||||
// Initial stake is 0
|
||||
stake, err := registry.GetStake(did)
|
||||
require.NoError(t, err)
|
||||
require.Equal(t, uint64(0), stake)
|
||||
|
||||
// Set stake
|
||||
require.NoError(t, registry.SetStake(did, 1000))
|
||||
|
||||
// Get stake
|
||||
stake, err = registry.GetStake(did)
|
||||
require.NoError(t, err)
|
||||
require.Equal(t, uint64(1000), stake)
|
||||
|
||||
// Total stake
|
||||
require.Equal(t, uint64(1000), registry.TotalStake())
|
||||
|
||||
// Has sufficient stake
|
||||
has, err := registry.HasSufficientStake(did, 500)
|
||||
require.NoError(t, err)
|
||||
require.True(t, has)
|
||||
|
||||
has, err = registry.HasSufficientStake(did, 2000)
|
||||
require.NoError(t, err)
|
||||
require.False(t, has)
|
||||
|
||||
// Stake weight
|
||||
weight, err := registry.StakeWeight(did)
|
||||
require.NoError(t, err)
|
||||
require.Equal(t, 1.0, weight) // Only staker
|
||||
}
|
||||
|
||||
func TestAgentMessageType(t *testing.T) {
|
||||
require.Equal(t, "Query", AgentMessageTypeQuery.String())
|
||||
require.Equal(t, "Response", AgentMessageTypeResponse.String())
|
||||
require.Equal(t, "Vote", AgentMessageTypeVote.String())
|
||||
require.Equal(t, "Finality", AgentMessageTypeFinality.String())
|
||||
require.Equal(t, "Unknown", AgentMessageType(255).String())
|
||||
}
|
||||
|
||||
func TestBase58EncodeDecode(t *testing.T) {
|
||||
tests := []struct {
|
||||
name string
|
||||
data []byte
|
||||
}{
|
||||
{"empty", []byte{}},
|
||||
{"single byte", []byte{0x01}},
|
||||
{"multiple bytes", []byte{0x01, 0x02, 0x03, 0x04}},
|
||||
{"leading zeros", []byte{0x00, 0x00, 0x01, 0x02}},
|
||||
{"all zeros", []byte{0x00, 0x00, 0x00}},
|
||||
}
|
||||
|
||||
for _, tt := range tests {
|
||||
t.Run(tt.name, func(t *testing.T) {
|
||||
encoded := base58Encode(tt.data)
|
||||
decoded, err := base58Decode(encoded)
|
||||
require.NoError(t, err)
|
||||
require.Equal(t, tt.data, decoded)
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestBase58DecodeInvalidChar(t *testing.T) {
|
||||
_, err := base58Decode("0OIl") // Invalid chars
|
||||
require.Error(t, err)
|
||||
}
|
||||
@@ -1,355 +0,0 @@
|
||||
// Copyright (C) 2019-2025, Lux Industries Inc. All rights reserved.
|
||||
// See the file LICENSE for licensing terms.
|
||||
|
||||
package zap
|
||||
|
||||
import (
|
||||
"crypto/sha256"
|
||||
"encoding/hex"
|
||||
"fmt"
|
||||
"strings"
|
||||
|
||||
"github.com/luxfi/ids"
|
||||
)
|
||||
|
||||
const (
|
||||
// MethodLux is the did:lux method for blockchain-anchored DIDs
|
||||
MethodLux = "lux"
|
||||
// MethodKey is the did:key method for self-certifying DIDs
|
||||
MethodKey = "key"
|
||||
// MethodWeb is the did:web method for DNS-based DIDs
|
||||
MethodWeb = "web"
|
||||
|
||||
// MLDSAPublicKeySize is the expected size of ML-DSA-65 public keys
|
||||
MLDSAPublicKeySize = 1952
|
||||
|
||||
// MultibaseBase58BTC is the multibase prefix for base58btc
|
||||
MultibaseBase58BTC = 'z'
|
||||
)
|
||||
|
||||
// MulticodecMLDSA65 is the provisional multicodec prefix for ML-DSA-65
|
||||
var MulticodecMLDSA65 = []byte{0x13, 0x09}
|
||||
|
||||
// DIDMethod represents a DID method identifier
|
||||
type DIDMethod string
|
||||
|
||||
const (
|
||||
DIDMethodLux DIDMethod = MethodLux
|
||||
DIDMethodKey DIDMethod = MethodKey
|
||||
DIDMethodWeb DIDMethod = MethodWeb
|
||||
)
|
||||
|
||||
// DID represents a W3C Decentralized Identifier
|
||||
type DID struct {
|
||||
Method DIDMethod
|
||||
ID string
|
||||
}
|
||||
|
||||
// NewDID creates a DID from method and identifier
|
||||
func NewDID(method DIDMethod, id string) *DID {
|
||||
return &DID{Method: method, ID: id}
|
||||
}
|
||||
|
||||
// ParseDID parses a DID from a string in format "did:method:id"
|
||||
func ParseDID(s string) (*DID, error) {
|
||||
if !strings.HasPrefix(s, "did:") {
|
||||
return nil, fmt.Errorf("%w: must start with 'did:'", ErrInvalidDID)
|
||||
}
|
||||
|
||||
rest := s[4:] // Skip "did:"
|
||||
colonIndex := strings.Index(rest, ":")
|
||||
if colonIndex == -1 {
|
||||
return nil, fmt.Errorf("%w: expected 'did:method:id'", ErrInvalidDID)
|
||||
}
|
||||
|
||||
methodStr := rest[:colonIndex]
|
||||
id := rest[colonIndex+1:]
|
||||
|
||||
if id == "" {
|
||||
return nil, fmt.Errorf("%w: identifier cannot be empty", ErrInvalidDID)
|
||||
}
|
||||
|
||||
var method DIDMethod
|
||||
switch methodStr {
|
||||
case MethodLux:
|
||||
method = DIDMethodLux
|
||||
case MethodKey:
|
||||
method = DIDMethodKey
|
||||
case MethodWeb:
|
||||
method = DIDMethodWeb
|
||||
default:
|
||||
return nil, fmt.Errorf("%w: unknown method '%s'", ErrInvalidDID, methodStr)
|
||||
}
|
||||
|
||||
return &DID{Method: method, ID: id}, nil
|
||||
}
|
||||
|
||||
// String returns the full DID URI
|
||||
func (d *DID) String() string {
|
||||
if d == nil {
|
||||
return ""
|
||||
}
|
||||
return fmt.Sprintf("did:%s:%s", d.Method, d.ID)
|
||||
}
|
||||
|
||||
// Valid checks if the DID is well-formed
|
||||
func (d *DID) Valid() bool {
|
||||
if d == nil || d.ID == "" {
|
||||
return false
|
||||
}
|
||||
switch d.Method {
|
||||
case DIDMethodLux, DIDMethodKey, DIDMethodWeb:
|
||||
return true
|
||||
default:
|
||||
return false
|
||||
}
|
||||
}
|
||||
|
||||
// DIDFromNodeID creates a did:lux from a Lux NodeID
|
||||
func DIDFromNodeID(nodeID ids.NodeID) *DID {
|
||||
// Use multibase-encoded NodeID bytes
|
||||
encoded := base58Encode(nodeID[:])
|
||||
return &DID{
|
||||
Method: DIDMethodLux,
|
||||
ID: string(MultibaseBase58BTC) + encoded,
|
||||
}
|
||||
}
|
||||
|
||||
// DIDFromPublicKey creates a did:key from an ML-DSA-65 public key
|
||||
func DIDFromPublicKey(publicKey []byte) (*DID, error) {
|
||||
if len(publicKey) != MLDSAPublicKeySize {
|
||||
return nil, fmt.Errorf("invalid ML-DSA public key size: expected %d, got %d",
|
||||
MLDSAPublicKeySize, len(publicKey))
|
||||
}
|
||||
|
||||
// Prefix with multicodec for ML-DSA-65
|
||||
prefixed := make([]byte, len(MulticodecMLDSA65)+len(publicKey))
|
||||
copy(prefixed, MulticodecMLDSA65)
|
||||
copy(prefixed[len(MulticodecMLDSA65):], publicKey)
|
||||
|
||||
// Encode with multibase (base58btc)
|
||||
encoded := base58Encode(prefixed)
|
||||
|
||||
return &DID{
|
||||
Method: DIDMethodKey,
|
||||
ID: string(MultibaseBase58BTC) + encoded,
|
||||
}, nil
|
||||
}
|
||||
|
||||
// DIDFromWeb creates a did:web from a domain and optional path
|
||||
func DIDFromWeb(domain string, path string) (*DID, error) {
|
||||
if domain == "" {
|
||||
return nil, fmt.Errorf("%w: domain cannot be empty", ErrInvalidDID)
|
||||
}
|
||||
if strings.ContainsAny(domain, "/:") {
|
||||
return nil, fmt.Errorf("%w: domain cannot contain '/' or ':'", ErrInvalidDID)
|
||||
}
|
||||
|
||||
var id string
|
||||
if path != "" {
|
||||
// Replace '/' with ':' per did:web spec
|
||||
pathParts := strings.ReplaceAll(path, "/", ":")
|
||||
id = domain + ":" + pathParts
|
||||
} else {
|
||||
id = domain
|
||||
}
|
||||
|
||||
return &DID{Method: DIDMethodWeb, ID: id}, nil
|
||||
}
|
||||
|
||||
// ExtractKeyMaterial extracts raw key bytes from did:key or did:lux
|
||||
func (d *DID) ExtractKeyMaterial() ([]byte, error) {
|
||||
if d == nil || d.ID == "" {
|
||||
return nil, fmt.Errorf("%w: empty identifier", ErrInvalidDID)
|
||||
}
|
||||
|
||||
if d.ID[0] != MultibaseBase58BTC {
|
||||
return nil, fmt.Errorf("%w: unsupported multibase encoding", ErrInvalidDID)
|
||||
}
|
||||
|
||||
// Decode base58btc (skip multibase prefix)
|
||||
decoded, err := base58Decode(d.ID[1:])
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("%w: %v", ErrInvalidDID, err)
|
||||
}
|
||||
|
||||
if len(decoded) < 2 {
|
||||
return nil, fmt.Errorf("%w: identifier too short", ErrInvalidDID)
|
||||
}
|
||||
|
||||
// Skip multicodec prefix if it matches ML-DSA-65
|
||||
if len(decoded) >= 2 && decoded[0] == MulticodecMLDSA65[0] && decoded[1] == MulticodecMLDSA65[1] {
|
||||
return decoded[2:], nil
|
||||
}
|
||||
|
||||
return decoded, nil
|
||||
}
|
||||
|
||||
// Hash returns a 32-byte hash of the DID for indexing
|
||||
func (d *DID) Hash() ids.ID {
|
||||
h := sha256.Sum256([]byte(d.String()))
|
||||
var id ids.ID
|
||||
copy(id[:], h[:])
|
||||
return id
|
||||
}
|
||||
|
||||
// ToHex returns the DID hash as a hex string
|
||||
func (d *DID) ToHex() string {
|
||||
id := d.Hash()
|
||||
return hex.EncodeToString(id[:])
|
||||
}
|
||||
|
||||
// VerificationMethod represents a verification method in a DID Document
|
||||
type VerificationMethod struct {
|
||||
ID string
|
||||
Type string
|
||||
Controller string
|
||||
PublicKeyMultibase string
|
||||
BlockchainAccountID string
|
||||
}
|
||||
|
||||
// Service represents a service endpoint in a DID Document
|
||||
type Service struct {
|
||||
ID string
|
||||
Type string
|
||||
ServiceEndpoint string
|
||||
}
|
||||
|
||||
// DIDDocument represents a W3C DID Document
|
||||
type DIDDocument struct {
|
||||
Context []string
|
||||
ID string
|
||||
Controller string
|
||||
VerificationMethod []VerificationMethod
|
||||
Authentication []string
|
||||
AssertionMethod []string
|
||||
KeyAgreement []string
|
||||
CapabilityInvocation []string
|
||||
CapabilityDelegation []string
|
||||
Service []Service
|
||||
}
|
||||
|
||||
// GenerateDocument creates a DID Document for a DID
|
||||
func (d *DID) GenerateDocument() (*DIDDocument, error) {
|
||||
if !d.Valid() {
|
||||
return nil, fmt.Errorf("%w: invalid DID", ErrInvalidDID)
|
||||
}
|
||||
|
||||
uri := d.String()
|
||||
keyID := uri + "#keys-1"
|
||||
|
||||
var vm VerificationMethod
|
||||
switch d.Method {
|
||||
case DIDMethodLux, DIDMethodKey:
|
||||
vm = VerificationMethod{
|
||||
ID: keyID,
|
||||
Type: "JsonWebKey2020",
|
||||
Controller: uri,
|
||||
PublicKeyMultibase: d.ID,
|
||||
}
|
||||
if d.Method == DIDMethodLux {
|
||||
// Add blockchain account ID
|
||||
keyMaterial, err := d.ExtractKeyMaterial()
|
||||
if err == nil && len(keyMaterial) >= 20 {
|
||||
vm.BlockchainAccountID = "lux:" + hex.EncodeToString(keyMaterial[:20])
|
||||
}
|
||||
}
|
||||
case DIDMethodWeb:
|
||||
vm = VerificationMethod{
|
||||
ID: keyID,
|
||||
Type: "JsonWebKey2020",
|
||||
Controller: uri,
|
||||
}
|
||||
}
|
||||
|
||||
return &DIDDocument{
|
||||
Context: []string{
|
||||
"https://www.w3.org/ns/did/v1",
|
||||
"https://w3id.org/security/suites/jws-2020/v1",
|
||||
},
|
||||
ID: uri,
|
||||
VerificationMethod: []VerificationMethod{vm},
|
||||
Authentication: []string{keyID},
|
||||
AssertionMethod: []string{keyID},
|
||||
CapabilityInvocation: []string{keyID},
|
||||
Service: []Service{
|
||||
{
|
||||
ID: uri + "#zap-agent",
|
||||
Type: "ZapAgent",
|
||||
ServiceEndpoint: "zap://" + d.ID,
|
||||
},
|
||||
},
|
||||
}, nil
|
||||
}
|
||||
|
||||
// Base58 encoding/decoding (Bitcoin alphabet)
|
||||
const base58Alphabet = "123456789ABCDEFGHJKLMNPQRSTUVWXYZabcdefghijkmnopqrstuvwxyz"
|
||||
|
||||
func base58Encode(data []byte) string {
|
||||
// Convert to big integer
|
||||
result := make([]byte, 0, len(data)*2)
|
||||
for _, b := range data {
|
||||
carry := int(b)
|
||||
for i := 0; i < len(result); i++ {
|
||||
carry += int(result[i]) << 8
|
||||
result[i] = byte(carry % 58)
|
||||
carry /= 58
|
||||
}
|
||||
for carry > 0 {
|
||||
result = append(result, byte(carry%58))
|
||||
carry /= 58
|
||||
}
|
||||
}
|
||||
|
||||
// Handle leading zeros
|
||||
for _, b := range data {
|
||||
if b != 0 {
|
||||
break
|
||||
}
|
||||
result = append(result, 0)
|
||||
}
|
||||
|
||||
// Reverse and convert to alphabet
|
||||
output := make([]byte, len(result))
|
||||
for i := range result {
|
||||
output[len(result)-1-i] = base58Alphabet[result[i]]
|
||||
}
|
||||
|
||||
return string(output)
|
||||
}
|
||||
|
||||
func base58Decode(s string) ([]byte, error) {
|
||||
result := make([]byte, 0, len(s))
|
||||
for _, c := range s {
|
||||
index := strings.IndexRune(base58Alphabet, c)
|
||||
if index == -1 {
|
||||
return nil, fmt.Errorf("invalid base58 character: %c", c)
|
||||
}
|
||||
|
||||
carry := index
|
||||
for i := 0; i < len(result); i++ {
|
||||
carry += int(result[i]) * 58
|
||||
result[i] = byte(carry)
|
||||
carry >>= 8
|
||||
}
|
||||
for carry > 0 {
|
||||
result = append(result, byte(carry))
|
||||
carry >>= 8
|
||||
}
|
||||
}
|
||||
|
||||
// Handle leading ones
|
||||
for _, c := range s {
|
||||
if c != rune(base58Alphabet[0]) {
|
||||
break
|
||||
}
|
||||
result = append(result, 0)
|
||||
}
|
||||
|
||||
// Reverse
|
||||
for i, j := 0, len(result)-1; i < j; i, j = i+1, j-1 {
|
||||
result[i], result[j] = result[j], result[i]
|
||||
}
|
||||
|
||||
return result, nil
|
||||
}
|
||||
@@ -1,260 +0,0 @@
|
||||
// Copyright (C) 2019-2025, Lux Industries Inc. All rights reserved.
|
||||
// See the file LICENSE for licensing terms.
|
||||
|
||||
package zap
|
||||
|
||||
import (
|
||||
"crypto/sha256"
|
||||
"encoding/hex"
|
||||
"time"
|
||||
)
|
||||
|
||||
// QueryState represents the state of a consensus query
|
||||
type QueryState struct {
|
||||
ID string
|
||||
Content []byte
|
||||
Submitter *DID
|
||||
Timestamp time.Time
|
||||
Responses map[string]*Response
|
||||
Votes map[string][]*DID // ResponseID -> list of voter DIDs
|
||||
Finalized string // ResponseID if finalized
|
||||
}
|
||||
|
||||
// Response represents a response to a query
|
||||
type Response struct {
|
||||
ID string
|
||||
QueryID string
|
||||
Content []byte
|
||||
Responder *DID
|
||||
Timestamp time.Time
|
||||
}
|
||||
|
||||
// ConsensusResult represents the outcome of consensus voting
|
||||
type ConsensusResult struct {
|
||||
Response *Response
|
||||
Votes int
|
||||
TotalVoters int
|
||||
Confidence float64
|
||||
}
|
||||
|
||||
// Query represents an agentic consensus query
|
||||
type Query struct {
|
||||
ID string
|
||||
Content []byte
|
||||
Submitter *DID
|
||||
Timestamp int64
|
||||
}
|
||||
|
||||
// NewQuery creates a query with auto-generated ID
|
||||
func NewQuery(content []byte, submitter *DID) *Query {
|
||||
timestamp := time.Now().Unix()
|
||||
data := append(content, []byte(submitter.String())...)
|
||||
data = append(data, int64ToBytes(timestamp)...)
|
||||
hash := sha256.Sum256(data)
|
||||
|
||||
return &Query{
|
||||
ID: hex.EncodeToString(hash[:]),
|
||||
Content: content,
|
||||
Submitter: submitter,
|
||||
Timestamp: timestamp,
|
||||
}
|
||||
}
|
||||
|
||||
// NewResponse creates a response with auto-generated ID
|
||||
func NewResponse(queryID string, content []byte, responder *DID) *Response {
|
||||
timestamp := time.Now()
|
||||
queryIDBytes, _ := hex.DecodeString(queryID)
|
||||
data := append(queryIDBytes, content...)
|
||||
data = append(data, []byte(responder.String())...)
|
||||
data = append(data, int64ToBytes(timestamp.Unix())...)
|
||||
hash := sha256.Sum256(data)
|
||||
|
||||
return &Response{
|
||||
ID: hex.EncodeToString(hash[:]),
|
||||
QueryID: queryID,
|
||||
Content: content,
|
||||
Responder: responder,
|
||||
Timestamp: timestamp,
|
||||
}
|
||||
}
|
||||
|
||||
// Vote represents a vote cast by a validator
|
||||
type Vote struct {
|
||||
QueryID string
|
||||
ResponseID string
|
||||
Voter *DID
|
||||
Timestamp int64
|
||||
Signature []byte // Optional post-quantum signature
|
||||
}
|
||||
|
||||
// NewVote creates a new vote
|
||||
func NewVote(queryID, responseID string, voter *DID) *Vote {
|
||||
return &Vote{
|
||||
QueryID: queryID,
|
||||
ResponseID: responseID,
|
||||
Voter: voter,
|
||||
Timestamp: time.Now().Unix(),
|
||||
}
|
||||
}
|
||||
|
||||
// FinalityProof represents proof of agentic consensus finality
|
||||
type FinalityProof struct {
|
||||
QueryID string
|
||||
ResponseID string
|
||||
Votes []Vote
|
||||
TotalVoters int
|
||||
Confidence float64
|
||||
Timestamp int64
|
||||
Signature []byte // Quasar hybrid signature
|
||||
}
|
||||
|
||||
// ValidatorWeight represents a validator's weight in consensus
|
||||
type ValidatorWeight struct {
|
||||
DID *DID
|
||||
Weight uint64
|
||||
Stake uint64
|
||||
Active bool
|
||||
}
|
||||
|
||||
// AgentMessage represents a ZAP protocol message for agentic consensus
|
||||
type AgentMessage struct {
|
||||
Type AgentMessageType
|
||||
Query *Query
|
||||
Response *Response
|
||||
Vote *Vote
|
||||
Signature []byte
|
||||
}
|
||||
|
||||
// AgentMessageType identifies the type of agent message
|
||||
type AgentMessageType uint8
|
||||
|
||||
const (
|
||||
AgentMessageTypeQuery AgentMessageType = iota
|
||||
AgentMessageTypeResponse
|
||||
AgentMessageTypeVote
|
||||
AgentMessageTypeFinality
|
||||
)
|
||||
|
||||
// String returns the message type name
|
||||
func (t AgentMessageType) String() string {
|
||||
switch t {
|
||||
case AgentMessageTypeQuery:
|
||||
return "Query"
|
||||
case AgentMessageTypeResponse:
|
||||
return "Response"
|
||||
case AgentMessageTypeVote:
|
||||
return "Vote"
|
||||
case AgentMessageTypeFinality:
|
||||
return "Finality"
|
||||
default:
|
||||
return "Unknown"
|
||||
}
|
||||
}
|
||||
|
||||
func int64ToBytes(n int64) []byte {
|
||||
b := make([]byte, 8)
|
||||
for i := 0; i < 8; i++ {
|
||||
b[i] = byte(n >> (i * 8))
|
||||
}
|
||||
return b
|
||||
}
|
||||
|
||||
// PQSignatureType identifies the post-quantum signature algorithm
|
||||
type PQSignatureType uint8
|
||||
|
||||
const (
|
||||
// PQSignatureTypeMLDSA65 is NIST FIPS 204 ML-DSA-65
|
||||
PQSignatureTypeMLDSA65 PQSignatureType = iota
|
||||
// PQSignatureTypeCorona is Ring-LWE based threshold signatures
|
||||
PQSignatureTypeCorona
|
||||
// PQSignatureTypeHybrid combines classical and post-quantum
|
||||
PQSignatureTypeHybrid
|
||||
)
|
||||
|
||||
// PQSignature wraps a post-quantum signature
|
||||
type PQSignature struct {
|
||||
Type PQSignatureType
|
||||
Signature []byte
|
||||
PublicKey []byte
|
||||
}
|
||||
|
||||
// PQKeypair represents a post-quantum keypair
|
||||
type PQKeypair struct {
|
||||
Type PQSignatureType
|
||||
PublicKey []byte
|
||||
PrivateKey []byte
|
||||
}
|
||||
|
||||
// Sign signs a message using the keypair (stub - real impl in pqcrypto)
|
||||
func (k *PQKeypair) Sign(message []byte) (*PQSignature, error) {
|
||||
// Stub: returns SHA-256 hash as fixed-size signature for testing
|
||||
hash := sha256.Sum256(append(message, k.PrivateKey...))
|
||||
return &PQSignature{
|
||||
Type: k.Type,
|
||||
Signature: hash[:],
|
||||
PublicKey: k.PublicKey,
|
||||
}, nil
|
||||
}
|
||||
|
||||
// Verify verifies a signature (stub - real impl in pqcrypto)
|
||||
func (k *PQKeypair) Verify(message []byte, sig *PQSignature) bool {
|
||||
// Stub: accepts any non-empty signature
|
||||
return sig != nil && len(sig.Signature) > 0
|
||||
}
|
||||
|
||||
// StakeRegistry interface for validator stake tracking
|
||||
type StakeRegistry interface {
|
||||
GetStake(did *DID) (uint64, error)
|
||||
SetStake(did *DID, amount uint64) error
|
||||
TotalStake() uint64
|
||||
HasSufficientStake(did *DID, minimum uint64) (bool, error)
|
||||
StakeWeight(did *DID) (float64, error)
|
||||
}
|
||||
|
||||
// InMemoryStakeRegistry is a simple in-memory stake registry for testing
|
||||
type InMemoryStakeRegistry struct {
|
||||
stakes map[string]uint64
|
||||
}
|
||||
|
||||
// NewInMemoryStakeRegistry creates a new in-memory stake registry
|
||||
func NewInMemoryStakeRegistry() *InMemoryStakeRegistry {
|
||||
return &InMemoryStakeRegistry{
|
||||
stakes: make(map[string]uint64),
|
||||
}
|
||||
}
|
||||
|
||||
// GetStake returns the stake for a DID
|
||||
func (r *InMemoryStakeRegistry) GetStake(did *DID) (uint64, error) {
|
||||
return r.stakes[did.String()], nil
|
||||
}
|
||||
|
||||
// SetStake sets the stake for a DID
|
||||
func (r *InMemoryStakeRegistry) SetStake(did *DID, amount uint64) error {
|
||||
r.stakes[did.String()] = amount
|
||||
return nil
|
||||
}
|
||||
|
||||
// TotalStake returns the total stake across all validators
|
||||
func (r *InMemoryStakeRegistry) TotalStake() uint64 {
|
||||
var total uint64
|
||||
for _, stake := range r.stakes {
|
||||
total += stake
|
||||
}
|
||||
return total
|
||||
}
|
||||
|
||||
// HasSufficientStake checks if a DID has at least the minimum stake
|
||||
func (r *InMemoryStakeRegistry) HasSufficientStake(did *DID, minimum uint64) (bool, error) {
|
||||
stake, _ := r.GetStake(did)
|
||||
return stake >= minimum, nil
|
||||
}
|
||||
|
||||
// StakeWeight returns the stake weight as a fraction of total stake
|
||||
func (r *InMemoryStakeRegistry) StakeWeight(did *DID) (float64, error) {
|
||||
stake, _ := r.GetStake(did)
|
||||
total := r.TotalStake()
|
||||
if total == 0 {
|
||||
return 0.0, nil
|
||||
}
|
||||
return float64(stake) / float64(total), nil
|
||||
}
|
||||
@@ -86,7 +86,6 @@ import (
|
||||
platformconfig "github.com/luxfi/node/vms/platformvm/config"
|
||||
|
||||
gpuconfig "github.com/luxfi/node/config"
|
||||
"github.com/luxfi/node/consensus/quasar"
|
||||
)
|
||||
|
||||
const (
|
||||
@@ -329,11 +328,6 @@ func New(
|
||||
return nil, fmt.Errorf("couldn't initialize chains: %w", err)
|
||||
}
|
||||
|
||||
// Initialize Quasar hybrid finality engine if Q-Chain is in genesis
|
||||
if err := n.initQuasar(); err != nil {
|
||||
n.Log.Warn("quasar init skipped", "error", err)
|
||||
}
|
||||
|
||||
return n, nil
|
||||
}
|
||||
|
||||
@@ -454,9 +448,6 @@ type Node struct {
|
||||
// Manages shutdown of a VM process
|
||||
runtimeManager runtime.Manager
|
||||
|
||||
// Quasar hybrid finality engine — binds P-Chain BLS + Q-Chain Corona
|
||||
Quasar *quasar.Quasar
|
||||
|
||||
resourceManager resource.Manager
|
||||
|
||||
// Tracks the CPU/disk usage caused by processing
|
||||
@@ -2150,9 +2141,6 @@ func (n *Node) shutdown() {
|
||||
time.Sleep(n.Config.ShutdownWait)
|
||||
}
|
||||
|
||||
if n.Quasar != nil {
|
||||
n.Quasar.Stop()
|
||||
}
|
||||
if n.resourceManager != nil {
|
||||
n.resourceManager.Shutdown()
|
||||
}
|
||||
|
||||
-117
@@ -1,117 +0,0 @@
|
||||
// Copyright (C) 2019-2025, Lux Industries Inc. All rights reserved.
|
||||
// See the file LICENSE for licensing terms.
|
||||
|
||||
package node
|
||||
|
||||
import (
|
||||
"context"
|
||||
"fmt"
|
||||
|
||||
"github.com/luxfi/constants"
|
||||
"github.com/luxfi/crypto/bls"
|
||||
"github.com/luxfi/ids"
|
||||
"github.com/luxfi/node/consensus/quasar"
|
||||
"github.com/luxfi/node/genesis/builder"
|
||||
nodevalidators "github.com/luxfi/validators"
|
||||
)
|
||||
|
||||
// initQuasar creates and starts the Quasar hybrid finality engine.
|
||||
// Quasar binds P-Chain BLS finality with Q-Chain Corona threshold
|
||||
// signatures for post-quantum secure block finality.
|
||||
//
|
||||
// Requires Q-Chain to be present in genesis. If absent, returns an error
|
||||
// and the caller logs a warning — the node operates without hybrid finality.
|
||||
func (n *Node) initQuasar() error {
|
||||
createQVMTx, err := builder.VMGenesis(n.Config.GenesisBytes, constants.QuantumVMID)
|
||||
if err != nil {
|
||||
return fmt.Errorf("Q-Chain not in genesis: %w", err)
|
||||
}
|
||||
qChainID := createQVMTx.ID()
|
||||
|
||||
// BLS quorum: 2/3 validator weight required
|
||||
// Corona threshold: 2 (minimum for threshold signing)
|
||||
q, err := quasar.NewQuasar(n.Log, 2, 2, 3)
|
||||
if err != nil {
|
||||
return fmt.Errorf("quasar create: %w", err)
|
||||
}
|
||||
|
||||
// Wire P-Chain provider — delivers validator state from PlatformVM's
|
||||
// validator manager (n.vdrs is populated by initValidatorSets() from
|
||||
// genesis + on-chain stakers). Finality events are emitted by
|
||||
// PChainAcceptedSubscriber once block acceptance is wired; for now the
|
||||
// channel exists but is not driven from outside (Run loop blocks on it).
|
||||
provider := &pChainProvider{
|
||||
nodeID: n.ID,
|
||||
vdrs: n.vdrs,
|
||||
finCh: make(chan quasar.FinalityEvent, 64),
|
||||
}
|
||||
q.ConnectPChain(provider)
|
||||
|
||||
// Wire quantum fallback signer using the node's BLS key.
|
||||
// Satisfies the Start() precondition (quantumFallback != nil).
|
||||
// Corona threshold signing supersedes this once initialized.
|
||||
q.ConnectQuantumFallback(&blsQuantumFallback{
|
||||
signer: n.Config.StakingSigningKey,
|
||||
})
|
||||
|
||||
if err := q.Start(context.Background()); err != nil {
|
||||
return fmt.Errorf("quasar start: %w", err)
|
||||
}
|
||||
|
||||
n.Quasar = q
|
||||
n.Log.Info("quasar hybrid finality engine started",
|
||||
"qChainID", qChainID,
|
||||
"quorum", "2/3",
|
||||
"threshold", 2,
|
||||
)
|
||||
return nil
|
||||
}
|
||||
|
||||
// pChainProvider adapts the node's PlatformVM-backed validator manager to
|
||||
// quasar.PChainProvider. GetValidators reads the live primary-network set
|
||||
// (BLS pubkey + light/weight) so Quasar's t-of-n threshold tracks the real
|
||||
// staker set as it changes. Finality events are pushed via finCh by the
|
||||
// block-acceptance bridge (wired separately).
|
||||
type pChainProvider struct {
|
||||
nodeID ids.NodeID
|
||||
vdrs nodevalidators.Manager
|
||||
finCh chan quasar.FinalityEvent
|
||||
}
|
||||
|
||||
func (p *pChainProvider) GetFinalizedHeight() uint64 { return 0 }
|
||||
|
||||
func (p *pChainProvider) GetValidators(_ uint64) ([]quasar.ValidatorState, error) {
|
||||
if p.vdrs == nil {
|
||||
return nil, fmt.Errorf("validator manager not initialized")
|
||||
}
|
||||
vmap := p.vdrs.GetMap(constants.PrimaryNetworkID)
|
||||
out := make([]quasar.ValidatorState, 0, len(vmap))
|
||||
for nodeID, v := range vmap {
|
||||
// PublicKey is already serialized BLS bytes from validators.GetValidatorOutput.
|
||||
out = append(out, quasar.ValidatorState{
|
||||
NodeID: nodeID,
|
||||
Weight: v.Weight,
|
||||
BLSPubKey: v.PublicKey,
|
||||
Active: true,
|
||||
})
|
||||
}
|
||||
return out, nil
|
||||
}
|
||||
|
||||
func (p *pChainProvider) SubscribeFinality() <-chan quasar.FinalityEvent {
|
||||
return p.finCh
|
||||
}
|
||||
|
||||
// blsQuantumFallback wraps the node's BLS signer to satisfy
|
||||
// quasar.QuantumSignerFallback.
|
||||
type blsQuantumFallback struct {
|
||||
signer bls.Signer
|
||||
}
|
||||
|
||||
func (f *blsQuantumFallback) SignMessage(msg []byte) ([]byte, error) {
|
||||
sig, err := f.signer.Sign(msg)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("BLS sign: %w", err)
|
||||
}
|
||||
return bls.SignatureToBytes(sig), nil
|
||||
}
|
||||
@@ -46,7 +46,7 @@ func countContaining(deps []string, needle string) int {
|
||||
//
|
||||
// The four core VMs are deliberately NOT asserted here: P(platformvm) and
|
||||
// X(xvm) are foundational primary-network VMs, and Q(quantumvm)/Z(zkvm) stay
|
||||
// in-process for now (Q backs Quasar hybrid finality + is critical by default).
|
||||
// in-process for now (Q is the post-quantum chain and is critical by default).
|
||||
// Turning Q/Z into plugins is a separate design-first phase.
|
||||
func TestOptionalVMsNotLinkedInProcess(t *testing.T) {
|
||||
optionalVMs := []string{
|
||||
|
||||
@@ -4,7 +4,6 @@
|
||||
// Package pq provides P+Q (Post-Quantum) integration tests for Lux network.
|
||||
//
|
||||
// These tests verify that all post-quantum security measures are enforced:
|
||||
// - Q-chain validators require RTSignature (Corona) in consensus votes
|
||||
// - TLS connections use X25519MLKEM768 hybrid key exchange (no fallback)
|
||||
// - SignedHost uses DNS hostnames only (no IP literals)
|
||||
// - ML-DSA signatures work for X-Chain UTXOs
|
||||
@@ -14,28 +13,21 @@ package pq
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"context"
|
||||
"crypto/ecdsa"
|
||||
"crypto/elliptic"
|
||||
"crypto/rand"
|
||||
"crypto/tls"
|
||||
"crypto/x509"
|
||||
"crypto/x509/pkix"
|
||||
"github.com/go-json-experiment/json"
|
||||
"errors"
|
||||
"github.com/go-json-experiment/json"
|
||||
"math/big"
|
||||
"strings"
|
||||
"sync"
|
||||
"testing"
|
||||
"time"
|
||||
|
||||
"github.com/stretchr/testify/require"
|
||||
|
||||
"github.com/luxfi/crypto/bls"
|
||||
"github.com/luxfi/ids"
|
||||
"github.com/luxfi/log"
|
||||
|
||||
"github.com/luxfi/node/consensus/quasar"
|
||||
"github.com/luxfi/node/network/peer"
|
||||
)
|
||||
|
||||
@@ -43,13 +35,6 @@ import (
|
||||
// Test Constants
|
||||
// ----------------------------------------------------------------------------
|
||||
|
||||
const (
|
||||
testValidatorCount = 5
|
||||
testQuorumNum = 2
|
||||
testQuorumDen = 3
|
||||
testThreshold = 3
|
||||
)
|
||||
|
||||
// ML-DSA security level constants
|
||||
const (
|
||||
mldsaSecLevel44 = 0 // 128-bit security
|
||||
@@ -67,371 +52,6 @@ const (
|
||||
mldsa87SigLen = 4627
|
||||
)
|
||||
|
||||
// ----------------------------------------------------------------------------
|
||||
// Test Validator Infrastructure
|
||||
// ----------------------------------------------------------------------------
|
||||
|
||||
// testValidator represents a validator node for integration testing.
|
||||
type testValidator struct {
|
||||
nodeID ids.NodeID
|
||||
blsKey *bls.SecretKey
|
||||
blsPubKey *bls.PublicKey
|
||||
rtKey []byte // ML-DSA-65 public key
|
||||
rtPrivKey []byte // ML-DSA-65 private key (for signing)
|
||||
weight uint64
|
||||
active bool
|
||||
}
|
||||
|
||||
// newTestValidator creates a test validator with all required keys.
|
||||
func newTestValidator(weight uint64) (*testValidator, error) {
|
||||
// Generate BLS keypair
|
||||
blsKey, err := bls.NewSecretKey()
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
blsPubKey := bls.PublicFromSecretKey(blsKey)
|
||||
|
||||
// Generate mock ML-DSA-65 keys (in production, use actual ML-DSA)
|
||||
rtKey := make([]byte, mldsa65PubKeyLen)
|
||||
rtPrivKey := make([]byte, mldsa65PubKeyLen) // Simplified for test
|
||||
if _, err := rand.Read(rtKey); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
if _, err := rand.Read(rtPrivKey); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
return &testValidator{
|
||||
nodeID: ids.GenerateTestNodeID(),
|
||||
blsKey: blsKey,
|
||||
blsPubKey: blsPubKey,
|
||||
rtKey: rtKey,
|
||||
rtPrivKey: rtPrivKey,
|
||||
weight: weight,
|
||||
active: true,
|
||||
}, nil
|
||||
}
|
||||
|
||||
// toValidatorState converts to quasar.ValidatorState.
|
||||
func (v *testValidator) toValidatorState() quasar.ValidatorState {
|
||||
return quasar.ValidatorState{
|
||||
NodeID: v.nodeID,
|
||||
Weight: v.weight,
|
||||
BLSPubKey: bls.PublicKeyToCompressedBytes(v.blsPubKey),
|
||||
CoronaKey: v.rtKey,
|
||||
Active: v.active,
|
||||
}
|
||||
}
|
||||
|
||||
// testValidatorSet creates a set of test validators.
|
||||
func testValidatorSet(n int) ([]*testValidator, error) {
|
||||
validators := make([]*testValidator, n)
|
||||
for i := 0; i < n; i++ {
|
||||
v, err := newTestValidator(1000)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
validators[i] = v
|
||||
}
|
||||
return validators, nil
|
||||
}
|
||||
|
||||
// toValidatorStates converts validators to quasar.ValidatorState slice.
|
||||
func toValidatorStates(validators []*testValidator) []quasar.ValidatorState {
|
||||
states := make([]quasar.ValidatorState, len(validators))
|
||||
for i, v := range validators {
|
||||
states[i] = v.toValidatorState()
|
||||
}
|
||||
return states
|
||||
}
|
||||
|
||||
// ----------------------------------------------------------------------------
|
||||
// Mock P-Chain Provider
|
||||
// ----------------------------------------------------------------------------
|
||||
|
||||
type mockPChainProvider struct {
|
||||
mu sync.RWMutex
|
||||
height uint64
|
||||
validators []quasar.ValidatorState
|
||||
finalityCh chan quasar.FinalityEvent
|
||||
closed bool
|
||||
}
|
||||
|
||||
func newMockPChainProvider(validators []quasar.ValidatorState) *mockPChainProvider {
|
||||
return &mockPChainProvider{
|
||||
height: 0,
|
||||
validators: validators,
|
||||
finalityCh: make(chan quasar.FinalityEvent, 100),
|
||||
}
|
||||
}
|
||||
|
||||
func (m *mockPChainProvider) GetFinalizedHeight() uint64 {
|
||||
m.mu.RLock()
|
||||
defer m.mu.RUnlock()
|
||||
return m.height
|
||||
}
|
||||
|
||||
func (m *mockPChainProvider) GetValidators(height uint64) ([]quasar.ValidatorState, error) {
|
||||
m.mu.RLock()
|
||||
defer m.mu.RUnlock()
|
||||
return m.validators, nil
|
||||
}
|
||||
|
||||
func (m *mockPChainProvider) SubscribeFinality() <-chan quasar.FinalityEvent {
|
||||
return m.finalityCh
|
||||
}
|
||||
|
||||
func (m *mockPChainProvider) EmitFinality(event quasar.FinalityEvent) {
|
||||
m.mu.Lock()
|
||||
defer m.mu.Unlock()
|
||||
if m.closed {
|
||||
return
|
||||
}
|
||||
m.height = event.Height
|
||||
select {
|
||||
case m.finalityCh <- event:
|
||||
default:
|
||||
}
|
||||
}
|
||||
|
||||
func (m *mockPChainProvider) Close() {
|
||||
m.mu.Lock()
|
||||
defer m.mu.Unlock()
|
||||
if !m.closed {
|
||||
m.closed = true
|
||||
close(m.finalityCh)
|
||||
}
|
||||
}
|
||||
|
||||
// ----------------------------------------------------------------------------
|
||||
// SECTION 1: Q-Chain Validator Network Tests
|
||||
// ----------------------------------------------------------------------------
|
||||
|
||||
// TestQChainValidatorNetwork tests 5-node Q-chain validator consensus.
|
||||
func TestQChainValidatorNetwork(t *testing.T) {
|
||||
t.Run("5_node_initialization", func(t *testing.T) {
|
||||
validators, err := testValidatorSet(testValidatorCount)
|
||||
require.NoError(t, err, "failed to create validators")
|
||||
require.Len(t, validators, testValidatorCount)
|
||||
|
||||
// Verify each validator has required key material
|
||||
for i, v := range validators {
|
||||
require.NotNil(t, v.blsKey, "validator %d missing BLS key", i)
|
||||
require.NotNil(t, v.blsPubKey, "validator %d missing BLS pubkey", i)
|
||||
require.Len(t, v.rtKey, mldsa65PubKeyLen, "validator %d RT key wrong length", i)
|
||||
}
|
||||
})
|
||||
|
||||
t.Run("quasar_with_validators", func(t *testing.T) {
|
||||
validators, err := testValidatorSet(testValidatorCount)
|
||||
require.NoError(t, err)
|
||||
|
||||
states := toValidatorStates(validators)
|
||||
pchain := newMockPChainProvider(states)
|
||||
defer pchain.Close()
|
||||
|
||||
q, err := quasar.NewQuasar(log.NewNoOpLogger(), testThreshold, testQuorumNum, testQuorumDen)
|
||||
require.NoError(t, err)
|
||||
|
||||
q.ConnectPChain(pchain)
|
||||
|
||||
// Initialize Corona with node IDs
|
||||
nodeIDs := make([]ids.NodeID, len(validators))
|
||||
for i, v := range validators {
|
||||
nodeIDs[i] = v.nodeID
|
||||
}
|
||||
|
||||
// Corona init may fail due to lattice lib constraints in test env
|
||||
err = q.InitializeCorona(nodeIDs)
|
||||
if err != nil {
|
||||
t.Skipf("Skipping: Corona initialization requires lattice library: %v", err)
|
||||
}
|
||||
|
||||
stats := q.Stats()
|
||||
require.True(t, stats.CoronaReady, "Corona should be initialized")
|
||||
})
|
||||
|
||||
t.Run("consensus_starts_and_stops", func(t *testing.T) {
|
||||
validators, err := testValidatorSet(testValidatorCount)
|
||||
require.NoError(t, err)
|
||||
|
||||
states := toValidatorStates(validators)
|
||||
pchain := newMockPChainProvider(states)
|
||||
defer pchain.Close()
|
||||
|
||||
q, err := quasar.NewQuasar(log.NewNoOpLogger(), testThreshold, testQuorumNum, testQuorumDen)
|
||||
require.NoError(t, err)
|
||||
|
||||
q.ConnectPChain(pchain)
|
||||
q.ConnectQuantumFallback(&mockQuantumSigner{})
|
||||
|
||||
ctx, cancel := context.WithCancel(context.Background())
|
||||
defer cancel()
|
||||
|
||||
err = q.Start(ctx)
|
||||
require.NoError(t, err)
|
||||
|
||||
// Verify running state
|
||||
require.True(t, q.IsRunning(), "Quasar should be running")
|
||||
|
||||
// Emit finality event (it will be queued even if not fully processed)
|
||||
var blockID ids.ID
|
||||
_, _ = rand.Read(blockID[:])
|
||||
|
||||
event := quasar.FinalityEvent{
|
||||
Height: 1,
|
||||
BlockID: blockID,
|
||||
Validators: states,
|
||||
Timestamp: time.Now(),
|
||||
}
|
||||
pchain.EmitFinality(event)
|
||||
|
||||
// Give event time to be received
|
||||
time.Sleep(100 * time.Millisecond)
|
||||
|
||||
// Clean stop
|
||||
q.Stop()
|
||||
require.False(t, q.IsRunning(), "Quasar should stop cleanly")
|
||||
})
|
||||
|
||||
t.Run("manual_finality_set", func(t *testing.T) {
|
||||
// Test that we can manually set finality entries (simulates successful finality)
|
||||
q, err := quasar.NewQuasar(log.NewNoOpLogger(), testThreshold, testQuorumNum, testQuorumDen)
|
||||
require.NoError(t, err)
|
||||
|
||||
var blockID ids.ID
|
||||
_, _ = rand.Read(blockID[:])
|
||||
|
||||
// Create a valid finality with both proofs
|
||||
finality := &quasar.QuantumFinality{
|
||||
BlockID: blockID,
|
||||
PChainHeight: 1,
|
||||
QChainHeight: 1,
|
||||
BLSProof: make([]byte, 96),
|
||||
CoronaProof: make([]byte, mldsa65SigLen),
|
||||
TotalWeight: 1000,
|
||||
SignerWeight: 700, // 70% > 67% quorum
|
||||
}
|
||||
|
||||
q.SetFinalized(blockID, finality)
|
||||
|
||||
// Verify we can retrieve it
|
||||
retrieved, found := q.GetFinality(blockID)
|
||||
require.True(t, found, "should find finalized block")
|
||||
require.Equal(t, finality.BlockID, retrieved.BlockID)
|
||||
|
||||
stats := q.Stats()
|
||||
require.GreaterOrEqual(t, stats.FinalizedBlocks, 1, "should have at least 1 finalized block")
|
||||
})
|
||||
}
|
||||
|
||||
// mockQuantumSigner provides mock RT signing for tests.
|
||||
type mockQuantumSigner struct{}
|
||||
|
||||
func (m *mockQuantumSigner) SignMessage(msg []byte) ([]byte, error) {
|
||||
return []byte("RT-MOCK-SIG"), nil
|
||||
}
|
||||
|
||||
// ----------------------------------------------------------------------------
|
||||
// SECTION 2: RTSignature Enforcement Tests
|
||||
// ----------------------------------------------------------------------------
|
||||
|
||||
// TestRTSignatureRequired verifies RTSignature is REQUIRED at consensus-critical boundaries.
|
||||
func TestRTSignatureRequired(t *testing.T) {
|
||||
t.Run("vote_without_rt_rejected", func(t *testing.T) {
|
||||
// Create a vote message without RTSignature
|
||||
vote := &testVoteMessage{
|
||||
blockID: ids.GenerateTestID(),
|
||||
height: 1,
|
||||
blsSignature: []byte("valid-bls-sig"),
|
||||
rtSignature: nil, // Missing!
|
||||
}
|
||||
|
||||
// Validate vote - should fail
|
||||
err := validateVoteMessage(vote)
|
||||
require.Error(t, err, "vote without RTSignature should be rejected")
|
||||
require.Contains(t, err.Error(), "RTSignature required")
|
||||
})
|
||||
|
||||
t.Run("vote_with_empty_rt_rejected", func(t *testing.T) {
|
||||
vote := &testVoteMessage{
|
||||
blockID: ids.GenerateTestID(),
|
||||
height: 1,
|
||||
blsSignature: []byte("valid-bls-sig"),
|
||||
rtSignature: []byte{}, // Empty!
|
||||
}
|
||||
|
||||
err := validateVoteMessage(vote)
|
||||
require.Error(t, err, "vote with empty RTSignature should be rejected")
|
||||
})
|
||||
|
||||
t.Run("vote_with_invalid_rt_length_rejected", func(t *testing.T) {
|
||||
vote := &testVoteMessage{
|
||||
blockID: ids.GenerateTestID(),
|
||||
height: 1,
|
||||
blsSignature: []byte("valid-bls-sig"),
|
||||
rtSignature: []byte("too-short"), // Wrong length
|
||||
}
|
||||
|
||||
err := validateVoteMessage(vote)
|
||||
require.Error(t, err, "vote with invalid RTSignature length should be rejected")
|
||||
})
|
||||
|
||||
t.Run("vote_with_valid_rt_accepted", func(t *testing.T) {
|
||||
rtSig := make([]byte, mldsa65SigLen)
|
||||
_, _ = rand.Read(rtSig)
|
||||
|
||||
vote := &testVoteMessage{
|
||||
blockID: ids.GenerateTestID(),
|
||||
height: 1,
|
||||
blsSignature: []byte("valid-bls-sig"),
|
||||
rtSignature: rtSig,
|
||||
}
|
||||
|
||||
err := validateVoteMessage(vote)
|
||||
require.NoError(t, err, "vote with valid RTSignature should be accepted")
|
||||
})
|
||||
|
||||
t.Run("finality_without_both_proofs_rejected", func(t *testing.T) {
|
||||
// Finality requires both BLS and RT proofs
|
||||
finality := &quasar.QuantumFinality{
|
||||
BlockID: ids.GenerateTestID(),
|
||||
BLSProof: []byte("bls-proof"),
|
||||
CoronaProof: nil, // Missing RT proof!
|
||||
TotalWeight: 1000,
|
||||
SignerWeight: 700,
|
||||
}
|
||||
|
||||
q, _ := quasar.NewQuasar(log.NewNoOpLogger(), testThreshold, testQuorumNum, testQuorumDen)
|
||||
err := q.Verify(finality)
|
||||
require.Error(t, err, "finality without RT proof should be rejected")
|
||||
})
|
||||
}
|
||||
|
||||
// testVoteMessage simulates a consensus vote message.
|
||||
type testVoteMessage struct {
|
||||
blockID ids.ID
|
||||
height uint64
|
||||
blsSignature []byte
|
||||
rtSignature []byte
|
||||
}
|
||||
|
||||
// validateVoteMessage validates a vote message for Q-chain consensus.
|
||||
func validateVoteMessage(vote *testVoteMessage) error {
|
||||
// RTSignature is REQUIRED for Q-chain validators
|
||||
if vote.rtSignature == nil {
|
||||
return errors.New("RTSignature required for Q-chain consensus vote")
|
||||
}
|
||||
if len(vote.rtSignature) == 0 {
|
||||
return errors.New("RTSignature cannot be empty")
|
||||
}
|
||||
// Check signature length matches ML-DSA-65
|
||||
if len(vote.rtSignature) != mldsa65SigLen {
|
||||
return errors.New("RTSignature has invalid length for ML-DSA-65")
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
// ----------------------------------------------------------------------------
|
||||
// SECTION 3: PQ-Only TLS Tests
|
||||
// ----------------------------------------------------------------------------
|
||||
@@ -605,8 +225,8 @@ func TestHostnameOnlyAddressing(t *testing.T) {
|
||||
func TestMLDSACredential(t *testing.T) {
|
||||
t.Run("security_level_signature_lengths", func(t *testing.T) {
|
||||
testCases := []struct {
|
||||
level int
|
||||
sigLen int
|
||||
level int
|
||||
sigLen int
|
||||
pubKeyLen int
|
||||
}{
|
||||
{mldsaSecLevel44, mldsa44SigLen, mldsa44PubKeyLen},
|
||||
|
||||
Reference in New Issue
Block a user