mirror of
https://github.com/QuilibriumNetwork/ceremonyclient.git
synced 2026-02-21 18:37:26 +08:00
* wip: conversion of hotstuff from flow into Q-oriented model * bulk of tests * remaining non-integration tests * add integration test, adjust log interface, small tweaks * further adjustments, restore full pacemaker shape * add component lifecycle management+supervisor * further refinements * resolve timeout hanging * mostly finalized state for consensus * bulk of engine swap out * lifecycle-ify most types * wiring nearly complete, missing needed hooks for proposals * plugged in, vetting message validation paths * global consensus, plugged in and verified * app shard now wired in too * do not decode empty keys.yml (#456) * remove obsolete engine.maxFrames config parameter (#454) * default to Info log level unless debug is enabled (#453) * respect config's "logging" section params, remove obsolete single-file logging (#452) * Trivial code cleanup aiming to reduce Go compiler warnings (#451) * simplify range traversal * simplify channel read for single select case * delete rand.Seed() deprecated in Go 1.20 and no-op as of Go 1.24 * simplify range traversal * simplify channel read for single select case * remove redundant type from array * simplify range traversal * simplify channel read for single select case * RC slate * finalize 2.1.0.5 * Update comments in StrictMonotonicCounter Fix comment formatting and clarify description. --------- Co-authored-by: Black Swan <3999712+blacks1ne@users.noreply.github.com>
327 lines
9.9 KiB
Go
327 lines
9.9 KiB
Go
package time
|
|
|
|
import (
|
|
"context"
|
|
"fmt"
|
|
"sync"
|
|
"testing"
|
|
"time"
|
|
|
|
"github.com/stretchr/testify/assert"
|
|
"github.com/stretchr/testify/require"
|
|
"go.uber.org/zap"
|
|
"source.quilibrium.com/quilibrium/monorepo/lifecycle"
|
|
"source.quilibrium.com/quilibrium/monorepo/protobufs"
|
|
)
|
|
|
|
// TestGlobalTimeReel_MassiveEquivocationForkChoice tests a scenario where:
|
|
// - 200 frames are produced by one set of signers (bitmask 0b11100011)
|
|
// - Then 200 conflicting frames are produced by another set (bitmask 0b11111100)
|
|
// - The overlapping signers (0b11100000) should be detected as equivocating
|
|
// - Fork choice should still work, ignoring the equivocating signers
|
|
func TestGlobalTimeReel_MassiveEquivocationForkChoice(t *testing.T) {
|
|
logger, _ := zap.NewDevelopment()
|
|
s := setupTestClockStore(t)
|
|
atr, err := NewGlobalTimeReel(logger, createTestProverRegistry(true), s, 99, true)
|
|
require.NoError(t, err)
|
|
|
|
ctx, cancel, _ := lifecycle.WithSignallerAndCancel(context.Background())
|
|
go atr.Start(ctx, func() {})
|
|
time.Sleep(100 * time.Millisecond)
|
|
defer cancel()
|
|
eventCh := atr.GetEventCh()
|
|
|
|
// Collect events
|
|
var eventsMu sync.Mutex
|
|
var equivocationEvents []GlobalEvent
|
|
var forkEvents []GlobalEvent
|
|
stopCollecting := make(chan struct{})
|
|
|
|
go func() {
|
|
for {
|
|
select {
|
|
case event := <-eventCh:
|
|
eventsMu.Lock()
|
|
switch event.Type {
|
|
case TimeReelEventEquivocationDetected:
|
|
equivocationEvents = append(equivocationEvents, event)
|
|
case TimeReelEventForkDetected:
|
|
forkEvents = append(forkEvents, event)
|
|
}
|
|
eventsMu.Unlock()
|
|
case <-stopCollecting:
|
|
return
|
|
}
|
|
}
|
|
}()
|
|
|
|
// Insert genesis
|
|
genesis := &protobufs.GlobalFrame{
|
|
Header: &protobufs.GlobalFrameHeader{
|
|
FrameNumber: 0,
|
|
Timestamp: 1000,
|
|
Difficulty: 100,
|
|
Output: []byte("genesis"),
|
|
ParentSelector: []byte{},
|
|
},
|
|
}
|
|
|
|
err = atr.Insert(genesis)
|
|
require.NoError(t, err)
|
|
|
|
// Build chain A: 200 frames with bitmask 0b11100011 (signers 0,1,5,6,7)
|
|
prevOutput := genesis.Header.Output
|
|
for i := 1; i <= 200; i++ {
|
|
frameA := &protobufs.GlobalFrame{
|
|
Header: &protobufs.GlobalFrameHeader{
|
|
FrameNumber: uint64(i),
|
|
Timestamp: int64(1000 + i*1000),
|
|
Difficulty: uint32(100 + i),
|
|
Output: []byte(fmt.Sprintf("frameA_%d", i)),
|
|
ParentSelector: computeGlobalPoseidonHash(prevOutput),
|
|
PublicKeySignatureBls48581: &protobufs.BLS48581AggregateSignature{
|
|
Bitmask: []byte{0b11100011}, // Signers 0,1,5,6,7
|
|
},
|
|
},
|
|
}
|
|
|
|
err = atr.Insert(frameA)
|
|
require.NoError(t, err)
|
|
prevOutput = frameA.Header.Output
|
|
}
|
|
|
|
// Verify chain A is the head
|
|
head, err := atr.GetHead()
|
|
require.NoError(t, err)
|
|
assert.Equal(t, uint64(200), head.Header.FrameNumber)
|
|
assert.Equal(t, []byte("frameA_200"), head.Header.Output)
|
|
|
|
// Now build chain B: 200 frames with bitmask 0b11111100 (signers 2,3,4,5,6,7)
|
|
// This overlaps with chain A on signers 5,6,7 (0b11100000)
|
|
prevOutput = genesis.Header.Output
|
|
|
|
for i := 1; i <= 200; i++ {
|
|
frameB := &protobufs.GlobalFrame{
|
|
Header: &protobufs.GlobalFrameHeader{
|
|
FrameNumber: uint64(i),
|
|
Timestamp: int64(1000 + i*1000), // Same timestamps
|
|
Difficulty: uint32(100 + i), // Same difficulty
|
|
Output: []byte(fmt.Sprintf("frameB_%d", i)),
|
|
ParentSelector: computeGlobalPoseidonHash(prevOutput),
|
|
PublicKeySignatureBls48581: &protobufs.BLS48581AggregateSignature{
|
|
Bitmask: []byte{0b11111100}, // Signers 2,3,4,5,6,7
|
|
},
|
|
},
|
|
}
|
|
|
|
err = atr.Insert(frameB)
|
|
// Should now succeed even with equivocation
|
|
assert.NoError(t, err, "Should accept frame despite equivocation at frame %d", i)
|
|
prevOutput = frameB.Header.Output
|
|
}
|
|
|
|
// Wait for events to be processed
|
|
time.Sleep(500 * time.Millisecond)
|
|
close(stopCollecting)
|
|
|
|
// Check equivocation events
|
|
eventsMu.Lock()
|
|
t.Logf("Received %d equivocation events", len(equivocationEvents))
|
|
assert.Equal(t, 200, len(equivocationEvents), "should have 200 equivocation events")
|
|
eventsMu.Unlock()
|
|
|
|
// Fork choice should favor chain B
|
|
// Chain A has 5 signers (0,1,5,6,7) but 5,6,7 equivocated = 2 valid signers
|
|
// Chain B has 6 signers (2,3,4,5,6,7) but 5,6,7 equivocated = 3 valid signers
|
|
// So chain B should win
|
|
head, err = atr.GetHead()
|
|
require.NoError(t, err)
|
|
assert.Equal(t, uint64(200), head.Header.FrameNumber)
|
|
// Check it's from chain B (output contains "frameB")
|
|
assert.Contains(t, string(head.Header.Output), "frameB", "Head should be from chain B")
|
|
|
|
// Check tree info
|
|
info := atr.GetTreeInfo()
|
|
t.Logf("Tree info after equivocation: %+v", info)
|
|
}
|
|
|
|
// TestGlobalTimeReel_EquivocationWithForkChoice tests fork choice when equivocating signers are excluded
|
|
func TestGlobalTimeReel_EquivocationWithForkChoice(t *testing.T) {
|
|
logger, _ := zap.NewDevelopment()
|
|
s := setupTestClockStore(t)
|
|
atr, err := NewGlobalTimeReel(logger, createTestProverRegistry(true), s, 99, true)
|
|
require.NoError(t, err)
|
|
|
|
ctx, cancel, _ := lifecycle.WithSignallerAndCancel(context.Background())
|
|
go atr.Start(ctx, func() {})
|
|
time.Sleep(100 * time.Millisecond)
|
|
defer cancel()
|
|
eventCh := atr.GetEventCh()
|
|
|
|
// Drain initial events
|
|
select {
|
|
case <-eventCh:
|
|
case <-time.After(10 * time.Millisecond):
|
|
}
|
|
|
|
// Insert genesis
|
|
genesis := &protobufs.GlobalFrame{
|
|
Header: &protobufs.GlobalFrameHeader{
|
|
FrameNumber: 0,
|
|
Timestamp: 1000,
|
|
Difficulty: 100,
|
|
Output: []byte("genesis"),
|
|
ParentSelector: []byte{},
|
|
},
|
|
}
|
|
|
|
err = atr.Insert(genesis)
|
|
require.NoError(t, err)
|
|
|
|
// Drain genesis event
|
|
select {
|
|
case <-eventCh:
|
|
case <-time.After(50 * time.Millisecond):
|
|
}
|
|
|
|
// Create frame 1A with signers 0,1,2,3 (4 signers)
|
|
frame1A := &protobufs.GlobalFrame{
|
|
Header: &protobufs.GlobalFrameHeader{
|
|
FrameNumber: 1,
|
|
Timestamp: 2000,
|
|
Difficulty: 110,
|
|
Output: []byte("frame1A"),
|
|
ParentSelector: computeGlobalPoseidonHash(genesis.Header.Output),
|
|
PublicKeySignatureBls48581: &protobufs.BLS48581AggregateSignature{
|
|
Bitmask: []byte{0b00001111}, // Signers 0,1,2,3
|
|
},
|
|
},
|
|
}
|
|
|
|
err = atr.Insert(frame1A)
|
|
require.NoError(t, err)
|
|
|
|
// Drain new head event
|
|
select {
|
|
case <-eventCh:
|
|
case <-time.After(50 * time.Millisecond):
|
|
}
|
|
|
|
// Create frame 1B with signers 2,3,4,5,6,7 (6 signers, 2 overlap with 1A)
|
|
frame1B := &protobufs.GlobalFrame{
|
|
Header: &protobufs.GlobalFrameHeader{
|
|
FrameNumber: 1,
|
|
Timestamp: 2000,
|
|
Difficulty: 110,
|
|
Output: []byte("frame1B"),
|
|
ParentSelector: computeGlobalPoseidonHash(genesis.Header.Output),
|
|
PublicKeySignatureBls48581: &protobufs.BLS48581AggregateSignature{
|
|
Bitmask: []byte{0b11111100}, // Signers 2,3,4,5,6,7
|
|
},
|
|
},
|
|
}
|
|
|
|
// This should succeed now, but generate an equivocation event
|
|
err = atr.Insert(frame1B)
|
|
assert.NoError(t, err)
|
|
|
|
// Wait for equivocation event
|
|
select {
|
|
case event := <-eventCh:
|
|
assert.Equal(t, TimeReelEventEquivocationDetected, event.Type)
|
|
assert.Contains(t, event.Message, "equivocation at frame 1")
|
|
case <-time.After(100 * time.Millisecond):
|
|
t.Fatal("timeout waiting for equivocation event")
|
|
}
|
|
|
|
// Fork choice should favor frame 1B
|
|
// Frame 1A: 4 signers (0,1,2,3) but 2,3 equivocated = 2 valid signers
|
|
// Frame 1B: 6 signers (2,3,4,5,6,7) but 2,3 equivocated = 4 valid signers
|
|
// So frame 1B should win
|
|
head, err := atr.GetHead()
|
|
require.NoError(t, err)
|
|
assert.Equal(t, uint64(1), head.Header.FrameNumber)
|
|
assert.Equal(t, []byte("frame1B"), head.Header.Output, "Head should be frame 1B with more non-equivocating signers")
|
|
}
|
|
|
|
// TestGlobalTimeReel_NonOverlappingForks tests that non-overlapping forks work correctly
|
|
func TestGlobalTimeReel_NonOverlappingForks(t *testing.T) {
|
|
logger, _ := zap.NewDevelopment()
|
|
s := setupTestClockStore(t)
|
|
atr, err := NewGlobalTimeReel(logger, createTestProverRegistry(true), s, 99, true)
|
|
require.NoError(t, err)
|
|
|
|
ctx, cancel, _ := lifecycle.WithSignallerAndCancel(context.Background())
|
|
go atr.Start(ctx, func() {})
|
|
time.Sleep(100 * time.Millisecond)
|
|
defer cancel()
|
|
|
|
// Insert genesis
|
|
genesis := &protobufs.GlobalFrame{
|
|
Header: &protobufs.GlobalFrameHeader{
|
|
FrameNumber: 0,
|
|
Timestamp: 1000,
|
|
Difficulty: 100,
|
|
Output: []byte("genesis"),
|
|
ParentSelector: []byte{},
|
|
},
|
|
}
|
|
|
|
err = atr.Insert(genesis)
|
|
require.NoError(t, err)
|
|
|
|
// Build two non-overlapping chains
|
|
// Chain A: signers 0,1,2,3 (lower half)
|
|
// Chain B: signers 4,5,6,7 (upper half)
|
|
|
|
// Chain A
|
|
prevOutputA := genesis.Header.Output
|
|
for i := 1; i <= 10; i++ {
|
|
frameA := &protobufs.GlobalFrame{
|
|
Header: &protobufs.GlobalFrameHeader{
|
|
FrameNumber: uint64(i),
|
|
Timestamp: int64(1000 + i*1000),
|
|
Difficulty: uint32(100 + i),
|
|
Output: []byte(fmt.Sprintf("frameA_%d", i)),
|
|
ParentSelector: computeGlobalPoseidonHash(prevOutputA),
|
|
PublicKeySignatureBls48581: &protobufs.BLS48581AggregateSignature{
|
|
Bitmask: []byte{0b00001111}, // Signers 0,1,2,3
|
|
},
|
|
},
|
|
}
|
|
err = atr.Insert(frameA)
|
|
require.NoError(t, err)
|
|
prevOutputA = frameA.Header.Output
|
|
}
|
|
|
|
// Chain B - should be allowed as there's no overlap
|
|
prevOutputB := genesis.Header.Output
|
|
for i := 1; i <= 10; i++ {
|
|
frameB := &protobufs.GlobalFrame{
|
|
Header: &protobufs.GlobalFrameHeader{
|
|
FrameNumber: uint64(i),
|
|
Timestamp: int64(1000 + i*1000),
|
|
Difficulty: uint32(100 + i),
|
|
Output: []byte(fmt.Sprintf("frameB_%d", i)),
|
|
ParentSelector: computeGlobalPoseidonHash(prevOutputB),
|
|
PublicKeySignatureBls48581: &protobufs.BLS48581AggregateSignature{
|
|
Bitmask: []byte{0b11110000}, // Signers 4,5,6,7
|
|
},
|
|
},
|
|
}
|
|
err = atr.Insert(frameB)
|
|
require.NoError(t, err, "non-overlapping fork should be allowed")
|
|
prevOutputB = frameB.Header.Output
|
|
}
|
|
|
|
// Both chains should exist
|
|
framesAt10, err := atr.GetFramesByNumber(10)
|
|
require.NoError(t, err)
|
|
assert.Equal(t, 2, len(framesAt10), "should have 2 competing frames at height 10")
|
|
|
|
// Head should be one of them (both have same number of signers)
|
|
head, err := atr.GetHead()
|
|
require.NoError(t, err)
|
|
assert.Equal(t, uint64(10), head.Header.FrameNumber)
|
|
}
|