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