mirror of
https://github.com/luxfi/consensus.git
synced 2026-09-24 00:26:58 +00:00
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.
2498 lines
62 KiB
Go
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")
|
|
}
|
|
}
|