Files
zeekay cf73ba3c48 consensus(chain): round-2/3 hardening — epoch-bind certs, SyncState monotonic guard, BFT msg, drop dead witness
MEDIUM (epoch binding): stake-weighted finality must be measured at the
cert-position epoch, not at verification time. Bind the active weighted
validator-set commitment into VotePosition.ValidatorSetRoot and fold it into
CanonicalVoteMessage + the wire codec, so a cert is cryptographically pinned to
the exact set it was certified under: re-presenting an epoch-E cert as
certifying under a different epoch's set fails clause-6 signature verification,
and a later stake change cannot retroactively flip an already-correct cert.
Mirrors quasar.ConsensusCert.ValidatorSetRoot. New optional ValidatorSetRootSource
(WithValidatorSetRoot / NetworkConfig.ValidatorSetRoot), stamped in
blockPositionLocked; nil source => ids.Empty => behavior identical to before
(backward-safe for fixed-set chains). QuorumCertVersion bumped 1->2 so a
mixed-version cert fails loudly (version + signature) rather than mis-parsing —
a deliberate forward-only break (the set upgrades together in one cascade).
TDD: TestEpochBinding_{CrossEpochCertRejected,StakeReadAtCertHeight,RoundTripPreservesRoot}
(proven to fail without the canonical-message binding).

INFO-2 (SyncState monotonic guard): ChainConsensus.SyncState bypasses
markFinalizedLocked by design (import is an out-of-band reconcile), so the
monotonic invariant is re-asserted explicitly — a backward import (height below
the already-finalized height) is refused (ErrSyncStateRegression) and leaves
finalized state untouched; an equal-height re-import must agree on the block;
forward + empty-reset still succeed. SyncState now returns error; the sole caller
(Transitive.SyncState) propagates it and does NOT partially mutate on refusal.
TDD: TestSyncState_{RefusesBackwardRegression,EqualHeightDifferentBlockRefused,
ForwardImportAdvances,EmptyResetAllowed} (proven to fail without the guard).

INFO-3 (BFT message): ErrKTooLowForMainnet/Testnet now state the real bound
(2*AlphaPreference-K >= floor((K-1)/3)+1 with f>=3 / f>=1) instead of a bare
"K>=11" magic number. Sentinel-based assertions unaffected.

INFO-1 (dead code): delete the VerkleWitness apparatus (witness.go + its tests +
the VerkleWitness funcs in comprehensive_test.go + the verifyIPAOpening test) —
zero non-test callers; verifyBLSAggregate was a deserialize-only (no-pairing)
forgery sink if ever revived. The live deserialization path that matters is
crypto/bls.SignatureFromBytes (fixed + tested in the crypto module);
TestVerifyBLSAggregate_C1_ActualDeserialization was proven green in CGO_ENABLED=0
against the crypto fix BEFORE this deletion removed it.
2026-06-22 18:49:19 -07:00

2498 lines
62 KiB
Go

