mirror of
https://github.com/luxfi/consensus.git
synced 2026-09-24 00:26:58 +00:00
428 lines
9.3 KiB
Go
428 lines
9.3 KiB
Go
package quasar
|
|
|
|
import (
|
|
"context"
|
|
"testing"
|
|
"time"
|
|
|
|
"github.com/luxfi/consensus/config"
|
|
"github.com/luxfi/consensus/core/dag"
|
|
)
|
|
|
|
// mockStore is a simple mock implementation for testing
|
|
type mockStore struct {
|
|
vertices map[VertexID]*mockVertex
|
|
heads []VertexID
|
|
}
|
|
|
|
type mockVertex struct {
|
|
id VertexID
|
|
parents []VertexID
|
|
author string
|
|
round uint64
|
|
}
|
|
|
|
func (v *mockVertex) ID() VertexID { return v.id }
|
|
func (v *mockVertex) Parents() []VertexID { return v.parents }
|
|
func (v *mockVertex) Author() string { return v.author }
|
|
func (v *mockVertex) Round() uint64 { return v.round }
|
|
|
|
func newMockStore() *mockStore {
|
|
return &mockStore{
|
|
vertices: make(map[VertexID]*mockVertex),
|
|
heads: make([]VertexID, 0),
|
|
}
|
|
}
|
|
|
|
func (s *mockStore) Head() []VertexID {
|
|
return s.heads
|
|
}
|
|
|
|
func (s *mockStore) Get(id VertexID) (dag.BlockView[VertexID], bool) {
|
|
vertex, exists := s.vertices[id]
|
|
return vertex, exists
|
|
}
|
|
|
|
func (s *mockStore) Children(id VertexID) []VertexID {
|
|
return []VertexID{}
|
|
}
|
|
|
|
func TestNew(t *testing.T) {
|
|
cfg := config.DefaultParams()
|
|
q := NewBLS(cfg, newMockStore())
|
|
|
|
if q == nil {
|
|
t.Fatal("New() returned nil")
|
|
}
|
|
|
|
if q.K != cfg.K {
|
|
t.Errorf("K mismatch: got %d, want %d", q.K, cfg.K)
|
|
}
|
|
if q.Alpha != cfg.Alpha {
|
|
t.Errorf("Alpha mismatch: got %f, want %f", q.Alpha, cfg.Alpha)
|
|
}
|
|
if q.Beta != cfg.Beta {
|
|
t.Errorf("Beta mismatch: got %d, want %d", q.Beta, cfg.Beta)
|
|
}
|
|
}
|
|
|
|
func TestInitialize(t *testing.T) {
|
|
cfg := config.DefaultParams()
|
|
q := NewBLS(cfg, newMockStore())
|
|
|
|
ctx := context.Background()
|
|
blsKey := []byte("test-bls-key")
|
|
pqKey := []byte("test-pq-key")
|
|
|
|
err := q.Initialize(ctx, blsKey, pqKey)
|
|
if err != nil {
|
|
t.Fatalf("Initialize failed: %v", err)
|
|
}
|
|
|
|
// Verify keys are stored
|
|
if len(q.blsKey) == 0 {
|
|
t.Error("BLS key not stored")
|
|
}
|
|
if len(q.pqKey) == 0 {
|
|
t.Error("PQ key not stored")
|
|
}
|
|
}
|
|
|
|
func TestPhaseI_Propose(t *testing.T) {
|
|
cfg := config.DefaultParams()
|
|
q := NewBLS(cfg, newMockStore())
|
|
|
|
ctx := context.Background()
|
|
if err := q.Initialize(ctx, []byte("bls"), []byte("pq")); err != nil {
|
|
t.Fatalf("Initialize failed: %v", err)
|
|
}
|
|
|
|
// Mock DAG frontier
|
|
frontier := []string{"block1", "block2", "block3"}
|
|
|
|
proposal := q.phaseI(frontier)
|
|
|
|
if proposal == "" {
|
|
t.Error("phaseI returned empty proposal")
|
|
}
|
|
|
|
// Should select highest confidence block
|
|
found := false
|
|
for _, block := range frontier {
|
|
if block == proposal {
|
|
found = true
|
|
break
|
|
}
|
|
}
|
|
|
|
if !found {
|
|
t.Errorf("proposal %s not in frontier", proposal)
|
|
}
|
|
}
|
|
|
|
func TestPhaseII_Commit(t *testing.T) {
|
|
cfg := config.DefaultParams()
|
|
q := NewBLS(cfg, newMockStore())
|
|
|
|
ctx := context.Background()
|
|
if err := q.Initialize(ctx, []byte("bls"), []byte("pq")); err != nil {
|
|
t.Fatalf("Initialize failed: %v", err)
|
|
}
|
|
|
|
tests := []struct {
|
|
name string
|
|
votes map[string]int
|
|
proposal string
|
|
wantCert bool
|
|
}{
|
|
{
|
|
name: "sufficient agreement",
|
|
votes: map[string]int{
|
|
"block1": 18,
|
|
"block2": 3,
|
|
},
|
|
proposal: "block1",
|
|
wantCert: true,
|
|
},
|
|
{
|
|
name: "insufficient agreement",
|
|
votes: map[string]int{
|
|
"block1": 10,
|
|
"block2": 11,
|
|
},
|
|
proposal: "block1",
|
|
wantCert: false,
|
|
},
|
|
{
|
|
name: "exact threshold",
|
|
votes: map[string]int{
|
|
"block1": 16, // 16/20 = 0.8 = alpha
|
|
"block2": 4,
|
|
},
|
|
proposal: "block1",
|
|
wantCert: true,
|
|
},
|
|
}
|
|
|
|
for _, tt := range tests {
|
|
t.Run(tt.name, func(t *testing.T) {
|
|
cert := q.phaseII(tt.votes, tt.proposal)
|
|
|
|
if tt.wantCert && cert == nil {
|
|
t.Error("expected certificate, got nil")
|
|
}
|
|
if !tt.wantCert && cert != nil {
|
|
t.Error("expected no certificate, got one")
|
|
}
|
|
|
|
if cert != nil {
|
|
if len(cert.BLSAgg) == 0 {
|
|
t.Error("certificate missing BLS signature")
|
|
}
|
|
if len(cert.PQCert) == 0 {
|
|
t.Error("certificate missing PQ certificate")
|
|
}
|
|
}
|
|
})
|
|
}
|
|
}
|
|
|
|
func TestCertBundle_VerifyWithKeys_Engine(t *testing.T) {
|
|
blsKey := []byte("test-bls-key-32-bytes-long!!")
|
|
pqKey := []byte("test-pq-key-32-bytes-long!!!")
|
|
message := []byte("test message")
|
|
|
|
// Build a valid cert under KMAC256 with the canonical customizations.
|
|
cert := &CertBundle{
|
|
BLSAgg: kmac256(blsKey, message, kmacMACOutLen, customQuasarEventHorizonBLSMAC),
|
|
PQCert: kmac256(pqKey, message, kmacMACOutLen, customQuasarEventHorizonPQMAC),
|
|
Message: message,
|
|
}
|
|
|
|
valid := cert.VerifyWithKeys(blsKey, pqKey)
|
|
if !valid {
|
|
t.Error("certificate verification failed with correct keys")
|
|
}
|
|
|
|
// Test with wrong keys
|
|
valid = cert.VerifyWithKeys([]byte("wrong-key"), pqKey)
|
|
if valid {
|
|
t.Error("should fail with wrong BLS key")
|
|
}
|
|
|
|
// Test with empty BLS
|
|
emptyCert := &CertBundle{BLSAgg: nil, PQCert: cert.PQCert, Message: message}
|
|
valid = emptyCert.VerifyWithKeys(blsKey, pqKey)
|
|
if valid {
|
|
t.Error("empty BLS signature should fail verification")
|
|
}
|
|
|
|
// Test with empty PQ
|
|
emptyCert = &CertBundle{BLSAgg: cert.BLSAgg, PQCert: nil, Message: message}
|
|
valid = emptyCert.VerifyWithKeys(blsKey, pqKey)
|
|
if valid {
|
|
t.Error("empty PQ certificate should fail verification")
|
|
}
|
|
}
|
|
|
|
func TestCertBundle_Verify_Panics_Engine(t *testing.T) {
|
|
cert := &CertBundle{
|
|
BLSAgg: []byte{0x01},
|
|
PQCert: []byte{0x02},
|
|
}
|
|
defer func() {
|
|
r := recover()
|
|
if r == nil {
|
|
t.Error("CertBundle.Verify should panic")
|
|
}
|
|
}()
|
|
cert.Verify(nil)
|
|
}
|
|
|
|
func TestBlock(t *testing.T) {
|
|
block := &Block{
|
|
Height: 100,
|
|
Hash: "0xabc123",
|
|
Timestamp: time.Now(),
|
|
Cert: &QuasarCert{
|
|
BLS: []byte("bls"),
|
|
MLDSARollup: []byte("pq"),
|
|
},
|
|
}
|
|
|
|
if block.Height != 100 {
|
|
t.Errorf("Height mismatch: got %d, want 100", block.Height)
|
|
}
|
|
|
|
if block.Hash != "0xabc123" {
|
|
t.Errorf("Hash mismatch: got %s, want 0xabc123", block.Hash)
|
|
}
|
|
|
|
if block.Cert == nil {
|
|
t.Error("Block missing certificate")
|
|
}
|
|
}
|
|
|
|
func TestSetFinalizedCallback(t *testing.T) {
|
|
cfg := config.DefaultParams()
|
|
q := NewBLS(cfg, newMockStore())
|
|
|
|
called := false
|
|
q.SetFinalizedCallback(func(block *Block) {
|
|
called = true
|
|
if block.Height != 42 {
|
|
t.Errorf("callback got wrong height: %d", block.Height)
|
|
}
|
|
})
|
|
|
|
// Trigger callback
|
|
if q.finalizedCb != nil {
|
|
q.finalizedCb(&Block{Height: 42})
|
|
}
|
|
|
|
if !called {
|
|
t.Error("callback not called")
|
|
}
|
|
}
|
|
|
|
func TestQuantumFinality_Integration(t *testing.T) {
|
|
cfg := config.XChainParams() // Ultra-fast 1ms blocks
|
|
q := NewBLS(cfg, newMockStore())
|
|
|
|
ctx, cancel := context.WithTimeout(context.Background(), 100*time.Millisecond)
|
|
defer cancel()
|
|
|
|
// Initialize
|
|
err := q.Initialize(ctx, []byte("bls-key"), []byte("pq-key"))
|
|
if err != nil {
|
|
t.Fatalf("Initialize failed: %v", err)
|
|
}
|
|
|
|
// Set callback
|
|
finalized := make(chan *Block, 1)
|
|
q.SetFinalizedCallback(func(block *Block) {
|
|
select {
|
|
case finalized <- block:
|
|
default:
|
|
}
|
|
})
|
|
|
|
// Simulate consensus round
|
|
frontier := []string{"fast-block-1"}
|
|
proposal := q.phaseI(frontier)
|
|
|
|
votes := map[string]int{
|
|
proposal: 3, // 3/5 = 0.6 = alpha for X-Chain
|
|
"other": 2,
|
|
}
|
|
|
|
cert := q.phaseII(votes, proposal)
|
|
if cert == nil {
|
|
t.Skip("certificate creation not implemented in test mode")
|
|
}
|
|
|
|
// Create finalized block
|
|
qBlock := &Block{
|
|
Height: 1,
|
|
Hash: proposal,
|
|
Timestamp: time.Now(),
|
|
Cert: &QuasarCert{
|
|
BLS: cert.BLSAgg,
|
|
MLDSARollup: cert.PQCert,
|
|
},
|
|
}
|
|
|
|
// Trigger finalization
|
|
if q.finalizedCb != nil {
|
|
q.finalizedCb(qBlock)
|
|
}
|
|
|
|
// Wait for finalization
|
|
select {
|
|
case fb := <-finalized:
|
|
if fb.Hash != proposal {
|
|
t.Errorf("finalized wrong block: got %s, want %s", fb.Hash, proposal)
|
|
}
|
|
|
|
// Verify timing for 1ms blocks
|
|
elapsed := time.Since(fb.Timestamp)
|
|
if elapsed > 10*time.Millisecond {
|
|
t.Errorf("finalization too slow for 1ms blocks: %v", elapsed)
|
|
}
|
|
case <-ctx.Done():
|
|
t.Fatal("finalization timeout")
|
|
}
|
|
}
|
|
|
|
func BenchmarkPhaseI(b *testing.B) {
|
|
cfg := config.XChainParams()
|
|
q := NewBLS(cfg, newMockStore())
|
|
if err := q.Initialize(context.Background(), []byte("bls"), []byte("pq")); err != nil {
|
|
b.Fatalf("Initialize failed: %v", err)
|
|
}
|
|
|
|
frontier := []string{"b1", "b2", "b3", "b4", "b5"}
|
|
|
|
b.ResetTimer()
|
|
for i := 0; i < b.N; i++ {
|
|
_ = q.phaseI(frontier)
|
|
}
|
|
}
|
|
|
|
func BenchmarkPhaseII(b *testing.B) {
|
|
cfg := config.XChainParams()
|
|
q := NewBLS(cfg, newMockStore())
|
|
if err := q.Initialize(context.Background(), []byte("bls"), []byte("pq")); err != nil {
|
|
b.Fatalf("Initialize failed: %v", err)
|
|
}
|
|
|
|
votes := map[string]int{
|
|
"block1": 3,
|
|
"block2": 2,
|
|
}
|
|
|
|
b.ResetTimer()
|
|
for i := 0; i < b.N; i++ {
|
|
_ = q.phaseII(votes, "block1")
|
|
}
|
|
}
|
|
|
|
func BenchmarkCertVerifyWithKeys(b *testing.B) {
|
|
blsKey := []byte("bench-bls-key-32-bytes-long!!")
|
|
pqKey := []byte("bench-pq-key-32-bytes-long!!!")
|
|
message := []byte("benchmark message")
|
|
|
|
cert := &CertBundle{
|
|
BLSAgg: kmac256(blsKey, message, kmacMACOutLen, customQuasarEventHorizonBLSMAC),
|
|
PQCert: kmac256(pqKey, message, kmacMACOutLen, customQuasarEventHorizonPQMAC),
|
|
Message: message,
|
|
}
|
|
|
|
b.ResetTimer()
|
|
for i := 0; i < b.N; i++ {
|
|
_ = cert.VerifyWithKeys(blsKey, pqKey)
|
|
}
|
|
}
|
|
|
|
// NOTE: Post-quantum threshold crypto tests live in the Pulsar package at
|
|
// github.com/luxfi/corona/threshold. The quasar package uses that real
|
|
// implementation via the Signer type in quasar.go.
|
|
|
|
func TestBundle(t *testing.T) {
|
|
b := Bundle{
|
|
Epoch: 100,
|
|
Root: []byte("root hash"),
|
|
BLSAgg: []byte("bls aggregate"),
|
|
PQBatch: []byte("pq batch"),
|
|
Binding: []byte("binding"),
|
|
}
|
|
|
|
if b.Epoch != 100 {
|
|
t.Errorf("Epoch mismatch: got %d, want 100", b.Epoch)
|
|
}
|
|
|
|
if string(b.Root) != "root hash" {
|
|
t.Error("Root mismatch")
|
|
}
|
|
}
|