mirror of
https://github.com/luxfi/vm.git
synced 2026-10-30 15:13:40 +00:00
435 lines
10 KiB
Go
435 lines
10 KiB
Go
// 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()
|
|
}
|
|
}
|