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:
zeekay
2026-06-26 19:28:34 -07:00
parent bd42106be3
commit ad7ec6e5f3
20 changed files with 4 additions and 7183 deletions
-353
View File
@@ -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
}
-434
View File
@@ -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()
}
}
-306
View File
@@ -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())
}
}
-59
View File
@@ -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
-627
View File
@@ -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
}
-436
View File
@@ -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)
}
}
-970
View File
@@ -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")
})
}
-195
View File
@@ -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
}
-469
View File
@@ -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
}
-448
View File
@@ -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")
}
-681
View File
@@ -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))
}
}
-240
View File
@@ -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
}
-378
View File
@@ -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
}
-459
View File
@@ -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)
}
-355
View File
@@ -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
}
-260
View File
@@ -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
}