// Copyright (C) 2025, Lux Industries Inc All rights reserved.
// Comprehensive tests for Quasar consensus protocol
// Target: 90%+ coverage
package quasar
import (
"context"
"testing"
"time"
"github.com/luxfi/consensus/config"
"github.com/luxfi/crypto/banderwagon"
"github.com/luxfi/crypto/bls"
"github.com/luxfi/crypto/threshold"
)
// validStateRoot returns a 32-byte slice that is a valid banderwagon point
// encoding. The i parameter produces distinct roots by scalar multiplication.
func validStateRoot(t *testing.T, i int) []byte {
t.Helper()
var p banderwagon.Element
p.Add(&banderwagon.Generator, &banderwagon.Generator)
// Multiply by (i+1) to get distinct points
for j := 0; j < i; j++ {
p.Add(&p, &banderwagon.Generator)
}
b := p.Bytes()
return b[:]
}
// validBLSSig generates a real BLS signature for testing.
func validBLSSig(t *testing.T, msg []byte) []byte {
t.Helper()
sk, err := bls.NewSecretKey()
if err != nil {
t.Fatalf("BLS NewSecretKey: %v", err)
}
sig := bls.Sign(sk, msg)
return bls.SignatureToBytes(sig)
}
// =============================================================================
// BLS Tests (bls.go)
// =============================================================================
func TestBLS_GenerateBLSAggregate_EmptyKey(t *testing.T) {
cfg := config.DefaultParams()
q := NewBLS(cfg, newMockStore())
// No keys initialized - should return empty
blockID := [32]byte{1, 2, 3}
votes := map[string]int{"block1": 10}
result := q.generateBLSAggregate(blockID, votes)
if len(result) != 0 {
t.Errorf("expected empty result with no blsKey, got %d bytes", len(result))
}
}
func TestBLS_GeneratePQCertificate_EmptyKey(t *testing.T) {
cfg := config.DefaultParams()
q := NewBLS(cfg, newMockStore())
// No keys initialized - should return empty
blockID := [32]byte{1, 2, 3}
votes := map[string]int{"block1": 10}
result := q.generatePQCertificate(blockID, votes)
if len(result) != 0 {
t.Errorf("expected empty result with no pqKey, got %d bytes", len(result))
}
}
func TestBLS_GenerateBLSAggregate_WithKey(t *testing.T) {
cfg := config.DefaultParams()
q := NewBLS(cfg, newMockStore())
ctx := context.Background()
_ = q.Initialize(ctx, []byte("test-bls-key"), []byte("test-pq-key"))
blockID := [32]byte{1, 2, 3}
votes := map[string]int{
"validator1": 10,
"validator2": 5,
}
result := q.generateBLSAggregate(blockID, votes)
if len(result) == 0 {
t.Error("expected non-empty BLS aggregate with key")
}
if len(result) != kmacMACOutLen { // KMAC256 canonical width (re-pinned from 32 after HMAC-SHA256 → KMAC256 migration)
t.Errorf("expected %d bytes, got %d", kmacMACOutLen, len(result))
}
}
func TestBLS_GeneratePQCertificate_WithKey(t *testing.T) {
cfg := config.DefaultParams()
q := NewBLS(cfg, newMockStore())
ctx := context.Background()
_ = q.Initialize(ctx, []byte("test-bls-key"), []byte("test-pq-key"))
blockID := [32]byte{1, 2, 3}
votes := map[string]int{
"validator1": 10,
"validator2": 5,
}
result := q.generatePQCertificate(blockID, votes)
if len(result) == 0 {
t.Error("expected non-empty PQ certificate with key")
}
if len(result) != kmacMACOutLen { // KMAC256 canonical width (re-pinned from 32 after HMAC-SHA256 → KMAC256 migration)
t.Errorf("expected %d bytes, got %d", kmacMACOutLen, len(result))
}
}
func TestBLS_PhaseI_EmptyFrontier(t *testing.T) {
cfg := config.DefaultParams()
q := NewBLS(cfg, newMockStore())
result := q.phaseI([]string{})
if result != "" {
t.Errorf("expected empty string for empty frontier, got %s", result)
}
result = q.phaseI(nil)
if result != "" {
t.Errorf("expected empty string for nil frontier, got %s", result)
}
}
func TestBLS_PhaseII_ZeroTotalVotes(t *testing.T) {
cfg := config.DefaultParams()
q := NewBLS(cfg, newMockStore())
_ = q.Initialize(context.Background(), []byte("bls"), []byte("pq"))
// Empty votes map
result := q.phaseII(map[string]int{}, "proposal")
if result != nil {
t.Error("expected nil for zero total votes")
}
// Zero values
result = q.phaseII(map[string]int{"block1": 0, "block2": 0}, "block1")
if result != nil {
t.Error("expected nil for zero total votes")
}
}
func TestBLS_PhaseII_ProposalNotInVotes(t *testing.T) {
cfg := config.DefaultParams()
q := NewBLS(cfg, newMockStore())
_ = q.Initialize(context.Background(), []byte("bls"), []byte("pq"))
votes := map[string]int{"block1": 10, "block2": 5}
result := q.phaseII(votes, "nonexistent")
if result != nil {
t.Error("expected nil when proposal not in votes")
}
}
func TestBLS_EstablishHorizon(t *testing.T) {
cfg := config.DefaultParams()
q := NewBLS(cfg, newMockStore())
_ = q.Initialize(context.Background(), []byte("bls"), []byte("pq"))
checkpoint := VertexID{1, 2, 3}
validators := []string{"v1", "v2", "v3"}
horizon, err := q.EstablishHorizon(context.Background(), checkpoint, validators)
if err != nil {
t.Fatalf("EstablishHorizon failed: %v", err)
}
if horizon == nil {
t.Fatal("expected non-nil horizon")
}
if horizon.Checkpoint != checkpoint {
t.Error("checkpoint mismatch")
}
if horizon.Height != 1 {
t.Errorf("expected height 1, got %d", horizon.Height)
}
if len(horizon.Validators) != 3 {
t.Errorf("expected 3 validators, got %d", len(horizon.Validators))
}
if len(horizon.Signature) == 0 {
t.Error("expected non-empty signature")
}
}
func TestBLS_EstablishHorizon_Multiple(t *testing.T) {
cfg := config.DefaultParams()
q := NewBLS(cfg, newMockStore())
_ = q.Initialize(context.Background(), []byte("bls"), []byte("pq"))
// Establish multiple horizons
for i := 0; i < 5; i++ {
checkpoint := VertexID{byte(i)}
validators := []string{"v1", "v2"}
horizon, err := q.EstablishHorizon(context.Background(), checkpoint, validators)
if err != nil {
t.Fatalf("EstablishHorizon %d failed: %v", i, err)
}
if horizon.Height != uint64(i+1) {
t.Errorf("expected height %d, got %d", i+1, horizon.Height)
}
}
latest := q.GetLatestHorizon()
if latest.Height != 5 {
t.Errorf("expected latest height 5, got %d", latest.Height)
}
}
func TestBLS_IsBeyondHorizon_NoHorizons(t *testing.T) {
cfg := config.DefaultParams()
q := NewBLS(cfg, newMockStore())
vertex := VertexID{1, 2, 3}
result := q.IsBeyondHorizon(vertex)
if result {
t.Error("expected false when no horizons established")
}
}
func TestBLS_ComputeCanonicalOrder_NoHorizons(t *testing.T) {
cfg := config.DefaultParams()
q := NewBLS(cfg, newMockStore())
result := q.ComputeCanonicalOrder()
if len(result) != 0 {
t.Errorf("expected empty slice when no horizons, got %d elements", len(result))
}
}
func TestBLS_GetLatestHorizon_NoHorizons(t *testing.T) {
cfg := config.DefaultParams()
q := NewBLS(cfg, newMockStore())
result := q.GetLatestHorizon()
if result != nil {
t.Error("expected nil when no horizons")
}
}
func TestBLS_CreateHorizonSignature(t *testing.T) {
cfg := config.DefaultParams()
q := NewBLS(cfg, newMockStore())
_ = q.Initialize(context.Background(), []byte("bls-key"), []byte("pq-key"))
checkpoint := VertexID{1, 2, 3}
validators := []string{"v1", "v2"}
sig := q.createHorizonSignature(checkpoint, validators)
if len(sig) == 0 {
t.Error("expected non-empty signature")
}
// Fusion signature under KMAC256: kmacMACOutLen-byte BLS half +
// kmacMACOutLen-byte PQ half = 2 * kmacMACOutLen bytes total.
// Re-pinned from 64 after the HMAC-SHA256 → KMAC256 migration.
expectedLen := 2 * kmacMACOutLen
if len(sig) != expectedLen {
t.Errorf("expected signature length %d, got %d", expectedLen, len(sig))
}
// Verify determinism: same inputs should produce same signature
sig2 := q.createHorizonSignature(checkpoint, validators)
if string(sig) != string(sig2) {
t.Error("signature should be deterministic")
}
// Verify different checkpoint produces different signature
checkpoint2 := VertexID{4, 5, 6}
sig3 := q.createHorizonSignature(checkpoint2, validators)
if string(sig) == string(sig3) {
t.Error("different checkpoints should produce different signatures")
}
}
// =============================================================================
// Config Tests (config.go)
// =============================================================================
func TestConfig_DefaultValues(t *testing.T) {
if DefaultConfig.QThreshold != 3 {
t.Errorf("expected QThreshold 3, got %d", DefaultConfig.QThreshold)
}
if DefaultConfig.QuasarTimeout != 30 {
t.Errorf("expected QuasarTimeout 30, got %d", DefaultConfig.QuasarTimeout)
}
}
func TestConfig_CustomValues(t *testing.T) {
cfg := Config{
QThreshold: 5,
QuasarTimeout: 60,
}
if cfg.QThreshold != 5 {
t.Errorf("expected QThreshold 5, got %d", cfg.QThreshold)
}
if cfg.QuasarTimeout != 60 {
t.Errorf("expected QuasarTimeout 60, got %d", cfg.QuasarTimeout)
}
}
// =============================================================================
// Engine Tests (engine.go)
// =============================================================================
func TestEngine_NewEngine(t *testing.T) {
cfg := Config{QThreshold: 2, QuasarTimeout: 30}
engine, err := NewEngine(cfg)
if err != nil {
t.Fatalf("NewEngine failed: %v", err)
}
if engine == nil {
t.Fatal("expected non-nil engine")
}
}
func TestEngine_NewEngine_ZeroThreshold(t *testing.T) {
cfg := Config{QThreshold: 0, QuasarTimeout: 30}
_, err := NewEngine(cfg)
if err == nil {
t.Fatal("NewEngine with zero threshold should fail")
}
}
func TestEngine_NewEngine_ThresholdOne_Rejected(t *testing.T) {
cfg := Config{QThreshold: 1, QuasarTimeout: 30}
_, err := NewEngine(cfg)
if err == nil {
t.Fatal("NewEngine with threshold=1 should fail in production mode")
}
}
func TestEngine_StartStop(t *testing.T) {
cfg := Config{QThreshold: 1, QuasarTimeout: 30}
engine, err := NewTestEngine(cfg)
if err != nil {
t.Fatalf("NewEngine failed: %v", err)
}
ctx := context.Background()
if err := engine.Start(ctx); err != nil {
t.Fatalf("Start failed: %v", err)
}
// Allow some time for goroutine to start
time.Sleep(10 * time.Millisecond)
if err := engine.Stop(); err != nil {
t.Fatalf("Stop failed: %v", err)
}
}
func TestEngine_Submit_NilBlock(t *testing.T) {
cfg := Config{QThreshold: 1, QuasarTimeout: 30}
engine, err := NewTestEngine(cfg)
if err != nil {
t.Fatalf("NewEngine failed: %v", err)
}
err = engine.Submit(nil)
if err == nil {
t.Error("expected error for nil block")
}
}
func TestEngine_Submit_ValidBlock(t *testing.T) {
cfg := Config{QThreshold: 1, QuasarTimeout: 30}
engine, err := NewTestEngine(cfg)
if err != nil {
t.Fatalf("NewEngine failed: %v", err)
}
ctx := context.Background()
_ = engine.Start(ctx)
defer engine.Stop()
block := &Block{
ID: [32]byte{1, 2, 3},
ChainID: [32]byte{4, 5, 6},
ChainName: "Test-Chain",
Height: 100,
Timestamp: time.Now(),
Data: []byte("test data"),
}
err = engine.Submit(block)
if err != nil {
t.Errorf("Submit failed: %v", err)
}
}
func TestEngine_Submit_BufferFull(t *testing.T) {
cfg := Config{QThreshold: 1, QuasarTimeout: 30}
engine, err := NewTestEngine(cfg)
if err != nil {
t.Fatalf("NewEngine failed: %v", err)
}
// Don't start the engine so buffer doesn't drain
// Fill the buffer
for i := 0; i < 1001; i++ {
block := &Block{
ID: [32]byte{byte(i)},
Height: uint64(i),
Timestamp: time.Now(),
}
err := engine.Submit(block)
if i >= 1000 && err == nil {
// After 1000, should get buffer full error
continue // May still succeed if under limit
}
}
// One more should fail
block := &Block{ID: [32]byte{0xFF}, Height: 9999, Timestamp: time.Now()}
err = engine.Submit(block)
if err == nil {
t.Error("expected buffer full error")
}
}
func TestEngine_Finalized(t *testing.T) {
cfg := Config{QThreshold: 1, QuasarTimeout: 30}
engine, err := NewTestEngine(cfg)
if err != nil {
t.Fatalf("NewEngine failed: %v", err)
}
ch := engine.Finalized()
if ch == nil {
t.Error("expected non-nil finalized channel")
}
}
func TestEngine_IsFinalized(t *testing.T) {
cfg := Config{QThreshold: 1, QuasarTimeout: 30}
engine, err := NewTestEngine(cfg)
if err != nil {
t.Fatalf("NewEngine failed: %v", err)
}
ctx := context.Background()
_ = engine.Start(ctx)
defer engine.Stop()
// Initially nothing is finalized
blockID := [32]byte{1, 2, 3}
if engine.IsFinalized(blockID) {
t.Error("expected block not to be finalized initially")
}
// Submit a block
block := &Block{
ID: blockID,
ChainID: [32]byte{4, 5, 6},
ChainName: "Test-Chain",
Height: 100,
Timestamp: time.Now(),
Data: []byte("test"),
}
_ = engine.Submit(block)
// Wait for processing
time.Sleep(100 * time.Millisecond)
// Should now be finalized (check by hash, not ID)
stats := engine.Stats()
if stats.FinalizedBlocks == 0 {
t.Error("expected at least one finalized block")
}
}
func TestEngine_Stats(t *testing.T) {
cfg := Config{QThreshold: 1, QuasarTimeout: 30}
engine, err := NewTestEngine(cfg)
if err != nil {
t.Fatalf("NewEngine failed: %v", err)
}
ctx := context.Background()
_ = engine.Start(ctx)
defer engine.Stop()
// Submit some blocks
for i := 0; i < 5; i++ {
block := &Block{
ID: [32]byte{byte(i)},
ChainID: [32]byte{byte(i + 10)},
ChainName: "Test-Chain",
Height: uint64(i),
Timestamp: time.Now(),
Data: []byte("test"),
}
_ = engine.Submit(block)
}
time.Sleep(100 * time.Millisecond)
stats := engine.Stats()
if stats.ProcessedBlocks < 5 {
t.Errorf("expected at least 5 processed blocks, got %d", stats.ProcessedBlocks)
}
if stats.Uptime <= 0 {
t.Error("expected positive uptime")
}
}
func TestEngine_ComputeHash(t *testing.T) {
block := &Block{
ID: [32]byte{1, 2, 3},
ChainID: [32]byte{4, 5, 6},
ChainName: "Test-Chain",
Height: 100,
Timestamp: time.Now(),
Data: []byte("test"),
}
hash := computeHash(block)
if hash == "" {
t.Error("expected non-empty hash")
}
// Hash should be deterministic
hash2 := computeHash(block)
if hash != hash2 {
t.Error("hash should be deterministic")
}
// Different block should have different hash
block2 := &Block{
ID: [32]byte{7, 8, 9},
ChainID: [32]byte{10, 11, 12},
ChainName: "Other-Chain",
Height: 200,
Timestamp: time.Now(),
Data: []byte("other"),
}
hash3 := computeHash(block2)
if hash == hash3 {
t.Error("different blocks should have different hashes")
}
}
func TestQuasarConsensus_AddRemoveValidator(t *testing.T) {
hc, err := newCertifier(1)
if err != nil {
t.Fatalf("newCertifier failed: %v", err)
}
hc.AddValidator("v1", 100)
hc.AddValidator("v2", 200)
if hc.validatorCount() != 2 {
t.Errorf("expected 2 validators, got %d", hc.validatorCount())
}
hc.RemoveValidator("v1")
if hc.validatorCount() != 1 {
t.Errorf("expected 1 validator after removal, got %d", hc.validatorCount())
}
hc.RemoveValidator("nonexistent") // Should not panic
if hc.validatorCount() != 1 {
t.Error("removing nonexistent validator should not change count")
}
}
func TestQuasarConsensus_GenerateCert(t *testing.T) {
hc, err := newCertifier(1)
if err != nil {
t.Fatalf("newCertifier failed: %v", err)
}
block := &Block{
ID: [32]byte{1, 2, 3},
ChainID: [32]byte{4, 5, 6},
ChainName: "Test",
Height: 100,
Timestamp: time.Now(),
}
cert := hc.generateCert(block)
if cert == nil {
t.Fatal("expected non-nil cert")
}
if len(cert.BLS) == 0 {
t.Error("expected non-empty BLS")
}
if len(cert.MLDSARollup) == 0 {
t.Error("expected non-empty PQ")
}
if cert.Epoch != block.Height {
t.Errorf("expected epoch %d, got %d", block.Height, cert.Epoch)
}
}
// =============================================================================
// Hybrid Tests (hybrid.go)
// =============================================================================
func TestSigner_NewSigner_InvalidThreshold(t *testing.T) {
_, err := NewSigner(0)
if err == nil {
t.Error("expected error for threshold 0")
}
_, err = NewSigner(-1)
if err == nil {
t.Error("expected error for negative threshold")
}
}
func TestSigner_AddValidator(t *testing.T) {
h, err := NewSigner(1)
if err != nil {
t.Fatalf("NewSigner failed: %v", err)
}
err = h.AddValidator("v1", 100)
if err != nil {
t.Fatalf("AddValidator failed: %v", err)
}
if h.GetActiveValidatorCount() != 1 {
t.Errorf("expected 1 active validator, got %d", h.GetActiveValidatorCount())
}
if h.GetThreshold() != 1 {
t.Errorf("expected threshold 1, got %d", h.GetThreshold())
}
}
func TestSigner_SignMessage_ValidatorNotFound(t *testing.T) {
h, err := NewSigner(1)
if err != nil {
t.Fatalf("NewSigner failed: %v", err)
}
_, err = h.SignMessage("nonexistent", []byte("test"))
if err == nil {
t.Error("expected error for nonexistent validator")
}
}
func TestSigner_ReleaseQuasarSig(t *testing.T) {
// Test with nil - should not panic
ReleaseQuasarSig(nil)
// Test with valid signature
h, _ := NewSigner(1)
_ = h.AddValidator("v1", 100)
sig, _ := h.SignMessage("v1", []byte("test"))
ReleaseQuasarSig(sig)
// No assertion needed - just checking it doesn't panic
}
func TestSigner_VerifyQuasarSig_ValidatorNotFound(t *testing.T) {
h, _ := NewSigner(1)
_ = h.AddValidator("v1", 100)
sig := &QuasarSig{
ValidatorID: "nonexistent",
BLS: []byte{1, 2, 3},
Corona: []byte{4, 5, 6},
}
result := h.VerifyQuasarSig([]byte("test"), sig)
if result {
t.Error("expected false for nonexistent validator")
}
}
func TestSigner_VerifyQuasarSig_InvalidBLS(t *testing.T) {
h, _ := NewSigner(1)
_ = h.AddValidator("v1", 100)
sig := &QuasarSig{
ValidatorID: "v1",
BLS: []byte{1, 2, 3}, // Invalid BLS bytes
Corona: []byte{4, 5, 6},
}
result := h.VerifyQuasarSig([]byte("test"), sig)
if result {
t.Error("expected false for invalid BLS signature")
}
}
func TestSigner_AggregateSignatures_InsufficientSignatures(t *testing.T) {
h, _ := NewSigner(3) // Require 3 validators
_ = h.AddValidator("v1", 100)
sig, _ := h.SignMessage("v1", []byte("test"))
// Only 1 signature, but threshold is 3
_, err := h.AggregateSignatures([]byte("test"), []*QuasarSig{sig})
if err == nil {
t.Error("expected error for insufficient signatures")
}
}
func TestSigner_AggregateSignatures_Success(t *testing.T) {
h, _ := NewSigner(2)
_ = h.AddValidator("v1", 100)
_ = h.AddValidator("v2", 100)
msg := []byte("test message")
sig1, _ := h.SignMessage("v1", msg)
sig2, _ := h.SignMessage("v2", msg)
aggSig, err := h.AggregateSignatures(msg, []*QuasarSig{sig1, sig2})
if err != nil {
t.Fatalf("AggregateSignatures failed: %v", err)
}
if aggSig.SignerCount != 2 {
t.Errorf("expected 2 signers, got %d", aggSig.SignerCount)
}
if len(aggSig.BLSAggregated) == 0 {
t.Error("expected non-empty BLS aggregate")
}
if len(aggSig.ValidatorIDs) != 2 {
t.Errorf("expected 2 validator IDs, got %d", len(aggSig.ValidatorIDs))
}
}
func TestSigner_VerifyAggregatedSignature(t *testing.T) {
h, _ := NewSigner(2)
_ = h.AddValidator("v1", 100)
_ = h.AddValidator("v2", 100)
msg := []byte("test message")
sig1, _ := h.SignMessage("v1", msg)
sig2, _ := h.SignMessage("v2", msg)
aggSig, _ := h.AggregateSignatures(msg, []*QuasarSig{sig1, sig2})
result := h.VerifyAggregatedSignature(msg, aggSig)
if !result {
t.Error("expected valid aggregated signature to verify")
}
}
func TestSigner_VerifyAggregatedSignature_InsufficientSigners(t *testing.T) {
h, _ := NewSigner(3) // Require 3
_ = h.AddValidator("v1", 100)
_ = h.AddValidator("v2", 100)
// Create aggregated sig with only 2 signers
aggSig := &AggregatedSignature{
BLSAggregated: []byte{1, 2, 3},
ValidatorIDs: []string{"v1", "v2"},
SignerCount: 2, // Below threshold
}
result := h.VerifyAggregatedSignature([]byte("test"), aggSig)
if result {
t.Error("expected false for insufficient signers")
}
}
func TestSigner_VerifyAggregatedSignature_InvalidBLS(t *testing.T) {
h, _ := NewSigner(1)
_ = h.AddValidator("v1", 100)
aggSig := &AggregatedSignature{
BLSAggregated: []byte{1, 2, 3}, // Invalid BLS bytes
ValidatorIDs: []string{"v1"},
SignerCount: 1,
}
result := h.VerifyAggregatedSignature([]byte("test"), aggSig)
if result {
t.Error("expected false for invalid BLS")
}
}
func TestSigner_VerifyAggregatedSignature_InactiveValidator(t *testing.T) {
h, _ := NewSigner(1)
_ = h.AddValidator("v1", 100)
// Make validator inactive
h.mu.Lock()
h.validators["v1"].Active = false
h.mu.Unlock()
sig, _ := h.SignMessage("v1", []byte("test"))
aggSig := &AggregatedSignature{
BLSAggregated: sig.BLS,
ValidatorIDs: []string{"v1"},
SignerCount: 1,
}
result := h.VerifyAggregatedSignature([]byte("test"), aggSig)
if result {
t.Error("expected false for inactive validator")
}
}
func TestSigner_GetActiveValidatorCount(t *testing.T) {
h, _ := NewSigner(1)
if h.GetActiveValidatorCount() != 0 {
t.Error("expected 0 validators initially")
}
_ = h.AddValidator("v1", 100)
_ = h.AddValidator("v2", 100)
if h.GetActiveValidatorCount() != 2 {
t.Errorf("expected 2 validators, got %d", h.GetActiveValidatorCount())
}
// Deactivate one
h.mu.Lock()
h.validators["v1"].Active = false
h.mu.Unlock()
if h.GetActiveValidatorCount() != 1 {
t.Errorf("expected 1 active validator, got %d", h.GetActiveValidatorCount())
}
}
// =============================================================================
// Corona Tests
// =============================================================================
//
// NOTE: Post-quantum threshold crypto is tested in the Pulsar package at
// github.com/luxfi/corona/threshold. The quasar package uses that real
// implementation via the Signer type in quasar.go. Tests for threshold
// signing are in quasar_test.go and epoch_test.go.
// =============================================================================
// Types Tests (types.go)
// =============================================================================
func TestQuasarCert_Verify(t *testing.T) {
tests := []struct {
name string
cert *QuasarCert
validators []string
want bool
}{
{
name: "nil cert",
cert: nil,
validators: []string{"v1"},
want: false,
},
{
name: "empty BLS",
cert: &QuasarCert{
BLS: nil,
MLDSARollup: []byte{1, 2, 3},
},
validators: []string{"v1"},
want: false,
},
{
name: "empty PQ",
cert: &QuasarCert{
BLS: []byte{1, 2, 3},
MLDSARollup: nil,
},
validators: []string{"v1"},
want: false,
},
{
name: "non-empty cert without crypto verification returns false",
cert: &QuasarCert{
BLS: []byte{1, 2, 3},
MLDSARollup: []byte{4, 5, 6},
Validators: 0,
},
validators: []string{"v1", "v2"},
want: false, // Verify() always returns false; use VerifyWithKeys
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
got := tt.cert.Verify(tt.validators)
if got != tt.want {
t.Errorf("Verify() = %v, want %v", got, tt.want)
}
})
}
}
func TestBlock_Fields(t *testing.T) {
now := time.Now()
block := &Block{
ID: [32]byte{1, 2, 3},
ChainID: [32]byte{4, 5, 6},
ChainName: "Test-Chain",
Height: 12345,
Hash: "0xabc",
Timestamp: now,
Data: []byte("test data"),
Cert: &QuasarCert{
BLS: []byte{7, 8, 9},
MLDSARollup: []byte{10, 11, 12},
Validators: 1,
Epoch: 100,
Finality: now,
},
}
if block.ChainName != "Test-Chain" {
t.Error("ChainName mismatch")
}
if block.Height != 12345 {
t.Error("Height mismatch")
}
if block.Cert.Epoch != 100 {
t.Error("Cert.Epoch mismatch")
}
}
func TestStats_Fields(t *testing.T) {
stats := Stats{
Height: 100,
ProcessedBlocks: 200,
FinalizedBlocks: 150,
PendingBlocks: 50,
Validators: 10,
Uptime: time.Hour,
}
if stats.Height != 100 {
t.Error("Height mismatch")
}
if stats.ProcessedBlocks != 200 {
t.Error("ProcessedBlocks mismatch")
}
if stats.Uptime != time.Hour {
t.Error("Uptime mismatch")
}
}
// =============================================================================
// Witness Tests (witness.go)
// =============================================================================
// =============================================================================
// Core (Quasar) Additional Tests
// =============================================================================
func TestQuasar_ProcessBlockWithContext_Cancelled(t *testing.T) {
q, err := NewTestQuasar(1)
if err != nil {
t.Fatalf("NewQuasar failed: %v", err)
}
_, _ = q.AddValidator("v1", 100)
block := &Block{
ChainID: [32]byte{1},
ChainName: "Test",
ID: [32]byte{2},
Height: 100,
Timestamp: time.Now(),
}
ctx, cancel := context.WithCancel(context.Background())
cancel()
// Should return early without processing
initialHeight := q.GetQuantumHeight()
q.processBlockWithContext(ctx, block)
// Height should not change
if q.GetQuantumHeight() != initialHeight {
t.Error("block should not be processed with cancelled context")
}
}
func TestQuasar_SubmitBlock_AutoRegister(t *testing.T) {
q, err := NewTestQuasar(1)
if err != nil {
t.Fatalf("NewQuasar failed: %v", err)
}
_, _ = q.AddValidator("v1", 100)
ctx := context.Background()
_ = q.Start(ctx)
block := &Block{
ChainID: [32]byte{1},
ChainName: "New-Chain",
ID: [32]byte{2},
Height: 1,
Timestamp: time.Now(),
}
err = q.SubmitBlock(block)
if err != nil {
t.Fatalf("SubmitBlock failed: %v", err)
}
// Wait for registration
time.Sleep(50 * time.Millisecond)
chains := q.GetRegisteredChains()
found := false
for _, c := range chains {
if c == "New-Chain" {
found = true
break
}
}
if !found {
t.Error("chain should be auto-registered")
}
}
func TestQuasar_SubmitBlock_BufferFull(t *testing.T) {
q, err := NewTestQuasar(1)
if err != nil {
t.Fatalf("NewQuasar failed: %v", err)
}
// Don't start - buffers won't drain
// Register a chain
_ = q.RegisterChain("Full-Chain")
// Fill the buffer
for i := 0; i < 101; i++ {
block := &Block{
ChainID: [32]byte{byte(i)},
ChainName: "Full-Chain",
ID: [32]byte{byte(i)},
Height: uint64(i),
Timestamp: time.Now(),
}
_ = q.SubmitBlock(block) // Should not error, just drop oldest
}
// No error expected - buffer full handling drops oldest
}
func TestQuasar_FinalizeQuantumEpoch_NoBlocks(t *testing.T) {
q, err := NewTestQuasar(1)
if err != nil {
t.Fatalf("NewQuasar failed: %v", err)
}
// Should not panic with empty finalized blocks
q.finalizeQuantumEpoch()
_, proofs := q.GetMetrics()
if proofs != 0 {
t.Error("expected 0 proofs with no blocks")
}
}
func TestQuasar_ComputeQuantumHash(t *testing.T) {
q, _ := NewTestQuasar(1)
block := &Block{
ChainName: "Test",
ID: [32]byte{1, 2, 3},
Height: 100,
Timestamp: time.Unix(1234567890, 0),
}
hash := q.computeQuantumHash(block)
if hash == "" {
t.Error("expected non-empty hash")
}
// Deterministic
hash2 := q.computeQuantumHash(block)
if hash != hash2 {
t.Error("hash should be deterministic")
}
}
func TestCertBundle_Verify_Panics(t *testing.T) {
// Non-nil cert panics to enforce VerifyWithKeys usage
cert := &CertBundle{BLSAgg: []byte{1}, PQCert: []byte{2}}
defer func() {
if r := recover(); r == nil {
t.Error("CertBundle.Verify should panic")
}
}()
cert.Verify([]string{"v1"})
}
func TestCertBundle_VerifyWithKeys(t *testing.T) {
cfg := config.DefaultParams()
q := NewBLS(cfg, newMockStore())
blsKey := []byte("test-bls-key-material-32-bytes!!")
pqKey := []byte("test-pq-key-material-32-bytes!!!")
_ = q.Initialize(context.Background(), blsKey, pqKey)
// Generate a cert via phaseII
votes := map[string]int{"block-A": 10, "block-B": 2}
cert := q.phaseII(votes, "block-A")
if cert == nil {
t.Fatal("expected non-nil cert from phaseII")
}
// Correct keys must verify
if !cert.VerifyWithKeys(blsKey, pqKey) {
t.Error("VerifyWithKeys should pass with correct keys")
}
// Wrong BLS key must fail
if cert.VerifyWithKeys([]byte("wrong-bls-key"), pqKey) {
t.Error("VerifyWithKeys should fail with wrong BLS key")
}
// Wrong PQ key must fail
if cert.VerifyWithKeys(blsKey, []byte("wrong-pq-key")) {
t.Error("VerifyWithKeys should fail with wrong PQ key")
}
// Both wrong must fail
if cert.VerifyWithKeys([]byte("wrong-bls"), []byte("wrong-pq")) {
t.Error("VerifyWithKeys should fail with both wrong keys")
}
// Nil cert must fail
var nilCert *CertBundle
if nilCert.VerifyWithKeys(blsKey, pqKey) {
t.Error("nil cert should fail VerifyWithKeys")
}
// Empty message must fail
emptyMsg := &CertBundle{BLSAgg: cert.BLSAgg, PQCert: cert.PQCert}
if emptyMsg.VerifyWithKeys(blsKey, pqKey) {
t.Error("empty message cert should fail VerifyWithKeys")
}
// Tampered BLS signature must fail
tampered := &CertBundle{
BLSAgg: append([]byte{0xff}, cert.BLSAgg[1:]...),
PQCert: cert.PQCert,
Message: cert.Message,
}
if tampered.VerifyWithKeys(blsKey, pqKey) {
t.Error("tampered BLS sig should fail VerifyWithKeys")
}
// Tampered PQ cert must fail
tampered2 := &CertBundle{
BLSAgg: cert.BLSAgg,
PQCert: append([]byte{0xff}, cert.PQCert[1:]...),
Message: cert.Message,
}
if tampered2.VerifyWithKeys(blsKey, pqKey) {
t.Error("tampered PQ cert should fail VerifyWithKeys")
}
// Tampered message must fail
tampered3 := &CertBundle{
BLSAgg: cert.BLSAgg,
PQCert: cert.PQCert,
Message: append([]byte{0xff}, cert.Message[1:]...),
}
if tampered3.VerifyWithKeys(blsKey, pqKey) {
t.Error("tampered message should fail VerifyWithKeys")
}
}
func TestCertBundle_KMAC256DifferentKeys(t *testing.T) {
cfg := config.DefaultParams()
// Two BLS instances with different keys
q1 := NewBLS(cfg, newMockStore())
_ = q1.Initialize(context.Background(), []byte("key-alpha-32-bytes-of-material!"), []byte("pq-alpha-32-bytes-of-material!!"))
q2 := NewBLS(cfg, newMockStore())
_ = q2.Initialize(context.Background(), []byte("key-beta--32-bytes-of-material!"), []byte("pq-beta--32-bytes-of-material!!"))
blockID := [32]byte{0xab, 0xcd}
votes := map[string]int{"v1": 5}
sig1 := q1.generateBLSAggregate(blockID, votes)
sig2 := q2.generateBLSAggregate(blockID, votes)
if string(sig1) == string(sig2) {
t.Error("different keys must produce different KMAC256 outputs")
}
pq1 := q1.generatePQCertificate(blockID, votes)
pq2 := q2.generatePQCertificate(blockID, votes)
if string(pq1) == string(pq2) {
t.Error("different PQ keys must produce different KMAC256 outputs")
}
}
func TestCertBundle_MessageDigestStored(t *testing.T) {
cfg := config.DefaultParams()
q := NewBLS(cfg, newMockStore())
blsKey := []byte("bls-key-for-message-digest-test!")
pqKey := []byte("pq-key-for-message-digest-test!!")
_ = q.Initialize(context.Background(), blsKey, pqKey)
votes := map[string]int{"block-X": 8}
cert := q.phaseII(votes, "block-X")
if cert == nil {
t.Fatal("expected non-nil cert")
}
// Vote digest is cSHAKE256 → 32 bytes (unchanged).
if len(cert.Message) != 32 {
t.Errorf("expected 32-byte message digest, got %d", len(cert.Message))
}
// Canonical KMAC256 width is kmacMACOutLen (48 bytes, SHA3-384 width).
// Re-pinned after the HMAC-SHA256 → KMAC256 migration.
if len(cert.BLSAgg) != kmacMACOutLen {
t.Errorf("expected %d-byte BLS KMAC256, got %d", kmacMACOutLen, len(cert.BLSAgg))
}
if len(cert.PQCert) != kmacMACOutLen {
t.Errorf("expected %d-byte PQ KMAC256, got %d", kmacMACOutLen, len(cert.PQCert))
}
// BLS and PQ MACs must differ — they use distinct keys AND distinct
// SP 800-185 customizations, so two ways of being independent.
if string(cert.BLSAgg) == string(cert.PQCert) {
t.Error("BLS and PQ KMAC256 outputs should differ (different keys and customizations)")
}
}
func TestHorizon_ComputeBlockHash(t *testing.T) {
block := &Block{
Hash: "0xabc123",
Height: 100,
Timestamp: time.Unix(1234567890, 0),
}
hash := computeBlockHash(block)
if hash == "" {
t.Error("expected non-empty hash")
}
// Different block should have different hash
block2 := &Block{
Hash: "0xdef456",
Height: 200,
Timestamp: time.Unix(1234567891, 0),
}
hash2 := computeBlockHash(block2)
if hash == hash2 {
t.Error("different blocks should have different hashes")
}
}
// =============================================================================
// Type Alias Tests
// =============================================================================
func TestTypeAliases(t *testing.T) {
// NewBLS creates a BLS instance
cfg := config.DefaultParams()
store := newMockStore()
p := NewBLS(cfg, store)
if p == nil {
t.Error("NewBLS returned nil")
}
// NewSigner creates a signer instance
h, err := NewSigner(1)
if err != nil {
t.Fatalf("NewSigner failed: %v", err)
}
if h == nil {
t.Error("NewSigner returned nil")
}
}
// =============================================================================
// Edge Cases and Error Paths
// =============================================================================
func TestSigner_AggregateSignatures_InvalidBLS(t *testing.T) {
h, _ := NewSigner(1)
_ = h.AddValidator("v1", 100)
sig := &QuasarSig{
ValidatorID: "v1",
BLS: []byte{1, 2, 3}, // Invalid BLS bytes
Corona: []byte{4, 5, 6},
}
_, err := h.AggregateSignatures([]byte("test"), []*QuasarSig{sig})
if err == nil {
t.Error("expected error for invalid BLS signature")
}
}
// =============================================================================
// Additional Coverage Tests - Targeting Missing 9.7%
// =============================================================================
func TestBLS_IsBeyondHorizon_WithHorizons(t *testing.T) {
cfg := config.DefaultParams()
store := newMockStore()
// Add a vertex to the store
vertex := &mockVertex{
id: VertexID{1, 2, 3},
parents: []VertexID{},
author: "author1",
round: 1,
}
store.vertices[vertex.id] = vertex
store.heads = append(store.heads, vertex.id)
q := NewBLS(cfg, store)
_ = q.Initialize(context.Background(), []byte("bls"), []byte("pq"))
// Establish a horizon
checkpoint := VertexID{4, 5, 6}
validators := []string{"v1", "v2"}
_, _ = q.EstablishHorizon(context.Background(), checkpoint, validators)
// Now test IsBeyondHorizon with horizons present
result := q.IsBeyondHorizon(vertex.id)
// The result depends on dag.BeyondHorizon implementation
// Just ensure we exercise the code path
_ = result
}
func TestBLS_ComputeCanonicalOrder_WithHorizons(t *testing.T) {
cfg := config.DefaultParams()
store := newMockStore()
q := NewBLS(cfg, store)
_ = q.Initialize(context.Background(), []byte("bls"), []byte("pq"))
// Establish a horizon
checkpoint := VertexID{1, 2, 3}
validators := []string{"v1", "v2"}
_, _ = q.EstablishHorizon(context.Background(), checkpoint, validators)
// Now test ComputeCanonicalOrder with horizons present
order := q.ComputeCanonicalOrder()
// Result depends on dag.ComputeHorizonOrder implementation
_ = order
}
func TestQuasar_SubmitChainBlock_BufferFull(t *testing.T) {
q, err := NewTestQuasar(1)
if err != nil {
t.Fatalf("NewQuasar failed: %v", err)
}
// Don't start - buffers won't drain
// Test P-Chain buffer full path
for i := 0; i < 101; i++ {
block := &Block{
ChainID: [32]byte{byte(i)},
ChainName: "P-Chain",
ID: [32]byte{byte(i)},
Height: uint64(i),
Timestamp: time.Now(),
}
q.SubmitPChainBlock(block)
}
// Test X-Chain buffer full path
for i := 0; i < 101; i++ {
block := &Block{
ChainID: [32]byte{byte(i)},
ChainName: "X-Chain",
ID: [32]byte{byte(i)},
Height: uint64(i),
Timestamp: time.Now(),
}
q.SubmitXChainBlock(block)
}
// Test C-Chain buffer full path
for i := 0; i < 101; i++ {
block := &Block{
ChainID: [32]byte{byte(i)},
ChainName: "C-Chain",
ID: [32]byte{byte(i)},
Height: uint64(i),
Timestamp: time.Now(),
}
q.SubmitCChainBlock(block)
}
}
func TestQuasar_ProcessBlockWithContext_ContextCancelledAfterLock(t *testing.T) {
q, err := NewTestQuasar(1)
if err != nil {
t.Fatalf("NewQuasar failed: %v", err)
}
_, _ = q.AddValidator("v1", 100)
block := &Block{
ChainID: [32]byte{1},
ChainName: "Test",
ID: [32]byte{2},
Height: 100,
Timestamp: time.Now(),
}
// Create context that we'll cancel during processing
// This tests the path where context is checked after acquiring lock
ctx, cancel := context.WithCancel(context.Background())
// Process normally first to ensure validator is set up
q.processBlockWithContext(ctx, block)
// Now cancel and try again
cancel()
q.processBlockWithContext(ctx, block)
}
func TestQuasar_VerifyQuantumFinalityWithContext_AllPaths(t *testing.T) {
q, err := NewTestQuasar(1)
if err != nil {
t.Fatalf("NewQuasar failed: %v", err)
}
_, _ = q.AddValidator("v1", 100)
// Process a block first
block := &Block{
ChainID: [32]byte{1},
ChainName: "Test",
ID: [32]byte{2},
Height: 100,
Timestamp: time.Now(),
}
q.processBlock(block)
blockHash := q.computeQuantumHash(block)
// Test with non-existent block hash
result := q.VerifyQuantumFinalityWithContext(context.Background(), "nonexistent")
if result {
t.Error("expected false for non-existent block")
}
// Test with cancelled context before RLock
ctx, cancel := context.WithCancel(context.Background())
cancel()
result = q.VerifyQuantumFinalityWithContext(ctx, blockHash)
if result {
t.Error("expected false for cancelled context")
}
}
func TestQuasar_RegisterChain_AlreadyRegistered(t *testing.T) {
q, err := NewTestQuasar(1)
if err != nil {
t.Fatalf("NewQuasar failed: %v", err)
}
// Register a chain
err = q.RegisterChain("Test-Chain")
if err != nil {
t.Fatalf("RegisterChain failed: %v", err)
}
// Register same chain again - should return early
err = q.RegisterChain("Test-Chain")
if err != nil {
t.Fatalf("RegisterChain (duplicate) failed: %v", err)
}
}
func TestQuasar_ProcessChain_ContextDone(t *testing.T) {
q, err := NewTestQuasar(1)
if err != nil {
t.Fatalf("NewQuasar failed: %v", err)
}
// Register a chain
_ = q.RegisterChain("Test-Chain")
// Create cancelled context
ctx, cancel := context.WithCancel(context.Background())
cancel()
// processChain should return early due to cancelled context
// Start it in goroutine and let it exit
done := make(chan struct{})
go func() {
q.processChain(ctx, "Test-Chain")
close(done)
}()
select {
case <-done:
// Good - processChain exited
case <-time.After(100 * time.Millisecond):
t.Error("processChain did not exit on cancelled context")
}
}
func TestQuasar_QuantumFinalizer_ContextDone(t *testing.T) {
q, err := NewTestQuasar(1)
if err != nil {
t.Fatalf("NewQuasar failed: %v", err)
}
// Create context that expires quickly
ctx, cancel := context.WithTimeout(context.Background(), 10*time.Millisecond)
defer cancel()
// quantumFinalizer should exit when context is done
done := make(chan struct{})
go func() {
q.quantumFinalizer(ctx)
close(done)
}()
select {
case <-done:
// Good - quantumFinalizer exited
case <-time.After(500 * time.Millisecond):
t.Error("quantumFinalizer did not exit on cancelled context")
}
}
func TestEngine_NewEngine_NegativeThreshold(t *testing.T) {
// Negative threshold should be rejected
cfg := Config{QThreshold: -5, QuasarTimeout: 30}
_, err := NewEngine(cfg)
if err == nil {
t.Fatal("NewEngine with negative threshold should fail")
}
}
func TestEngine_ProcessBlock_NilCert(t *testing.T) {
// This tests the path where generateCert returns nil
// The default implementation always returns a cert; to test the nil-cert
// path, call processBlock with a modified hybrid that returns nil.
cfg := Config{QThreshold: 1, QuasarTimeout: 30}
engine, err := NewTestEngine(cfg)
if err != nil {
t.Fatalf("NewTestEngine failed: %v", err)
}
ctx := context.Background()
_ = engine.Start(ctx)
defer engine.Stop()
// Submit a block
block := &Block{
ID: [32]byte{1, 2, 3},
ChainID: [32]byte{4, 5, 6},
ChainName: "Test",
Height: 100,
Timestamp: time.Now(),
}
_ = engine.Submit(block)
// Wait for processing
time.Sleep(50 * time.Millisecond)
// Check stats
stats := engine.Stats()
_ = stats // Just ensure the code path is exercised
}
func TestEngine_FinalizedChannel_BufferFull(t *testing.T) {
cfg := Config{QThreshold: 1, QuasarTimeout: 30}
engine, err := NewTestEngine(cfg)
if err != nil {
t.Fatalf("NewEngine failed: %v", err)
}
ctx := context.Background()
_ = engine.Start(ctx)
defer engine.Stop()
// Submit many blocks quickly without reading from finalized channel
for i := 0; i < 1100; i++ {
block := &Block{
ID: [32]byte{byte(i), byte(i >> 8)},
ChainID: [32]byte{byte(i + 10)},
ChainName: "Test",
Height: uint64(i),
Timestamp: time.Now(),
}
_ = engine.Submit(block)
}
// Wait for processing
time.Sleep(200 * time.Millisecond)
// The finalized channel should have dropped some blocks when full
// Just ensure no panic occurred
}
func TestSigner_SignMessageWithContext_ValidatorNotFound(t *testing.T) {
h, err := NewSigner(1)
if err != nil {
t.Fatalf("NewSigner failed: %v", err)
}
// Add validator but then remove it manually
_ = h.AddValidator("v1", 100)
// Remove validator keys
h.mu.Lock()
delete(h.blsKeys, "v1")
h.mu.Unlock()
// Try to sign - should fail due to missing keys
_, err = h.SignMessage("v1", []byte("test"))
if err == nil {
t.Error("expected error for missing validator keys")
}
}
func TestSigner_VerifyQuasarSigWithContext_ContextCancelledMidVerify(t *testing.T) {
h, err := NewSigner(1)
if err != nil {
t.Fatalf("NewSigner failed: %v", err)
}
_ = h.AddValidator("v1", 100)
msg := []byte("test message")
sig, err := h.SignMessage("v1", msg)
if err != nil {
t.Fatalf("SignMessage failed: %v", err)
}
// Test verification with cancelled context
ctx, cancel := context.WithCancel(context.Background())
cancel()
result := h.VerifyQuasarSigWithContext(ctx, msg, sig)
if result {
t.Error("expected false for cancelled context")
}
}
func TestSigner_AggregateSignaturesWithContext_ContextCancelledMid(t *testing.T) {
h, err := NewSigner(1)
if err != nil {
t.Fatalf("NewSigner failed: %v", err)
}
_ = h.AddValidator("v1", 100)
_ = h.AddValidator("v2", 100)
msg := []byte("test message")
sig1, _ := h.SignMessage("v1", msg)
sig2, _ := h.SignMessage("v2", msg)
// Test with cancelled context
ctx, cancel := context.WithCancel(context.Background())
cancel()
_, err = h.AggregateSignaturesWithContext(ctx, msg, []*QuasarSig{sig1, sig2})
if err == nil {
t.Error("expected error for cancelled context")
}
}
func TestSigner_VerifyAggregatedSignatureWithContext_AllErrorPaths(t *testing.T) {
h, err := NewSigner(1)
if err != nil {
t.Fatalf("NewSigner failed: %v", err)
}
_ = h.AddValidator("v1", 100)
msg := []byte("test message")
sig, _ := h.SignMessage("v1", msg)
aggSig, _ := h.AggregateSignatures(msg, []*QuasarSig{sig})
// Test with context cancelled before RLock
ctx, cancel := context.WithCancel(context.Background())
cancel()
result := h.VerifyAggregatedSignatureWithContext(ctx, msg, aggSig)
if result {
t.Error("expected false for cancelled context")
}
// Test with validator that doesn't exist
aggSig2 := &AggregatedSignature{
BLSAggregated: sig.BLS,
ValidatorIDs: []string{"nonexistent"},
SignerCount: 1,
}
result = h.VerifyAggregatedSignature(msg, aggSig2)
if result {
t.Error("expected false for nonexistent validator")
}
}
func TestProcessPXCChain_ContextDone(t *testing.T) {
q, err := NewTestQuasar(1)
if err != nil {
t.Fatalf("NewQuasar failed: %v", err)
}
// Create cancelled context
ctx, cancel := context.WithCancel(context.Background())
cancel()
// Test processPChain exits on cancelled context
done := make(chan struct{})
go func() {
q.processPChain(ctx)
close(done)
}()
select {
case <-done:
case <-time.After(100 * time.Millisecond):
t.Error("processPChain did not exit")
}
// Test processXChain exits on cancelled context
done2 := make(chan struct{})
go func() {
q.processXChain(ctx)
close(done2)
}()
select {
case <-done2:
case <-time.After(100 * time.Millisecond):
t.Error("processXChain did not exit")
}
// Test processCChain exits on cancelled context
done3 := make(chan struct{})
go func() {
q.processCChain(ctx)
close(done3)
}()
select {
case <-done3:
case <-time.After(100 * time.Millisecond):
t.Error("processCChain did not exit")
}
}
func TestQuasar_SubmitBlock_BufferNotExists(t *testing.T) {
q, err := NewTestQuasar(1)
if err != nil {
t.Fatalf("NewQuasar failed: %v", err)
}
// Manually corrupt internal state - remove chain buffer but keep it registered
q.mu.Lock()
q.registeredChains["Ghost-Chain"] = true
// Don't create a buffer for it
q.mu.Unlock()
block := &Block{
ChainID: [32]byte{1},
ChainName: "Ghost-Chain",
ID: [32]byte{2},
Height: 100,
Timestamp: time.Now(),
}
err = q.SubmitBlock(block)
if err == nil {
t.Error("expected error for missing buffer")
}
}
func TestQuasar_VerifyQuantumFinalityWithContext_InvalidSignature(t *testing.T) {
q, err := NewTestQuasar(1)
if err != nil {
t.Fatalf("NewQuasar failed: %v", err)
}
_, _ = q.AddValidator("validator1", 100)
// Process a block first
block := &Block{
ChainID: [32]byte{1},
ChainName: "Test",
ID: [32]byte{2},
Height: 100,
Timestamp: time.Now(),
}
q.processBlock(block)
blockHash := q.computeQuantumHash(block)
// Modify the validator signature to be invalid
// This tests the verification failure path (line 336-339)
q.mu.Lock()
qBlock := q.finalizedBlocks[blockHash]
if qBlock != nil && len(qBlock.ValidatorSigs) > 0 {
// Corrupt the signature by changing validator ID to one that doesn't exist
// in the hybrid consensus
for vid := range qBlock.ValidatorSigs {
sig := qBlock.ValidatorSigs[vid]
// Change validator ID to nonexistent one - this will fail lookup
sig.ValidatorID = "nonexistent-validator"
}
}
q.mu.Unlock()
// Now verification should fail because validator doesn't exist in hybrid
result := q.VerifyQuantumFinality(blockHash)
if result {
t.Error("expected false for nonexistent validator")
}
}
func TestQuasar_QuantumFinalizer_TickerCase(t *testing.T) {
q, err := NewTestQuasar(1)
if err != nil {
t.Fatalf("NewQuasar failed: %v", err)
}
// Add some blocks first
q.mu.Lock()
q.finalizedBlocks["test1"] = &QuantumBlock{Height: 1}
q.finalizedBlocks["test2"] = &QuantumBlock{Height: 2}
q.mu.Unlock()
// Directly call finalizeQuantumEpoch to exercise the ticker case code
q.finalizeQuantumEpoch()
_, proofs := q.GetMetrics()
if proofs < 1 {
t.Error("expected at least 1 proof after epoch finalization")
}
}
func TestSigner_SignMessageWithContext_ContextCancelledAfterLock(t *testing.T) {
h, err := NewSigner(1)
if err != nil {
t.Fatalf("NewSigner failed: %v", err)
}
_ = h.AddValidator("v1", 100)
// Create context that will be cancelled
ctx, cancel := context.WithCancel(context.Background())
// We need to test the path where context is cancelled after acquiring RLock
// This is tricky in single-threaded tests; the best we can do is pre-cancel
cancel()
_, err = h.SignMessageWithContext(ctx, "v1", []byte("test"))
if err == nil {
t.Error("expected error for cancelled context")
}
}
func TestSigner_VerifyQuasarSigWithContext_ContextCancelledAfterLock(t *testing.T) {
h, err := NewSigner(1)
if err != nil {
t.Fatalf("NewSigner failed: %v", err)
}
_ = h.AddValidator("v1", 100)
msg := []byte("test message")
sig, _ := h.SignMessage("v1", msg)
// Pre-cancel context - tests early return path
ctx, cancel := context.WithCancel(context.Background())
cancel()
result := h.VerifyQuasarSigWithContext(ctx, msg, sig)
if result {
t.Error("expected false for cancelled context")
}
}
func TestSigner_VerifyAggregatedSignatureWithContext_ContextCancelledAfterLock(t *testing.T) {
h, err := NewSigner(1)
if err != nil {
t.Fatalf("NewSigner failed: %v", err)
}
_ = h.AddValidator("v1", 100)
msg := []byte("test message")
sig, _ := h.SignMessage("v1", msg)
aggSig, _ := h.AggregateSignatures(msg, []*QuasarSig{sig})
// Pre-cancel context
ctx, cancel := context.WithCancel(context.Background())
cancel()
result := h.VerifyAggregatedSignatureWithContext(ctx, msg, aggSig)
if result {
t.Error("expected false for cancelled context")
}
}
func TestSigner_AggregateSignaturesWithContext_ContextCancelledAfterLock(t *testing.T) {
h, err := NewSigner(1)
if err != nil {
t.Fatalf("NewSigner failed: %v", err)
}
_ = h.AddValidator("v1", 100)
msg := []byte("test message")
sig, _ := h.SignMessage("v1", msg)
// Pre-cancel context
ctx, cancel := context.WithCancel(context.Background())
cancel()
_, err = h.AggregateSignaturesWithContext(ctx, msg, []*QuasarSig{sig})
if err == nil {
t.Error("expected error for cancelled context")
}
}
func TestSigner_VerifyAggregatedSignature_PublicKeyAggregationError(t *testing.T) {
h, err := NewSigner(1)
if err != nil {
t.Fatalf("NewSigner failed: %v", err)
}
_ = h.AddValidator("v1", 100)
msg := []byte("test message")
sig, _ := h.SignMessage("v1", msg)
// Create aggSig with valid BLS but referencing non-existent validator
// This should fail at public key lookup
aggSig := &AggregatedSignature{
BLSAggregated: sig.BLS,
ValidatorIDs: []string{"nonexistent"},
SignerCount: 1,
}
result := h.VerifyAggregatedSignature(msg, aggSig)
if result {
t.Error("expected false for nonexistent validator")
}
}
func TestSigner_VerifyAggregatedSignature_BLSVerifyFail(t *testing.T) {
h, err := NewSigner(1)
if err != nil {
t.Fatalf("NewSigner failed: %v", err)
}
_ = h.AddValidator("v1", 100)
msg := []byte("test message")
sig, _ := h.SignMessage("v1", msg)
aggSig, _ := h.AggregateSignatures(msg, []*QuasarSig{sig})
// Verify with different message - BLS verification should fail
wrongMsg := []byte("wrong message")
result := h.VerifyAggregatedSignature(wrongMsg, aggSig)
if result {
t.Error("expected false for wrong message")
}
}
func TestSigner_VerifyAggregatedSignature_BLSSignatureTamperedFail(t *testing.T) {
h, err := NewSigner(1)
if err != nil {
t.Fatalf("NewSigner failed: %v", err)
}
_ = h.AddValidator("v1", 100)
_ = h.AddValidator("v2", 100)
msg := []byte("test message")
sig1, _ := h.SignMessage("v1", msg)
sig2, _ := h.SignMessage("v2", msg)
aggSig, _ := h.AggregateSignatures(msg, []*QuasarSig{sig1, sig2})
// Corrupt the BLS signature
if len(aggSig.BLSAggregated) > 0 {
aggSig.BLSAggregated[0] ^= 0xFF // Tamper with signature
}
result := h.VerifyAggregatedSignature(msg, aggSig)
if result {
t.Error("expected false for corrupted BLS signature")
}
}
func TestEngine_ProcessBlock_FinalizedChannelDrop(t *testing.T) {
cfg := Config{QThreshold: 1, QuasarTimeout: 30}
engine, err := NewTestEngine(cfg)
if err != nil {
t.Fatalf("NewEngine failed: %v", err)
}
ctx := context.Background()
_ = engine.Start(ctx)
defer engine.Stop()
// Submit many blocks to fill finalized channel
for i := 0; i < 1100; i++ {
block := &Block{
ID: [32]byte{byte(i), byte(i >> 8)},
ChainID: [32]byte{byte(i + 10)},
ChainName: "Test",
Height: uint64(i),
Timestamp: time.Now(),
}
_ = engine.Submit(block)
}
// Wait for processing - some blocks should be dropped from finalized channel
time.Sleep(300 * time.Millisecond)
stats := engine.Stats()
// Just verify we processed blocks without panic
if stats.ProcessedBlocks == 0 {
t.Error("expected some processed blocks")
}
}
func TestQuasar_SubmitBlock_RegisterChainError(t *testing.T) {
// This is hard to test because RegisterChain only returns nil
// But we can ensure the auto-register path is exercised
q, err := NewTestQuasar(1)
if err != nil {
t.Fatalf("NewQuasar failed: %v", err)
}
// Submit block with new chain name that doesn't exist
block := &Block{
ChainID: [32]byte{1},
ChainName: "Brand-New-Chain",
ID: [32]byte{2},
Height: 100,
Timestamp: time.Now(),
}
err = q.SubmitBlock(block)
if err != nil {
t.Fatalf("SubmitBlock failed: %v", err)
}
// Verify chain was auto-registered
chains := q.GetRegisteredChains()
found := false
for _, c := range chains {
if c == "Brand-New-Chain" {
found = true
break
}
}
if !found {
t.Error("expected chain to be auto-registered")
}
}
func TestQuasar_VerifyQuantumFinalityWithContext_LoopContextCheck(t *testing.T) {
q, err := NewTestQuasar(1)
if err != nil {
t.Fatalf("NewQuasar failed: %v", err)
}
_, _ = q.AddValidator("validator1", 100)
_, _ = q.AddValidator("validator2", 100)
_, _ = q.AddValidator("validator3", 100)
// Process a block first
block := &Block{
ChainID: [32]byte{1},
ChainName: "Test",
ID: [32]byte{2},
Height: 100,
Timestamp: time.Now(),
}
q.processBlock(block)
blockHash := q.computeQuantumHash(block)
// Add multiple signatures manually to test loop iteration
q.mu.Lock()
qBlock := q.finalizedBlocks[blockHash]
if qBlock != nil {
// Add signatures for all validators
for _, vid := range []string{"validator1", "validator2", "validator3"} {
sig, _ := q.SignMessage(vid, []byte(blockHash))
if sig != nil {
qBlock.ValidatorSigs[vid] = sig
}
}
}
q.mu.Unlock()
// Verify should pass with all valid signatures
result := q.VerifyQuantumFinality(blockHash)
if !result {
t.Error("expected true for valid signatures")
}
}
func TestSigner_VerifyAggregatedSignature_ContextCancelledInLoop(t *testing.T) {
h, err := NewSigner(1)
if err != nil {
t.Fatalf("NewSigner failed: %v", err)
}
// Add multiple validators
_ = h.AddValidator("v1", 100)
_ = h.AddValidator("v2", 100)
_ = h.AddValidator("v3", 100)
msg := []byte("test message")
sig1, _ := h.SignMessage("v1", msg)
sig2, _ := h.SignMessage("v2", msg)
sig3, _ := h.SignMessage("v3", msg)
aggSig, _ := h.AggregateSignatures(msg, []*QuasarSig{sig1, sig2, sig3})
// Verify with valid context should pass
result := h.VerifyAggregatedSignature(msg, aggSig)
if !result {
t.Error("expected true for valid aggregated signature")
}
}
func TestEngine_Stop_NotStarted(t *testing.T) {
cfg := Config{QThreshold: 1, QuasarTimeout: 30}
engine, err := NewTestEngine(cfg)
if err != nil {
t.Fatalf("NewEngine failed: %v", err)
}
// Stop without starting - should not panic
err = engine.Stop()
if err != nil {
t.Errorf("Stop without start should not error: %v", err)
}
}
func TestQuasar_SubmitBlock_BufferFullDropOldest(t *testing.T) {
q, err := NewTestQuasar(1)
if err != nil {
t.Fatalf("NewQuasar failed: %v", err)
}
// Register a test chain (without starting processors)
_ = q.RegisterChain("Test-Full-Chain")
// Fill the buffer beyond capacity
for i := 0; i < 102; i++ {
block := &Block{
ChainID: [32]byte{byte(i)},
ChainName: "Test-Full-Chain",
ID: [32]byte{byte(i)},
Height: uint64(i),
Timestamp: time.Now(),
}
err := q.SubmitBlock(block)
if err != nil {
t.Fatalf("SubmitBlock failed at %d: %v", i, err)
}
}
// Should not error - drops oldest when full
}
func TestSigner_AggregateSignatures_ContextCancelledDuringLoop(t *testing.T) {
h, err := NewSigner(2)
if err != nil {
t.Fatalf("NewSigner failed: %v", err)
}
_ = h.AddValidator("v1", 100)
_ = h.AddValidator("v2", 100)
msg := []byte("test message")
sig1, _ := h.SignMessage("v1", msg)
sig2, _ := h.SignMessage("v2", msg)
// Valid context should work
ctx := context.Background()
aggSig, err := h.AggregateSignaturesWithContext(ctx, msg, []*QuasarSig{sig1, sig2})
if err != nil {
t.Errorf("AggregateSignatures failed: %v", err)
}
if aggSig.SignerCount != 2 {
t.Errorf("expected 2 signers, got %d", aggSig.SignerCount)
}
}
// =============================================================================
// Threshold Mode Tests (hybrid.go threshold integration)
// =============================================================================
func TestSigner_ThresholdMode_GenerateKeys(t *testing.T) {
// Generate threshold key shares using the helper function
// t=1 means need at least 2 shares to sign
shares, groupKey, err := GenerateThresholdKeys(threshold.SchemeBLS, 1, 3)
if err != nil {
t.Fatalf("GenerateThresholdKeys failed: %v", err)
}
if len(shares) != 3 {
t.Fatalf("expected 3 shares, got %d", len(shares))
}
if groupKey == nil {
t.Fatal("expected non-nil group key")
}
// Verify all shares have the same group key
for i, share := range shares {
if !share.GroupKey().Equal(groupKey) {
t.Errorf("share %d has different group key", i)
}
}
}
func TestSigner_ThresholdMode_NewSignerWithThreshold(t *testing.T) {
// Generate threshold key shares
shares, groupKey, err := GenerateThresholdKeys(threshold.SchemeBLS, 1, 3)
if err != nil {
t.Fatalf("GenerateThresholdKeys failed: %v", err)
}
// Create key shares map
keyShares := make(map[string]threshold.KeyShare)
keyShares["v1"] = shares[0]
keyShares["v2"] = shares[1]
keyShares["v3"] = shares[2]
// Create hybrid engine with threshold mode
config := ThresholdConfig{
SchemeID: threshold.SchemeBLS,
Threshold: 1,
TotalParties: 3,
KeyShares: keyShares,
GroupKey: groupKey,
}
h, err := NewSignerWithThresholdConfig(config)
if err != nil {
t.Fatalf("NewSignerWithThreshold failed: %v", err)
}
// Verify threshold mode is enabled
if !h.IsThresholdMode() {
t.Error("expected threshold mode to be enabled")
}
// Verify scheme is set
if h.ThresholdScheme() == nil {
t.Error("expected threshold scheme to be set")
}
// Verify group key is set
if h.ThresholdGroupKey() == nil {
t.Error("expected threshold group key to be set")
}
}
func TestSigner_ThresholdMode_SignAndVerify(t *testing.T) {
// Generate threshold key shares (t=1 means need 2+ shares)
shares, groupKey, err := GenerateThresholdKeys(threshold.SchemeBLS, 1, 3)
if err != nil {
t.Fatalf("GenerateThresholdKeys failed: %v", err)
}
// Create key shares map
keyShares := make(map[string]threshold.KeyShare)
keyShares["v1"] = shares[0]
keyShares["v2"] = shares[1]
keyShares["v3"] = shares[2]
// Create hybrid engine with threshold mode
config := ThresholdConfig{
SchemeID: threshold.SchemeBLS,
Threshold: 1,
TotalParties: 3,
KeyShares: keyShares,
GroupKey: groupKey,
}
h, err := NewSignerWithThresholdConfig(config)
if err != nil {
t.Fatalf("NewSignerWithThreshold failed: %v", err)
}
// Sign message with threshold signing
ctx := context.Background()
message := []byte("test threshold signing")
// Get signature shares from 2 validators (enough for t+1=2)
share1, err := h.SignMessageThreshold(ctx, "v1", message)
if err != nil {
t.Fatalf("SignMessageThreshold v1 failed: %v", err)
}
share2, err := h.SignMessageThreshold(ctx, "v2", message)
if err != nil {
t.Fatalf("SignMessageThreshold v2 failed: %v", err)
}
// Verify signature shares were created
if share1 == nil || share2 == nil {
t.Fatal("expected non-nil signature shares")
}
// Aggregate signature shares
sig, err := h.AggregateThresholdSignatures(ctx, message, []threshold.SignatureShare{share1, share2})
if err != nil {
t.Fatalf("AggregateThresholdSignatures failed: %v", err)
}
// Verify the signature bytes are non-empty
if len(sig.Bytes()) == 0 {
t.Error("expected non-empty signature bytes")
}
// Note: Full threshold verification requires proper polynomial evaluation and
// Lagrange interpolation in the BLS scheme, which is not yet implemented.
// The current test setup uses the master key for all shares, making aggregated
// signatures invalid for true threshold verification.
// Production BLS threshold uses luxfi/threshold/protocols/frost for proper
// polynomial evaluation and Lagrange interpolation.
//
// For now, we verify the infrastructure is correctly wired up:
// - Key generation works
// - Signing produces valid shares
// - Aggregation produces a signature
// - The threshold scheme is correctly registered and usable
t.Log("Note: Full threshold signature verification pending BLS scalar field implementation")
}
func TestSigner_ThresholdMode_InsufficientShares(t *testing.T) {
// Generate threshold key shares (t=1 means need 2+ shares)
shares, groupKey, err := GenerateThresholdKeys(threshold.SchemeBLS, 1, 3)
if err != nil {
t.Fatalf("GenerateThresholdKeys failed: %v", err)
}
// Create key shares map
keyShares := make(map[string]threshold.KeyShare)
keyShares["v1"] = shares[0]
keyShares["v2"] = shares[1]
keyShares["v3"] = shares[2]
// Create hybrid engine with threshold mode
config := ThresholdConfig{
SchemeID: threshold.SchemeBLS,
Threshold: 1,
TotalParties: 3,
KeyShares: keyShares,
GroupKey: groupKey,
}
h, err := NewSignerWithThresholdConfig(config)
if err != nil {
t.Fatalf("NewSignerWithThreshold failed: %v", err)
}
// Sign message with threshold signing
ctx := context.Background()
message := []byte("test threshold signing")
// Get only 1 signature share (not enough for t+1=2)
share1, err := h.SignMessageThreshold(ctx, "v1", message)
if err != nil {
t.Fatalf("SignMessageThreshold v1 failed: %v", err)
}
// Try to aggregate with insufficient shares - should fail
_, err = h.AggregateThresholdSignatures(ctx, message, []threshold.SignatureShare{share1})
if err == nil {
t.Error("expected error for insufficient shares, got nil")
}
}
func TestSigner_ThresholdMode_NotEnabled(t *testing.T) {
// Create regular hybrid engine (non-threshold mode)
h, err := NewSigner(2)
if err != nil {
t.Fatalf("NewSigner failed: %v", err)
}
// Threshold mode should be disabled
if h.IsThresholdMode() {
t.Error("expected threshold mode to be disabled")
}
// Threshold operations should fail
ctx := context.Background()
_, err = h.SignMessageThreshold(ctx, "v1", []byte("test"))
if err == nil {
t.Error("expected error for SignMessageThreshold when threshold mode not enabled")
}
_, err = h.AggregateThresholdSignatures(ctx, []byte("test"), nil)
if err == nil {
t.Error("expected error for AggregateThresholdSignatures when threshold mode not enabled")
}
if h.VerifyThresholdSignature([]byte("test"), nil) {
t.Error("expected false for VerifyThresholdSignature when threshold mode not enabled")
}
}
func TestSigner_ThresholdMode_AddValidatorThreshold(t *testing.T) {
// Generate threshold key shares
shares, groupKey, err := GenerateThresholdKeys(threshold.SchemeBLS, 1, 3)
if err != nil {
t.Fatalf("GenerateThresholdKeys failed: %v", err)
}
// Start with only 2 validators
keyShares := make(map[string]threshold.KeyShare)
keyShares["v1"] = shares[0]
keyShares["v2"] = shares[1]
config := ThresholdConfig{
SchemeID: threshold.SchemeBLS,
Threshold: 1,
TotalParties: 3,
KeyShares: keyShares,
GroupKey: groupKey,
}
h, err := NewSignerWithThresholdConfig(config)
if err != nil {
t.Fatalf("NewSignerWithThresholdConfig failed: %v", err)
}
// Add third validator dynamically
err = h.AddValidatorThreshold("v3", shares[2], 100)
if err != nil {
t.Fatalf("AddValidatorThreshold failed: %v", err)
}
// Now v3 should be able to sign
ctx := context.Background()
share3, err := h.SignMessageThreshold(ctx, "v3", []byte("test"))
if err != nil {
t.Errorf("SignMessageThreshold v3 failed: %v", err)
}
if share3 == nil {
t.Error("expected non-nil share from v3")
}
}