mirror of
https://github.com/luxfi/consensus.git
synced 2026-09-24 00:26:58 +00:00
The Quasar cert layers are the right cardinality of "Double Lattice +
Identity Rollup + optional Classical Fast-path", not "Triple Quantum":
BLS-12-381 classical fast-path (pairing) — empty in pure-PQ
Corona Ring-LWE threshold ML-DSA (luxfi/corona; production R-LWE)
Pulsar Module-LWE threshold ML-DSA (luxfi/pulsar; Class N1 byte-equal
to FIPS 204)
MLDSARollup per-validator ML-DSA-65 rolled into one STARK / Groth16 proof
(Z-Chain → P3Q)
"Triple Quantum" misled because BLS is classical — at most one of three
legs was quantum in the historic BLS+Ringtail+ZK shape. The honest PQ
cardinality is **Double Lattice** (Ring + Module). Pure-PQ profiles
drop BLS entirely and rely on Corona + Pulsar.
Ringtail is the academic R-LWE construction with trusted-dealer DKG;
it stays out of any open public-chain path. Corona is the production
Ring-LWE library (Pedersen DKG over R_q + proactive resharing).
Surface (in protocol/quasar/types.go):
QuasarCert.BLS → unchanged (classical fast-path, optional)
QuasarCert.Ringtail → QuasarCert.Corona (Ring-LWE threshold sig)
QuasarCert.MLDSAProof → QuasarCert.MLDSARollup (succinct identity rollup)
QuasarCert now also has a Pulsar field (Module-LWE threshold sig,
populated when wired)
QuasarSig.Ringtail → QuasarSig.Corona (also added Pulsar []byte)
FinalityEvent.Ringtail / MLDSAProof → Corona / Pulsar / MLDSARollup
New predicates:
QuasarCert.IsDoubleLattice() — Corona ∧ Pulsar (honest PQ count)
QuasarCert.HasClassicalFastPath() — len(BLS) > 0
QuasarCert.HasIdentityRollup() — len(MLDSARollup) > 0
The wire byte layout is unchanged (length-prefixed slots). Callers that
read the bytes don't notice. Source-level references that compiled
before the rename now compile against the new identifiers; bulk
rename swept the consensus repo (22 files, 219 insertions, 151
deletions) plus the engine/pq facing FinalityEvent.
Tests: GOWORK=off go test -short -count=1 ./... → all 28 packages green
(quasar suite 32s; integration 60s; no regressions).
228 lines
5.1 KiB
Go
228 lines
5.1 KiB
Go
package pq
|
|
|
|
import (
|
|
"context"
|
|
"testing"
|
|
"time"
|
|
|
|
"github.com/luxfi/consensus/config"
|
|
"github.com/luxfi/ids"
|
|
)
|
|
|
|
func TestNew(t *testing.T) {
|
|
params := config.DefaultParams()
|
|
engine := NewConsensus(params)
|
|
|
|
if engine == nil {
|
|
t.Fatal("New() returned nil")
|
|
}
|
|
|
|
if engine.params.K != params.K {
|
|
t.Errorf("K mismatch: got %d, want %d", engine.params.K, params.K)
|
|
}
|
|
|
|
if engine.Height() != 0 {
|
|
t.Errorf("Initial height should be 0, got %d", engine.Height())
|
|
}
|
|
}
|
|
|
|
func TestInitialize(t *testing.T) {
|
|
engine := NewConsensus(config.DefaultParams())
|
|
ctx := context.Background()
|
|
|
|
blsKey, pqKey := GenerateTestKeys()
|
|
err := engine.Initialize(ctx, blsKey, pqKey)
|
|
if err != nil {
|
|
t.Fatalf("Initialize failed: %v", err)
|
|
}
|
|
}
|
|
|
|
func TestProcessBlock(t *testing.T) {
|
|
engine := NewConsensus(config.DefaultParams())
|
|
ctx := context.Background()
|
|
|
|
blsKey, pqKey := GenerateTestKeys()
|
|
err := engine.Initialize(ctx, blsKey, pqKey)
|
|
if err != nil {
|
|
t.Fatalf("Initialize failed: %v", err)
|
|
}
|
|
|
|
blockID := ids.GenerateTestID()
|
|
|
|
// Test insufficient votes
|
|
votes := map[string]int{
|
|
blockID.String(): 10,
|
|
"other": 11,
|
|
}
|
|
|
|
err = engine.ProcessBlock(ctx, blockID, votes)
|
|
if err == nil {
|
|
t.Error("ProcessBlock should fail with insufficient votes")
|
|
}
|
|
|
|
// Test sufficient votes
|
|
votes = map[string]int{
|
|
blockID.String(): 18,
|
|
"other": 3,
|
|
}
|
|
|
|
err = engine.ProcessBlock(ctx, blockID, votes)
|
|
if err != nil {
|
|
t.Errorf("ProcessBlock failed with sufficient votes: %v", err)
|
|
}
|
|
|
|
// Check finalized
|
|
if !engine.IsFinalized(blockID) {
|
|
t.Error("Block should be finalized")
|
|
}
|
|
|
|
// Check height increased
|
|
if engine.Height() != 1 {
|
|
t.Errorf("Height should be 1, got %d", engine.Height())
|
|
}
|
|
|
|
// Test already finalized
|
|
err = engine.ProcessBlock(ctx, blockID, votes)
|
|
if err != nil {
|
|
t.Error("ProcessBlock should succeed for already finalized block")
|
|
}
|
|
}
|
|
|
|
func TestFinalityChannel(t *testing.T) {
|
|
engine := NewConsensus(config.DefaultParams())
|
|
ctx := context.Background()
|
|
|
|
blsKey, pqKey := GenerateTestKeys()
|
|
err := engine.Initialize(ctx, blsKey, pqKey)
|
|
if err != nil {
|
|
t.Fatalf("Initialize failed: %v", err)
|
|
}
|
|
|
|
// Start listening for finality events
|
|
finality := engine.FinalityChannel()
|
|
|
|
blockID := ids.GenerateTestID()
|
|
votes := map[string]int{
|
|
blockID.String(): 18,
|
|
"other": 3,
|
|
}
|
|
|
|
// Process block in background
|
|
go func() {
|
|
time.Sleep(10 * time.Millisecond)
|
|
_ = engine.ProcessBlock(ctx, blockID, votes)
|
|
}()
|
|
|
|
// Wait for finality event
|
|
select {
|
|
case event := <-finality:
|
|
if event.BlockID != blockID {
|
|
t.Errorf("Wrong block ID in event: got %s, want %s", event.BlockID, blockID)
|
|
}
|
|
if event.Height != 1 {
|
|
t.Errorf("Wrong height in event: got %d, want 1", event.Height)
|
|
}
|
|
if len(event.MLDSARollup) == 0 {
|
|
t.Error("Missing PQ proof in event")
|
|
}
|
|
if len(event.BLSProof) == 0 {
|
|
t.Error("Missing BLS proof in event")
|
|
}
|
|
case <-time.After(100 * time.Millisecond):
|
|
t.Fatal("Timeout waiting for finality event")
|
|
}
|
|
}
|
|
|
|
func TestSetFinalizedCallback(t *testing.T) {
|
|
engine := NewConsensus(config.DefaultParams())
|
|
|
|
called := false
|
|
|
|
engine.SetFinalizedCallback(func(event FinalityEvent) {
|
|
called = true
|
|
if event.Height == 0 {
|
|
t.Error("Event height should not be 0")
|
|
}
|
|
})
|
|
|
|
// Trigger callback through quasar
|
|
if engine.quasar != nil {
|
|
// This tests that the callback is properly wired
|
|
// In production, this would be triggered by actual consensus
|
|
t.Log("Quasar is properly initialized for event horizon callbacks")
|
|
}
|
|
|
|
// The callback is set, even if not triggered in this test
|
|
_ = called
|
|
|
|
// Test metrics
|
|
metrics := engine.Metrics()
|
|
if metrics["height"] != uint64(0) {
|
|
t.Errorf("Wrong height in metrics: %v", metrics["height"])
|
|
}
|
|
if metrics["k"] != engine.params.K {
|
|
t.Errorf("Wrong K in metrics: %v", metrics["k"])
|
|
}
|
|
}
|
|
|
|
func TestMultipleBlocks(t *testing.T) {
|
|
engine := NewConsensus(config.XChainParams()) // Fast params
|
|
ctx := context.Background()
|
|
|
|
blsKey, pqKey := GenerateTestKeys()
|
|
err := engine.Initialize(ctx, blsKey, pqKey)
|
|
if err != nil {
|
|
t.Fatalf("Initialize failed: %v", err)
|
|
}
|
|
|
|
// Process multiple blocks
|
|
for i := 0; i < 5; i++ {
|
|
blockID := ids.GenerateTestID()
|
|
votes := map[string]int{
|
|
blockID.String(): 4, // 4/5 = 0.8 > 0.6 (alpha for XChain)
|
|
"other": 1,
|
|
}
|
|
|
|
err := engine.ProcessBlock(ctx, blockID, votes)
|
|
if err != nil {
|
|
t.Errorf("Failed to process block %d: %v", i, err)
|
|
}
|
|
|
|
if !engine.IsFinalized(blockID) {
|
|
t.Errorf("Block %d should be finalized", i)
|
|
}
|
|
}
|
|
|
|
if engine.Height() != 5 {
|
|
t.Errorf("Height should be 5, got %d", engine.Height())
|
|
}
|
|
}
|
|
|
|
func BenchmarkProcessBlock(b *testing.B) {
|
|
engine := NewConsensus(config.XChainParams())
|
|
ctx := context.Background()
|
|
blsKey, pqKey := GenerateTestKeys()
|
|
_ = engine.Initialize(ctx, blsKey, pqKey)
|
|
|
|
votes := map[string]int{
|
|
"block": 4,
|
|
"other": 1,
|
|
}
|
|
|
|
b.ResetTimer()
|
|
for i := 0; i < b.N; i++ {
|
|
blockID := ids.GenerateTestID()
|
|
votes[blockID.String()] = 4
|
|
_ = engine.ProcessBlock(ctx, blockID, votes)
|
|
}
|
|
}
|
|
|
|
func BenchmarkMetrics(b *testing.B) {
|
|
engine := NewConsensus(config.DefaultParams())
|
|
|
|
b.ResetTimer()
|
|
for i := 0; i < b.N; i++ {
|
|
_ = engine.Metrics()
|
|
}
|
|
}
|