Files
vm/consensus/quasar/integration_test.go

966 lines
26 KiB
Go

// 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) 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
// ----------------------------------------------------------------------------
// generateIntegrationValidators creates n validators with random IDs
func generateIntegrationValidators(n int) []ids.NodeID {
validators := make([]ids.NodeID, n)
for i := range validators {
validators[i] = ids.GenerateTestNodeID()
}
return validators
}
// 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 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{})
// 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) {
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)
defer q.Stop()
// Emit event
event := createTestEvent(1, validatorStates)
pchain.EmitFinality(event)
// Wait for processing
time.Sleep(100 * time.Millisecond)
stats := q.Stats()
require.Equal(t, uint64(1), stats.PChainHeight, "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
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)
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")
})
}