mirror of
https://github.com/luxfi/consensus.git
synced 2026-09-24 00:26:58 +00:00
470 lines
12 KiB
Go
470 lines
12 KiB
Go
// Copyright (C) 2025, Lux Industries Inc All rights reserved.
|
|
|
|
package quasar
|
|
|
|
import (
|
|
"crypto/sha256"
|
|
"testing"
|
|
"time"
|
|
|
|
"github.com/stretchr/testify/require"
|
|
)
|
|
|
|
func TestQuantumBundle_Hash(t *testing.T) {
|
|
qb := &QuantumBundle{
|
|
Epoch: 1,
|
|
Sequence: 5,
|
|
StartHeight: 100,
|
|
EndHeight: 105,
|
|
BlockCount: 6,
|
|
MerkleRoot: sha256.Sum256([]byte("merkle")),
|
|
PreviousHash: sha256.Sum256([]byte("prev")),
|
|
Timestamp: 1234567890,
|
|
}
|
|
|
|
hash1 := qb.Hash()
|
|
hash2 := qb.Hash()
|
|
|
|
// Same bundle produces same hash
|
|
require.Equal(t, hash1, hash2)
|
|
|
|
// Different bundle produces different hash
|
|
qb2 := *qb
|
|
qb2.Sequence = 6
|
|
hash3 := qb2.Hash()
|
|
require.NotEqual(t, hash1, hash3)
|
|
}
|
|
|
|
func TestQuantumBundle_SignableMessage(t *testing.T) {
|
|
qb := &QuantumBundle{
|
|
Epoch: 1,
|
|
Sequence: 0,
|
|
StartHeight: 0,
|
|
EndHeight: 5,
|
|
BlockCount: 6,
|
|
MerkleRoot: sha256.Sum256([]byte("test")),
|
|
PreviousHash: [32]byte{},
|
|
Timestamp: time.Now().Unix(),
|
|
}
|
|
|
|
msg := qb.SignableMessage()
|
|
require.Contains(t, msg, "QUASAR-QB-v1:")
|
|
require.Len(t, msg, 77) // "QUASAR-QB-v1:" (13 chars) + 64 hex chars
|
|
}
|
|
|
|
func TestComputeMerkleRoot(t *testing.T) {
|
|
t.Run("empty", func(t *testing.T) {
|
|
root := ComputeMerkleRoot(nil)
|
|
require.Equal(t, [32]byte{}, root)
|
|
})
|
|
|
|
t.Run("single", func(t *testing.T) {
|
|
h := sha256.Sum256([]byte("block1"))
|
|
root := ComputeMerkleRoot([][32]byte{h})
|
|
require.Equal(t, h, root)
|
|
})
|
|
|
|
t.Run("two", func(t *testing.T) {
|
|
h1 := sha256.Sum256([]byte("block1"))
|
|
h2 := sha256.Sum256([]byte("block2"))
|
|
root := ComputeMerkleRoot([][32]byte{h1, h2})
|
|
|
|
// Manual calculation
|
|
combined := append(h1[:], h2[:]...)
|
|
expected := sha256.Sum256(combined)
|
|
require.Equal(t, expected, root)
|
|
})
|
|
|
|
t.Run("three_pads", func(t *testing.T) {
|
|
h1 := sha256.Sum256([]byte("block1"))
|
|
h2 := sha256.Sum256([]byte("block2"))
|
|
h3 := sha256.Sum256([]byte("block3"))
|
|
root := ComputeMerkleRoot([][32]byte{h1, h2, h3})
|
|
|
|
// Should pad h3 and compute correctly
|
|
require.NotEqual(t, [32]byte{}, root)
|
|
})
|
|
|
|
t.Run("six_blocks", func(t *testing.T) {
|
|
hashes := make([][32]byte, 6)
|
|
for i := 0; i < 6; i++ {
|
|
hashes[i] = sha256.Sum256([]byte{byte(i)})
|
|
}
|
|
root := ComputeMerkleRoot(hashes)
|
|
require.NotEqual(t, [32]byte{}, root)
|
|
|
|
// Consistent with same input
|
|
root2 := ComputeMerkleRoot(hashes)
|
|
require.Equal(t, root, root2)
|
|
})
|
|
}
|
|
|
|
func TestBundleSigner_Basic(t *testing.T) {
|
|
em := NewEpochManager(2, 3) // 2-of-N threshold, keep 3 epochs
|
|
validators := []string{"v0", "v1", "v2"}
|
|
_, err := em.InitializeEpoch(validators)
|
|
require.NoError(t, err)
|
|
|
|
signer := NewBundleSigner(em)
|
|
require.NotNil(t, signer)
|
|
require.Equal(t, 0, signer.PendingCount())
|
|
require.Nil(t, signer.LastBundle())
|
|
}
|
|
|
|
func TestBundleSigner_AddBLSBlock(t *testing.T) {
|
|
em := NewEpochManager(2, 3)
|
|
validators := []string{"v0", "v1", "v2"}
|
|
em.InitializeEpoch(validators)
|
|
|
|
signer := NewBundleSigner(em)
|
|
|
|
// Add 6 BLS blocks (simulating 3 seconds at 500ms each)
|
|
for i := 0; i < 6; i++ {
|
|
hash := sha256.Sum256([]byte{byte(i)})
|
|
signer.AddBLSBlock(uint64(i), hash)
|
|
}
|
|
|
|
require.Equal(t, 6, signer.PendingCount())
|
|
}
|
|
|
|
func TestBundleSigner_CreateBundle(t *testing.T) {
|
|
em := NewEpochManager(2, 3)
|
|
validators := []string{"v0", "v1", "v2"}
|
|
em.InitializeEpoch(validators)
|
|
|
|
signer := NewBundleSigner(em)
|
|
|
|
// No pending blocks returns nil
|
|
qb := signer.CreateBundle()
|
|
require.Nil(t, qb)
|
|
|
|
// Add 6 BLS blocks
|
|
for i := 0; i < 6; i++ {
|
|
hash := sha256.Sum256([]byte{byte(i)})
|
|
signer.AddBLSBlock(uint64(100+i), hash)
|
|
}
|
|
|
|
// Create bundle
|
|
qb = signer.CreateBundle()
|
|
require.NotNil(t, qb)
|
|
require.Equal(t, uint64(0), qb.Epoch)
|
|
require.Equal(t, uint64(0), qb.Sequence)
|
|
require.Equal(t, uint64(100), qb.StartHeight)
|
|
require.Equal(t, uint64(105), qb.EndHeight)
|
|
require.Equal(t, 6, qb.BlockCount)
|
|
require.Len(t, qb.BlockHashes, 6)
|
|
require.NotEqual(t, [32]byte{}, qb.MerkleRoot)
|
|
require.Equal(t, [32]byte{}, qb.PreviousHash) // First bundle has no previous
|
|
|
|
// Pending cleared
|
|
require.Equal(t, 0, signer.PendingCount())
|
|
|
|
// Create another - should link to previous
|
|
for i := 0; i < 4; i++ {
|
|
hash := sha256.Sum256([]byte{byte(10 + i)})
|
|
signer.AddBLSBlock(uint64(106+i), hash)
|
|
}
|
|
|
|
qb2 := signer.CreateBundle()
|
|
require.NotNil(t, qb2)
|
|
require.Equal(t, uint64(1), qb2.Sequence)
|
|
require.Equal(t, qb.Hash(), qb2.PreviousHash) // Linked to previous
|
|
require.Equal(t, 4, qb2.BlockCount)
|
|
}
|
|
|
|
func TestBundleSigner_SignBundle(t *testing.T) {
|
|
em := NewEpochManager(2, 3)
|
|
validators := []string{"v0", "v1", "v2"}
|
|
_, err := em.InitializeEpoch(validators)
|
|
require.NoError(t, err)
|
|
|
|
signer := NewBundleSigner(em)
|
|
|
|
// Add BLS blocks
|
|
for i := 0; i < 6; i++ {
|
|
hash := sha256.Sum256([]byte{byte(i)})
|
|
signer.AddBLSBlock(uint64(i), hash)
|
|
}
|
|
|
|
qb := signer.CreateBundle()
|
|
require.NotNil(t, qb)
|
|
require.Nil(t, qb.Signature)
|
|
|
|
// Sign with all validators
|
|
prfKey := []byte("prf-key-for-quantum-bundle-test!")
|
|
err = signer.SignBundle(qb, 1, prfKey, validators)
|
|
require.NoError(t, err)
|
|
require.NotNil(t, qb.Signature)
|
|
|
|
// Verify
|
|
valid := signer.VerifyBundle(qb)
|
|
require.True(t, valid)
|
|
}
|
|
|
|
func TestBundleSigner_SignWithThreshold(t *testing.T) {
|
|
em := NewEpochManager(2, 3) // 2-of-3 threshold
|
|
validators := []string{"v0", "v1", "v2"}
|
|
_, err := em.InitializeEpoch(validators)
|
|
require.NoError(t, err)
|
|
|
|
signer := NewBundleSigner(em)
|
|
|
|
for i := 0; i < 6; i++ {
|
|
hash := sha256.Sum256([]byte{byte(i)})
|
|
signer.AddBLSBlock(uint64(i), hash)
|
|
}
|
|
|
|
qb := signer.CreateBundle()
|
|
prfKey := []byte("prf-key-for-threshold-signing!!!")
|
|
|
|
// Sign with exactly 2 validators (threshold)
|
|
err = signer.SignBundle(qb, 1, prfKey, []string{"v0", "v1"})
|
|
require.NoError(t, err)
|
|
require.NotNil(t, qb.Signature)
|
|
|
|
valid := signer.VerifyBundle(qb)
|
|
require.True(t, valid)
|
|
}
|
|
|
|
func TestBundleSigner_InsufficientSigners(t *testing.T) {
|
|
em := NewEpochManager(2, 3)
|
|
validators := []string{"v0", "v1", "v2"}
|
|
em.InitializeEpoch(validators)
|
|
|
|
signer := NewBundleSigner(em)
|
|
|
|
for i := 0; i < 3; i++ {
|
|
hash := sha256.Sum256([]byte{byte(i)})
|
|
signer.AddBLSBlock(uint64(i), hash)
|
|
}
|
|
|
|
qb := signer.CreateBundle()
|
|
prfKey := []byte("prf-key-for-insufficient-test!!!")
|
|
|
|
// Sign with only 1 validator (below threshold)
|
|
err := signer.SignBundle(qb, 1, prfKey, []string{"v0"})
|
|
require.Error(t, err)
|
|
require.Contains(t, err.Error(), "insufficient signers")
|
|
}
|
|
|
|
func TestBundleSigner_RejectsDuplicateSigners(t *testing.T) {
|
|
em := NewEpochManager(2, 3)
|
|
_, err := em.InitializeEpoch([]string{"v0", "v1", "v2"})
|
|
require.NoError(t, err)
|
|
|
|
signer := NewBundleSigner(em)
|
|
hash := sha256.Sum256([]byte("block"))
|
|
signer.AddBLSBlock(1, hash)
|
|
bundle := signer.CreateBundle()
|
|
|
|
err = signer.SignBundle(bundle, 1, []byte("duplicate-signer-prf-key"), []string{"v0", "v0"})
|
|
require.ErrorContains(t, err, "duplicate signer")
|
|
require.Nil(t, bundle.Signature)
|
|
}
|
|
|
|
func TestBundleSigningSessionIDSeparatesRetries(t *testing.T) {
|
|
bundleHash := sha256.Sum256([]byte("bundle"))
|
|
const sessionID = 42
|
|
|
|
first := bundleSigningSessionID(bundleHash, sessionID, 0)
|
|
retry1 := bundleSigningSessionID(bundleHash, sessionID, 1)
|
|
retry2 := bundleSigningSessionID(bundleHash, sessionID, 2)
|
|
|
|
require.Equal(t, sessionID, first)
|
|
require.NotEqual(t, first, retry1)
|
|
require.NotEqual(t, retry1, retry2)
|
|
require.Equal(t, retry1, bundleSigningSessionID(bundleHash, sessionID, 1))
|
|
}
|
|
|
|
func TestAsyncBundleSigner_SignAsync(t *testing.T) {
|
|
em := NewEpochManager(2, 3)
|
|
validators := []string{"v0", "v1", "v2"}
|
|
em.InitializeEpoch(validators)
|
|
|
|
signer := NewAsyncBundleSigner(em)
|
|
|
|
for i := 0; i < 6; i++ {
|
|
hash := sha256.Sum256([]byte{byte(i)})
|
|
signer.AddBLSBlock(uint64(i), hash)
|
|
}
|
|
|
|
qb := signer.CreateBundle()
|
|
prfKey := []byte("prf-key-for-async-signing-test!!")
|
|
|
|
// Start async signing
|
|
signer.SignBundleAsync(qb, 1, prfKey, validators)
|
|
|
|
// Wait for signed bundle
|
|
select {
|
|
case signedQB := <-signer.SignedBundles():
|
|
require.NotNil(t, signedQB.Signature)
|
|
valid := signer.VerifyBundle(signedQB)
|
|
require.True(t, valid)
|
|
case <-time.After(30 * time.Second):
|
|
t.Fatal("async signing timed out")
|
|
}
|
|
}
|
|
|
|
func TestBundleSigner_MerkleVerification(t *testing.T) {
|
|
em := NewEpochManager(2, 3)
|
|
validators := []string{"v0", "v1", "v2"}
|
|
em.InitializeEpoch(validators)
|
|
|
|
signer := NewBundleSigner(em)
|
|
|
|
hashes := make([][32]byte, 6)
|
|
for i := 0; i < 6; i++ {
|
|
hashes[i] = sha256.Sum256([]byte{byte(i)})
|
|
signer.AddBLSBlock(uint64(i), hashes[i])
|
|
}
|
|
|
|
qb := signer.CreateBundle()
|
|
prfKey := []byte("prf-key-for-merkle-verify-test!!")
|
|
require.NoError(t, signer.SignBundle(qb, 1, prfKey, validators))
|
|
|
|
// Valid bundle verifies
|
|
require.True(t, signer.VerifyBundle(qb))
|
|
|
|
// Tampered Merkle root fails
|
|
qbTampered := *qb
|
|
qbTampered.MerkleRoot = sha256.Sum256([]byte("tampered"))
|
|
require.False(t, signer.VerifyBundle(&qbTampered))
|
|
|
|
// Tampered block hashes fail
|
|
qbTampered2 := *qb
|
|
qbTampered2.BlockHashes = make([][32]byte, 6)
|
|
require.False(t, signer.VerifyBundle(&qbTampered2))
|
|
}
|
|
|
|
func TestBundleRunner_StartStop(t *testing.T) {
|
|
em := NewEpochManager(2, 3)
|
|
validators := []string{"v0", "v1", "v2"}
|
|
em.InitializeEpoch(validators)
|
|
|
|
signer := NewAsyncBundleSigner(em)
|
|
prfKey := []byte("prf-key-for-runner-test-signing!")
|
|
|
|
runner := NewBundleRunner(signer, validators, prfKey)
|
|
require.NotNil(t, runner)
|
|
|
|
// Start runner
|
|
runner.Start()
|
|
|
|
// Add some BLS blocks
|
|
for i := 0; i < 6; i++ {
|
|
hash := sha256.Sum256([]byte{byte(i)})
|
|
signer.AddBLSBlock(uint64(i), hash)
|
|
time.Sleep(10 * time.Millisecond)
|
|
}
|
|
|
|
// Wait a bit for runner to process
|
|
time.Sleep(100 * time.Millisecond)
|
|
|
|
// Stop should complete cleanly
|
|
done := make(chan struct{})
|
|
go func() {
|
|
runner.Stop()
|
|
close(done)
|
|
}()
|
|
|
|
select {
|
|
case <-done:
|
|
// Success
|
|
case <-time.After(2 * time.Second):
|
|
t.Fatal("runner.Stop() timed out")
|
|
}
|
|
}
|
|
|
|
func TestBundleSigner_ChainLinkage(t *testing.T) {
|
|
em := NewEpochManager(2, 3)
|
|
validators := []string{"v0", "v1", "v2"}
|
|
em.InitializeEpoch(validators)
|
|
|
|
signer := NewBundleSigner(em)
|
|
prfKey := []byte("prf-key-for-chain-linkage-test!!")
|
|
|
|
// Create a chain of 3 bundles
|
|
var bundles []*QuantumBundle
|
|
|
|
for qbIdx := 0; qbIdx < 3; qbIdx++ {
|
|
for i := 0; i < 6; i++ {
|
|
height := uint64(qbIdx*6 + i)
|
|
hash := sha256.Sum256([]byte{byte(height)})
|
|
signer.AddBLSBlock(height, hash)
|
|
}
|
|
|
|
qb := signer.CreateBundle()
|
|
signer.SignBundle(qb, qbIdx+1, prfKey, validators)
|
|
bundles = append(bundles, qb)
|
|
}
|
|
|
|
// Verify chain linkage
|
|
require.Equal(t, [32]byte{}, bundles[0].PreviousHash) // Genesis has no previous
|
|
require.Equal(t, bundles[0].Hash(), bundles[1].PreviousHash) // Bundle 1 links to 0
|
|
require.Equal(t, bundles[1].Hash(), bundles[2].PreviousHash) // Bundle 2 links to 1
|
|
|
|
// Verify sequences
|
|
require.Equal(t, uint64(0), bundles[0].Sequence)
|
|
require.Equal(t, uint64(1), bundles[1].Sequence)
|
|
require.Equal(t, uint64(2), bundles[2].Sequence)
|
|
|
|
// All bundles verify
|
|
for i, qb := range bundles {
|
|
require.True(t, signer.VerifyBundle(qb), "bundle %d should verify", i)
|
|
}
|
|
}
|
|
|
|
func BenchmarkBundleSigner_Create(b *testing.B) {
|
|
em := NewEpochManager(2, 3)
|
|
validators := []string{"v0", "v1", "v2"}
|
|
em.InitializeEpoch(validators)
|
|
|
|
signer := NewBundleSigner(em)
|
|
|
|
b.ResetTimer()
|
|
for i := 0; i < b.N; i++ {
|
|
// Add 6 BLS blocks
|
|
for j := 0; j < 6; j++ {
|
|
hash := sha256.Sum256([]byte{byte(i), byte(j)})
|
|
signer.AddBLSBlock(uint64(i*6+j), hash)
|
|
}
|
|
signer.CreateBundle()
|
|
}
|
|
}
|
|
|
|
func BenchmarkBundleSigner_SignAndVerify(b *testing.B) {
|
|
em := NewEpochManager(2, 3)
|
|
validators := []string{"v0", "v1", "v2"}
|
|
em.InitializeEpoch(validators)
|
|
|
|
signer := NewBundleSigner(em)
|
|
prfKey := []byte("prf-key-for-benchmark-signing!!!")
|
|
|
|
b.ResetTimer()
|
|
for i := 0; i < b.N; i++ {
|
|
for j := 0; j < 6; j++ {
|
|
hash := sha256.Sum256([]byte{byte(i), byte(j)})
|
|
signer.AddBLSBlock(uint64(i*6+j), hash)
|
|
}
|
|
qb := signer.CreateBundle()
|
|
signer.SignBundle(qb, i, prfKey, validators)
|
|
signer.VerifyBundle(qb)
|
|
}
|
|
}
|
|
|
|
func BenchmarkMerkleRoot(b *testing.B) {
|
|
sizes := []int{6, 12, 24, 60}
|
|
for _, n := range sizes {
|
|
b.Run("blocks="+string(rune('0'+n/10))+string(rune('0'+n%10)), func(b *testing.B) {
|
|
hashes := make([][32]byte, n)
|
|
for i := 0; i < n; i++ {
|
|
hashes[i] = sha256.Sum256([]byte{byte(i)})
|
|
}
|
|
b.ResetTimer()
|
|
for i := 0; i < b.N; i++ {
|
|
ComputeMerkleRoot(hashes)
|
|
}
|
|
})
|
|
}
|
|
}
|