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>
2413 lines
74 KiB
Go
2413 lines
74 KiB
Go
package time
|
|
|
|
import (
|
|
"context"
|
|
"fmt"
|
|
"sync"
|
|
"testing"
|
|
"time"
|
|
|
|
"github.com/pkg/errors"
|
|
"github.com/stretchr/testify/assert"
|
|
"github.com/stretchr/testify/mock"
|
|
"github.com/stretchr/testify/require"
|
|
"go.uber.org/zap"
|
|
"source.quilibrium.com/quilibrium/monorepo/lifecycle"
|
|
"source.quilibrium.com/quilibrium/monorepo/protobufs"
|
|
"source.quilibrium.com/quilibrium/monorepo/types/mocks"
|
|
)
|
|
|
|
// createTestProverRegistry creates a mock ProverRegistry that returns expected provers
|
|
func createTestProverRegistry(defaultReturn bool) *mocks.MockProverRegistry {
|
|
proverRegistry := new(mocks.MockProverRegistry)
|
|
if defaultReturn {
|
|
// By default, allow any prover (return the same prover that was in the frame)
|
|
proverRegistry.On("GetNextProver", mock.Anything, mock.Anything).Return(nil, errors.New("no specific prover required"))
|
|
proverRegistry.On("GetOrderedProvers", mock.Anything, mock.Anything).Return(nil, errors.New("no ordered provers"))
|
|
}
|
|
return proverRegistry
|
|
}
|
|
|
|
// createStrictProverRegistry creates a mock ProverRegistry that expects specific provers
|
|
func createStrictProverRegistry(expectedProvers map[string][]byte) *mocks.MockProverRegistry {
|
|
proverRegistry := new(mocks.MockProverRegistry)
|
|
// Set up expectations for specific parent selectors
|
|
for parentSelectorStr, expectedProver := range expectedProvers {
|
|
var parentSelector [32]byte
|
|
copy(parentSelector[:], []byte(parentSelectorStr))
|
|
proverRegistry.On("GetNextProver", parentSelector, mock.Anything).Return(expectedProver, nil)
|
|
// Also set up GetOrderedProvers to return the expected prover as the first in the list
|
|
proverRegistry.On("GetOrderedProvers", parentSelector, mock.Anything).Return([][]byte{expectedProver}, nil)
|
|
}
|
|
// Default behavior for unknown parent selectors
|
|
proverRegistry.On("GetNextProver", mock.Anything, mock.Anything).Return(nil, errors.New("unknown parent selector"))
|
|
proverRegistry.On("GetOrderedProvers", mock.Anything, mock.Anything).Return(nil, errors.New("unknown parent selector"))
|
|
return proverRegistry
|
|
}
|
|
|
|
func TestAppTimeReel_BasicOperations(t *testing.T) {
|
|
logger, _ := zap.NewDevelopment()
|
|
address := []byte("test_app_address")
|
|
s := setupTestClockStore(t)
|
|
atr, err := NewAppTimeReel(logger, address, createTestProverRegistry(true), s, true)
|
|
require.NoError(t, err)
|
|
|
|
ctx, cancel, _ := lifecycle.WithSignallerAndCancel(context.Background())
|
|
go atr.Start(ctx, func() {})
|
|
time.Sleep(100 * time.Millisecond)
|
|
defer cancel()
|
|
// Test address getter
|
|
assert.Equal(t, address, atr.GetAddress())
|
|
|
|
// Test inserting genesis frame
|
|
genesis := &protobufs.AppShardFrame{
|
|
Header: &protobufs.FrameHeader{
|
|
Address: address,
|
|
FrameNumber: 0,
|
|
Timestamp: 1000,
|
|
Difficulty: 100,
|
|
Output: []byte("app_genesis_output"),
|
|
ParentSelector: []byte{}, // Empty for genesis
|
|
Prover: []byte("prover1"),
|
|
},
|
|
}
|
|
|
|
err = atr.Insert(genesis)
|
|
assert.NoError(t, err)
|
|
|
|
// Check that genesis became head
|
|
head, err := atr.GetHead()
|
|
assert.NoError(t, err)
|
|
assert.Equal(t, uint64(0), head.Header.FrameNumber)
|
|
|
|
// Test inserting next frame
|
|
frame1 := &protobufs.AppShardFrame{
|
|
Header: &protobufs.FrameHeader{
|
|
Address: address,
|
|
FrameNumber: 1,
|
|
Timestamp: 2000,
|
|
Difficulty: 110,
|
|
Output: []byte("app_frame1_output"),
|
|
ParentSelector: computeAppPoseidonHash(genesis.Header.Output),
|
|
Prover: []byte("prover2"),
|
|
},
|
|
}
|
|
|
|
err = atr.Insert(frame1)
|
|
assert.NoError(t, err)
|
|
|
|
// Check new head
|
|
head, err = atr.GetHead()
|
|
assert.NoError(t, err)
|
|
assert.Equal(t, uint64(1), head.Header.FrameNumber)
|
|
|
|
// Test retrieving frames by frame number
|
|
framesAtZero, err := atr.GetFramesByNumber(0)
|
|
assert.NoError(t, err)
|
|
assert.Len(t, framesAtZero, 1)
|
|
assert.Equal(t, genesis.Header.FrameNumber, framesAtZero[0].Header.FrameNumber)
|
|
|
|
framesAtOne, err := atr.GetFramesByNumber(1)
|
|
assert.NoError(t, err)
|
|
assert.Len(t, framesAtOne, 1)
|
|
assert.Equal(t, frame1.Header.FrameNumber, framesAtOne[0].Header.FrameNumber)
|
|
|
|
// Test lineage (head lineage)
|
|
lineage, err := atr.GetLineage()
|
|
assert.NoError(t, err)
|
|
assert.Len(t, lineage, 2)
|
|
assert.Equal(t, uint64(0), lineage[0].Header.FrameNumber)
|
|
assert.Equal(t, uint64(1), lineage[1].Header.FrameNumber)
|
|
|
|
// Test child frames - we need to get the frameID of genesis first
|
|
genesisID := atr.ComputeFrameID(genesis)
|
|
children, err := atr.GetChildFrames(genesisID)
|
|
assert.NoError(t, err)
|
|
assert.Len(t, children, 1)
|
|
assert.Equal(t, uint64(1), children[0].Header.FrameNumber)
|
|
}
|
|
|
|
func TestAppTimeReel_WrongAddress(t *testing.T) {
|
|
logger, _ := zap.NewDevelopment()
|
|
address := []byte("correct_address")
|
|
s := setupTestClockStore(t)
|
|
atr, err := NewAppTimeReel(logger, address, createTestProverRegistry(true), s, true)
|
|
require.NoError(t, err)
|
|
|
|
ctx, cancel, _ := lifecycle.WithSignallerAndCancel(context.Background())
|
|
go atr.Start(ctx, func() {})
|
|
time.Sleep(100 * time.Millisecond)
|
|
defer cancel()
|
|
|
|
// Try to insert frame with wrong address
|
|
wrongFrame := &protobufs.AppShardFrame{
|
|
Header: &protobufs.FrameHeader{
|
|
Address: []byte("wrong_address"),
|
|
FrameNumber: 0,
|
|
Timestamp: 1000,
|
|
Difficulty: 100,
|
|
Output: []byte("output"),
|
|
ParentSelector: []byte{},
|
|
Prover: []byte("prover"),
|
|
},
|
|
}
|
|
|
|
err = atr.Insert(wrongFrame)
|
|
assert.Error(t, err)
|
|
assert.Contains(t, err.Error(), "frame address does not match reel address")
|
|
}
|
|
|
|
func TestAppTimeReel_Equivocation(t *testing.T) {
|
|
logger, _ := zap.NewDevelopment()
|
|
address := []byte("test_app_address")
|
|
s := setupTestClockStore(t)
|
|
atr, err := NewAppTimeReel(logger, address, createTestProverRegistry(true), s, true)
|
|
require.NoError(t, err)
|
|
|
|
ctx, cancel, _ := lifecycle.WithSignallerAndCancel(context.Background())
|
|
go atr.Start(ctx, func() {})
|
|
time.Sleep(100 * time.Millisecond)
|
|
defer cancel()
|
|
|
|
// Subscribe to events
|
|
eventCh := atr.GetEventCh()
|
|
|
|
// Drain any existing events
|
|
select {
|
|
case <-eventCh:
|
|
default:
|
|
}
|
|
|
|
// Insert genesis
|
|
genesis := &protobufs.AppShardFrame{
|
|
Header: &protobufs.FrameHeader{
|
|
Address: address,
|
|
FrameNumber: 0,
|
|
Timestamp: 1000,
|
|
Difficulty: 100,
|
|
Output: []byte("genesis_output"),
|
|
ParentSelector: []byte{},
|
|
Prover: []byte("prover1"),
|
|
},
|
|
}
|
|
|
|
err = atr.Insert(genesis)
|
|
assert.NoError(t, err)
|
|
|
|
// Drain any events
|
|
select {
|
|
case <-eventCh:
|
|
case <-time.After(50 * time.Millisecond):
|
|
}
|
|
|
|
// Insert valid frame 1 with BLS signature
|
|
frame1 := &protobufs.AppShardFrame{
|
|
Header: &protobufs.FrameHeader{
|
|
Address: address,
|
|
FrameNumber: 1,
|
|
Timestamp: 2000,
|
|
Difficulty: 110,
|
|
Output: []byte("frame1_output"),
|
|
ParentSelector: computeAppPoseidonHash(genesis.Header.Output),
|
|
Prover: []byte("prover2"),
|
|
PublicKeySignatureBls48581: &protobufs.BLS48581AggregateSignature{
|
|
Bitmask: []byte{0b00001111}, // First 4 signers
|
|
},
|
|
},
|
|
}
|
|
|
|
err = atr.Insert(frame1)
|
|
assert.NoError(t, err)
|
|
|
|
// Drain any events
|
|
select {
|
|
case <-eventCh:
|
|
case <-time.After(50 * time.Millisecond):
|
|
}
|
|
|
|
// Try to insert equivocating frame 1 with overlapping bitmask
|
|
frame1Equivocation := &protobufs.AppShardFrame{
|
|
Header: &protobufs.FrameHeader{
|
|
Address: address,
|
|
FrameNumber: 1,
|
|
Timestamp: 2000,
|
|
Difficulty: 110,
|
|
Output: []byte("different_output"), // Different output
|
|
ParentSelector: computeAppPoseidonHash(genesis.Header.Output),
|
|
Prover: []byte("prover3"), // Different prover
|
|
PublicKeySignatureBls48581: &protobufs.BLS48581AggregateSignature{
|
|
Bitmask: []byte{0b00000011}, // First 2 signers - overlaps with frame1
|
|
},
|
|
},
|
|
}
|
|
|
|
err = atr.Insert(frame1Equivocation)
|
|
assert.NoError(t, err)
|
|
|
|
// Give the goroutine time to send the event
|
|
time.Sleep(50 * time.Millisecond)
|
|
|
|
// Check for equivocation event
|
|
select {
|
|
case event := <-eventCh:
|
|
t.Logf("Received event: Type=%d, Message=%s", event.Type, event.Message)
|
|
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")
|
|
}
|
|
}
|
|
|
|
func TestAppTimeReel_Fork(t *testing.T) {
|
|
logger, _ := zap.NewDevelopment()
|
|
address := []byte("test_app_address")
|
|
s := setupTestClockStore(t)
|
|
atr, err := NewAppTimeReel(logger, address, createTestProverRegistry(true), s, 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.AppShardFrame{
|
|
Header: &protobufs.FrameHeader{
|
|
Address: address,
|
|
FrameNumber: 0,
|
|
Timestamp: 1000,
|
|
Difficulty: 100,
|
|
Output: []byte("genesis_output"),
|
|
ParentSelector: []byte{},
|
|
Prover: []byte("prover1"),
|
|
},
|
|
}
|
|
|
|
err = atr.Insert(genesis)
|
|
assert.NoError(t, err)
|
|
|
|
// Insert valid frame 1 with BLS signature
|
|
frame1 := &protobufs.AppShardFrame{
|
|
Header: &protobufs.FrameHeader{
|
|
Address: address,
|
|
FrameNumber: 1,
|
|
Timestamp: 2000,
|
|
Difficulty: 110,
|
|
Output: []byte("frame1_output"),
|
|
ParentSelector: computeAppPoseidonHash(genesis.Header.Output),
|
|
Prover: []byte("prover2"),
|
|
PublicKeySignatureBls48581: &protobufs.BLS48581AggregateSignature{
|
|
Bitmask: []byte{0b00001111}, // First 4 signers
|
|
},
|
|
},
|
|
}
|
|
|
|
err = atr.Insert(frame1)
|
|
assert.NoError(t, err)
|
|
|
|
// Try to insert forking frame 1 with non-overlapping bitmask (different signers)
|
|
frame1Fork := &protobufs.AppShardFrame{
|
|
Header: &protobufs.FrameHeader{
|
|
Address: address,
|
|
FrameNumber: 1,
|
|
Timestamp: 2000,
|
|
Difficulty: 110,
|
|
Output: []byte("fork_output"), // Different output
|
|
ParentSelector: computeAppPoseidonHash(genesis.Header.Output),
|
|
Prover: []byte("prover3"), // Different prover
|
|
PublicKeySignatureBls48581: &protobufs.BLS48581AggregateSignature{
|
|
Bitmask: []byte{0b11110000}, // Different signers - no overlap
|
|
},
|
|
},
|
|
}
|
|
|
|
// This should succeed - it's a fork, not equivocation
|
|
err = atr.Insert(frame1Fork)
|
|
assert.NoError(t, err)
|
|
|
|
// Head should still be the original frame1
|
|
head, err := atr.GetHead()
|
|
require.NoError(t, err)
|
|
assert.Equal(t, frame1.Header.Output, head.Header.Output)
|
|
}
|
|
|
|
func TestAppTimeReel_ParentValidation(t *testing.T) {
|
|
logger, _ := zap.NewDevelopment()
|
|
address := []byte("test_app_address")
|
|
s := setupTestClockStore(t)
|
|
atr, err := NewAppTimeReel(logger, address, createTestProverRegistry(true), s, 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.AppShardFrame{
|
|
Header: &protobufs.FrameHeader{
|
|
Address: address,
|
|
FrameNumber: 0,
|
|
Timestamp: 1000,
|
|
Difficulty: 100,
|
|
Output: []byte("genesis_output"),
|
|
ParentSelector: []byte{},
|
|
Prover: []byte("prover1"),
|
|
},
|
|
}
|
|
|
|
err = atr.Insert(genesis)
|
|
assert.NoError(t, err)
|
|
|
|
// Insert valid frame 1
|
|
frame1 := &protobufs.AppShardFrame{
|
|
Header: &protobufs.FrameHeader{
|
|
Address: address,
|
|
FrameNumber: 1,
|
|
Timestamp: 2000,
|
|
Difficulty: 110,
|
|
Output: []byte("frame1_output"),
|
|
ParentSelector: computeAppPoseidonHash(genesis.Header.Output),
|
|
Prover: []byte("prover2"),
|
|
},
|
|
}
|
|
|
|
err = atr.Insert(frame1)
|
|
assert.NoError(t, err)
|
|
|
|
// Try to insert frame with a completely invalid parent selector that doesn't match any existing frame
|
|
badFrame := &protobufs.AppShardFrame{
|
|
Header: &protobufs.FrameHeader{
|
|
Address: address,
|
|
FrameNumber: 2,
|
|
Timestamp: 3000,
|
|
Difficulty: 120,
|
|
Output: []byte("bad_frame"),
|
|
ParentSelector: []byte("completely_invalid_parent_selector_that_matches_no_frame"),
|
|
Prover: []byte("prover3"),
|
|
},
|
|
}
|
|
|
|
// This should succeed (goes to pending since parent not found)
|
|
err = atr.Insert(badFrame)
|
|
assert.NoError(t, err)
|
|
|
|
// Check that it's in pending frames
|
|
pending := atr.GetPendingFrames()
|
|
assert.True(t, len(pending) > 0, "Frame should be in pending queue")
|
|
}
|
|
|
|
func TestAppTimeReel_ForkDetection(t *testing.T) {
|
|
logger, _ := zap.NewDevelopment()
|
|
address := []byte("test_app_address")
|
|
s := setupTestClockStore(t)
|
|
atr, err := NewAppTimeReel(logger, address, createTestProverRegistry(true), s, 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
|
|
events := make([]AppEvent, 0)
|
|
go func() {
|
|
for event := range eventCh {
|
|
eventsMu.Lock()
|
|
events = append(events, event)
|
|
eventsMu.Unlock()
|
|
}
|
|
}()
|
|
|
|
// Build a chain: 0 -> 1 -> 2
|
|
frames := []*protobufs.AppShardFrame{
|
|
{
|
|
Header: &protobufs.FrameHeader{
|
|
Address: address,
|
|
FrameNumber: 0,
|
|
Timestamp: 1000,
|
|
Difficulty: 100,
|
|
Output: []byte("genesis"),
|
|
ParentSelector: []byte{},
|
|
Prover: []byte("prover1"),
|
|
},
|
|
},
|
|
{Header: &protobufs.FrameHeader{
|
|
Address: address,
|
|
FrameNumber: 1,
|
|
Timestamp: 2000,
|
|
Difficulty: 110,
|
|
Output: []byte("frame1"),
|
|
ParentSelector: nil,
|
|
Prover: []byte("prover2"),
|
|
},
|
|
},
|
|
{Header: &protobufs.FrameHeader{
|
|
Address: address,
|
|
FrameNumber: 2,
|
|
Timestamp: 3000,
|
|
Difficulty: 120,
|
|
Output: []byte("frame2"),
|
|
ParentSelector: nil,
|
|
Prover: []byte("prover3"),
|
|
},
|
|
},
|
|
}
|
|
|
|
// Set parent selectors
|
|
frames[1].Header.ParentSelector = computeAppPoseidonHash(frames[0].Header.Output)
|
|
frames[2].Header.ParentSelector = computeAppPoseidonHash(frames[1].Header.Output)
|
|
|
|
// Insert chain
|
|
for _, frame := range frames {
|
|
err := atr.Insert(frame)
|
|
require.NoError(t, err)
|
|
}
|
|
|
|
// Verify head is frame 2
|
|
head, err := atr.GetHead()
|
|
require.NoError(t, err)
|
|
assert.Equal(t, uint64(2), head.Header.FrameNumber)
|
|
|
|
// Should have received new head events
|
|
assert.Eventually(t, func() bool {
|
|
eventsMu.Lock()
|
|
defer eventsMu.Unlock()
|
|
return len(events) >= 3 // One for each frame becoming head
|
|
}, time.Second, 10*time.Millisecond, "should have received new head events")
|
|
}
|
|
|
|
func TestAppTimeReel_ForkChoice_MoreSignatures(t *testing.T) {
|
|
logger, _ := zap.NewDevelopment()
|
|
address := []byte("test_app_address")
|
|
s := setupTestClockStore(t)
|
|
atr, err := NewAppTimeReel(logger, address, createTestProverRegistry(true), s, 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 any existing events
|
|
select {
|
|
case <-eventCh:
|
|
case <-time.After(50 * time.Millisecond):
|
|
}
|
|
|
|
// Insert genesis
|
|
genesis := &protobufs.AppShardFrame{
|
|
Header: &protobufs.FrameHeader{
|
|
Address: address,
|
|
FrameNumber: 0,
|
|
Timestamp: 1000,
|
|
Difficulty: 100,
|
|
Output: []byte("genesis_output"),
|
|
ParentSelector: []byte{},
|
|
Prover: []byte("prover1"),
|
|
},
|
|
}
|
|
|
|
err = atr.Insert(genesis)
|
|
require.NoError(t, err)
|
|
|
|
// Drain genesis event
|
|
select {
|
|
case <-eventCh:
|
|
case <-time.After(50 * time.Millisecond):
|
|
}
|
|
|
|
// Insert frame 1 with 2 signatures
|
|
frame1Weak := &protobufs.AppShardFrame{
|
|
Header: &protobufs.FrameHeader{
|
|
Address: address,
|
|
FrameNumber: 1,
|
|
Timestamp: 2000,
|
|
Difficulty: 110,
|
|
Output: []byte("weak_frame"),
|
|
ParentSelector: computeAppPoseidonHash(genesis.Header.Output),
|
|
Prover: []byte("prover2"),
|
|
PublicKeySignatureBls48581: &protobufs.BLS48581AggregateSignature{
|
|
Bitmask: []byte{0b11000000}, // 2 signers
|
|
},
|
|
},
|
|
}
|
|
|
|
err = atr.Insert(frame1Weak)
|
|
require.NoError(t, err)
|
|
|
|
// Verify weak frame is initially head
|
|
head, err := atr.GetHead()
|
|
require.NoError(t, err)
|
|
assert.Equal(t, uint64(1), head.Header.FrameNumber)
|
|
assert.Equal(t, []byte("weak_frame"), head.Header.Output)
|
|
|
|
// Drain frame1 event
|
|
select {
|
|
case <-eventCh:
|
|
case <-time.After(50 * time.Millisecond):
|
|
}
|
|
|
|
// Insert competing frame 1 with 4 signatures (should replace weak frame)
|
|
frame1Strong := &protobufs.AppShardFrame{
|
|
Header: &protobufs.FrameHeader{
|
|
Address: address,
|
|
FrameNumber: 1,
|
|
Timestamp: 2100, // Later timestamp
|
|
Difficulty: 110,
|
|
Output: []byte("strong_frame"),
|
|
ParentSelector: computeAppPoseidonHash(genesis.Header.Output),
|
|
Prover: []byte("prover3"),
|
|
PublicKeySignatureBls48581: &protobufs.BLS48581AggregateSignature{
|
|
Bitmask: []byte{0b00001111}, // 4 signers (more than weak frame)
|
|
},
|
|
},
|
|
}
|
|
|
|
err = atr.Insert(frame1Strong)
|
|
require.NoError(t, err)
|
|
|
|
// Give the goroutine time to send the event
|
|
time.Sleep(50 * time.Millisecond)
|
|
|
|
// Verify strong frame is now head
|
|
head, err = atr.GetHead()
|
|
require.NoError(t, err)
|
|
assert.Equal(t, uint64(1), head.Header.FrameNumber)
|
|
assert.Equal(t, []byte("strong_frame"), head.Header.Output, "should choose frame with more signatures")
|
|
|
|
// Check for reorganization event
|
|
select {
|
|
case event := <-eventCh:
|
|
assert.Equal(t, TimeReelEventForkDetected, event.Type)
|
|
assert.Contains(t, event.Message, "fork detected")
|
|
case <-time.After(100 * time.Millisecond):
|
|
t.Fatal("timeout waiting for reorganization event")
|
|
}
|
|
}
|
|
|
|
func TestAppTimeReel_ForkChoice_NoReplacement(t *testing.T) {
|
|
logger, _ := zap.NewDevelopment()
|
|
address := []byte("test_app_address")
|
|
s := setupTestClockStore(t)
|
|
atr, err := NewAppTimeReel(logger, address, createTestProverRegistry(true), s, 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 any existing events
|
|
select {
|
|
case <-eventCh:
|
|
case <-time.After(50 * time.Millisecond):
|
|
}
|
|
|
|
// Insert genesis
|
|
genesis := &protobufs.AppShardFrame{
|
|
Header: &protobufs.FrameHeader{
|
|
Address: address,
|
|
FrameNumber: 0,
|
|
Timestamp: 1000,
|
|
Difficulty: 100,
|
|
Output: []byte("genesis_output"),
|
|
ParentSelector: []byte{},
|
|
Prover: []byte("prover1"),
|
|
},
|
|
}
|
|
|
|
err = atr.Insert(genesis)
|
|
require.NoError(t, err)
|
|
|
|
// Drain genesis event
|
|
select {
|
|
case <-eventCh:
|
|
case <-time.After(50 * time.Millisecond):
|
|
}
|
|
|
|
// Insert frame 1 with more signatures and earlier timestamp
|
|
frame1Strong := &protobufs.AppShardFrame{
|
|
Header: &protobufs.FrameHeader{
|
|
Address: address,
|
|
FrameNumber: 1,
|
|
Timestamp: 2000, // Earlier timestamp
|
|
Difficulty: 110,
|
|
Output: []byte("strong_frame"),
|
|
ParentSelector: computeAppPoseidonHash(genesis.Header.Output),
|
|
Prover: []byte("prover2"),
|
|
PublicKeySignatureBls48581: &protobufs.BLS48581AggregateSignature{
|
|
Bitmask: []byte{0b00001111}, // 4 signers
|
|
},
|
|
},
|
|
}
|
|
|
|
err = atr.Insert(frame1Strong)
|
|
require.NoError(t, err)
|
|
|
|
// Verify strong frame is head
|
|
head, err := atr.GetHead()
|
|
require.NoError(t, err)
|
|
assert.Equal(t, uint64(1), head.Header.FrameNumber)
|
|
assert.Equal(t, []byte("strong_frame"), head.Header.Output)
|
|
|
|
// Drain frame1 event
|
|
select {
|
|
case <-eventCh:
|
|
case <-time.After(50 * time.Millisecond):
|
|
}
|
|
|
|
// Insert competing frame 1 with fewer signatures (should NOT replace)
|
|
frame1Weak := &protobufs.AppShardFrame{
|
|
Header: &protobufs.FrameHeader{
|
|
Address: address,
|
|
FrameNumber: 1,
|
|
Timestamp: 1900, // Even earlier timestamp but fewer signatures
|
|
Difficulty: 110,
|
|
Output: []byte("weak_frame"),
|
|
ParentSelector: computeAppPoseidonHash(genesis.Header.Output),
|
|
Prover: []byte("prover3"),
|
|
PublicKeySignatureBls48581: &protobufs.BLS48581AggregateSignature{
|
|
Bitmask: []byte{0b00110000}, // 2 signers (fewer than strong frame)
|
|
},
|
|
},
|
|
}
|
|
|
|
err = atr.Insert(frame1Weak)
|
|
require.NoError(t, err)
|
|
|
|
// Give some time for any potential events
|
|
time.Sleep(50 * time.Millisecond)
|
|
|
|
// Verify strong frame is still head (not replaced)
|
|
head, err = atr.GetHead()
|
|
require.NoError(t, err)
|
|
assert.Equal(t, uint64(1), head.Header.FrameNumber)
|
|
assert.Equal(t, []byte("strong_frame"), head.Header.Output, "should not replace frame with more signatures")
|
|
|
|
// Should not receive reorganization event since no replacement occurred
|
|
select {
|
|
case event := <-eventCh:
|
|
t.Fatalf("unexpected event received: %+v", event)
|
|
case <-time.After(50 * time.Millisecond):
|
|
// Expected - no event should be received
|
|
}
|
|
}
|
|
|
|
func TestAppTimeReel_DeepForkChoice_ReverseInsertion(t *testing.T) {
|
|
logger, _ := zap.NewDevelopment()
|
|
address := []byte("test_app_address")
|
|
reg := createTestProverRegistry(false)
|
|
s := setupTestClockStore(t)
|
|
atr, err := NewAppTimeReel(logger, address, reg, s, 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 any existing events
|
|
select {
|
|
case <-eventCh:
|
|
case <-time.After(50 * time.Millisecond):
|
|
}
|
|
|
|
// Insert genesis
|
|
genesis := &protobufs.AppShardFrame{
|
|
Header: &protobufs.FrameHeader{
|
|
Address: address,
|
|
FrameNumber: 0,
|
|
Timestamp: 1000,
|
|
Difficulty: 100,
|
|
Output: []byte("genesis_output"),
|
|
ParentSelector: []byte{},
|
|
Prover: []byte("prover1"),
|
|
},
|
|
}
|
|
|
|
reg.On("GetNextProver", [32]byte{}, mock.Anything).Return([]byte("prover1"), nil)
|
|
reg.On("GetOrderedProvers", [32]byte{}, mock.Anything).Return([][]byte{
|
|
[]byte("prover1"),
|
|
[]byte("prover2"),
|
|
[]byte("prover3"),
|
|
[]byte("prover4"),
|
|
[]byte("prover5"),
|
|
[]byte("prover6"),
|
|
[]byte("prover7"),
|
|
[]byte("prover8"),
|
|
}, nil)
|
|
|
|
err = atr.Insert(genesis)
|
|
require.NoError(t, err)
|
|
|
|
// Drain genesis event
|
|
select {
|
|
case <-eventCh:
|
|
case <-time.After(50 * time.Millisecond):
|
|
}
|
|
|
|
// Insert frame 1 (shared by both chains initially)
|
|
frame1 := &protobufs.AppShardFrame{
|
|
Header: &protobufs.FrameHeader{
|
|
Address: address,
|
|
FrameNumber: 1,
|
|
Timestamp: 2000,
|
|
Difficulty: 110,
|
|
Output: []byte("frame1_output"),
|
|
ParentSelector: computeAppPoseidonHash(genesis.Header.Output),
|
|
Prover: []byte("prover1"),
|
|
PublicKeySignatureBls48581: &protobufs.BLS48581AggregateSignature{
|
|
Bitmask: []byte{0b11111111}, // 4 signers
|
|
},
|
|
},
|
|
}
|
|
|
|
reg.On("GetNextProver", [32]byte(computeAppPoseidonHash(genesis.Header.Output)), mock.Anything).Return([]byte("prover1"), nil)
|
|
reg.On("GetOrderedProvers", [32]byte(computeAppPoseidonHash(genesis.Header.Output)), mock.Anything).Return([][]byte{
|
|
[]byte("prover1"),
|
|
[]byte("prover2"),
|
|
[]byte("prover3"),
|
|
[]byte("prover4"),
|
|
[]byte("prover5"),
|
|
[]byte("prover6"),
|
|
[]byte("prover7"),
|
|
[]byte("prover8"),
|
|
}, nil)
|
|
|
|
err = atr.Insert(frame1)
|
|
require.NoError(t, err)
|
|
select {
|
|
case <-eventCh:
|
|
case <-time.After(50 * time.Millisecond):
|
|
}
|
|
|
|
// Build chain A: 1 -> 2A -> 3A -> 4A (fewer signatures initially)
|
|
frame2A := &protobufs.AppShardFrame{
|
|
Header: &protobufs.FrameHeader{
|
|
Address: address,
|
|
FrameNumber: 2,
|
|
Timestamp: 3000,
|
|
Difficulty: 120,
|
|
Output: []byte("frame2A_output"),
|
|
ParentSelector: computeAppPoseidonHash(frame1.Header.Output),
|
|
Prover: []byte("prover8"),
|
|
PublicKeySignatureBls48581: &protobufs.BLS48581AggregateSignature{
|
|
Bitmask: []byte{0b00000011}, // 2 signers (weaker)
|
|
},
|
|
},
|
|
}
|
|
|
|
reg.On("GetNextProver", [32]byte(computeAppPoseidonHash(frame1.Header.Output)), mock.Anything).Return([]byte("prover2"), nil)
|
|
reg.On("GetOrderedProvers", [32]byte(computeAppPoseidonHash(frame1.Header.Output)), mock.Anything).Return([][]byte{
|
|
[]byte("prover2"),
|
|
[]byte("prover1"),
|
|
[]byte("prover3"),
|
|
[]byte("prover4"),
|
|
[]byte("prover5"),
|
|
[]byte("prover6"),
|
|
[]byte("prover7"),
|
|
[]byte("prover8"),
|
|
}, nil)
|
|
|
|
frame3A := &protobufs.AppShardFrame{
|
|
Header: &protobufs.FrameHeader{
|
|
Address: address,
|
|
FrameNumber: 3,
|
|
Timestamp: 4000,
|
|
Difficulty: 130,
|
|
Output: []byte("frame3A_output"),
|
|
ParentSelector: computeAppPoseidonHash(frame2A.Header.Output),
|
|
Prover: []byte("prover7"),
|
|
PublicKeySignatureBls48581: &protobufs.BLS48581AggregateSignature{
|
|
Bitmask: []byte{0b00001111}, // 4 signers
|
|
},
|
|
},
|
|
}
|
|
|
|
reg.On("GetNextProver", [32]byte(computeAppPoseidonHash(frame2A.Header.Output)), mock.Anything).Return([]byte("prover7"), nil)
|
|
reg.On("GetOrderedProvers", [32]byte(computeAppPoseidonHash(frame2A.Header.Output)), mock.Anything).Return([][]byte{
|
|
[]byte("prover7"),
|
|
[]byte("prover5"),
|
|
[]byte("prover6"),
|
|
[]byte("prover8"),
|
|
[]byte("prover2"),
|
|
[]byte("prover1"),
|
|
[]byte("prover3"),
|
|
[]byte("prover4"),
|
|
}, nil)
|
|
|
|
frame4A := &protobufs.AppShardFrame{
|
|
Header: &protobufs.FrameHeader{
|
|
Address: address,
|
|
FrameNumber: 4,
|
|
Timestamp: 5000,
|
|
Difficulty: 140,
|
|
Output: []byte("frame4A_output"),
|
|
ParentSelector: computeAppPoseidonHash(frame3A.Header.Output),
|
|
Prover: []byte("prover6"),
|
|
PublicKeySignatureBls48581: &protobufs.BLS48581AggregateSignature{
|
|
Bitmask: []byte{0b00001111}, // 4 signers
|
|
},
|
|
},
|
|
}
|
|
|
|
reg.On("GetNextProver", [32]byte(computeAppPoseidonHash(frame3A.Header.Output)), mock.Anything).Return([]byte("prover6"), nil)
|
|
reg.On("GetOrderedProvers", [32]byte(computeAppPoseidonHash(frame3A.Header.Output)), mock.Anything).Return([][]byte{
|
|
[]byte("prover6"),
|
|
[]byte("prover7"),
|
|
[]byte("prover8"),
|
|
[]byte("prover2"),
|
|
[]byte("prover1"),
|
|
[]byte("prover3"),
|
|
[]byte("prover4"),
|
|
[]byte("prover5"),
|
|
}, nil)
|
|
|
|
// Insert chain A frames in order: 2A, 3A, 4A
|
|
err = atr.Insert(frame2A)
|
|
require.NoError(t, err)
|
|
select {
|
|
case <-eventCh:
|
|
case <-time.After(50 * time.Millisecond):
|
|
}
|
|
|
|
err = atr.Insert(frame3A)
|
|
require.NoError(t, err)
|
|
select {
|
|
case <-eventCh:
|
|
case <-time.After(50 * time.Millisecond):
|
|
}
|
|
|
|
err = atr.Insert(frame4A)
|
|
require.NoError(t, err)
|
|
select {
|
|
case <-eventCh:
|
|
case <-time.After(50 * time.Millisecond):
|
|
}
|
|
|
|
// Verify chain A is the current head
|
|
head, err := atr.GetHead()
|
|
require.NoError(t, err)
|
|
assert.Equal(t, uint64(4), head.Header.FrameNumber)
|
|
assert.Equal(t, []byte("frame4A_output"), head.Header.Output)
|
|
|
|
// Now create chain B: 1 -> 2B -> 3B -> 4B (stronger chain)
|
|
frame2B := &protobufs.AppShardFrame{
|
|
Header: &protobufs.FrameHeader{
|
|
Address: address,
|
|
FrameNumber: 2,
|
|
Timestamp: 2900, // Earlier timestamp than 2A
|
|
Difficulty: 120,
|
|
Output: []byte("frame2B_output"),
|
|
ParentSelector: computeAppPoseidonHash(frame1.Header.Output),
|
|
Prover: []byte("prover2"),
|
|
PublicKeySignatureBls48581: &protobufs.BLS48581AggregateSignature{
|
|
Bitmask: []byte{0b11110000}, // 4 signers (stronger than frame2A)
|
|
},
|
|
},
|
|
}
|
|
|
|
frame3B := &protobufs.AppShardFrame{
|
|
Header: &protobufs.FrameHeader{
|
|
Address: address,
|
|
FrameNumber: 3,
|
|
Timestamp: 3900, // Earlier timestamp than 3A
|
|
Difficulty: 130,
|
|
Output: []byte("frame3B_output"),
|
|
ParentSelector: computeAppPoseidonHash(frame2B.Header.Output),
|
|
Prover: []byte("prover3"),
|
|
PublicKeySignatureBls48581: &protobufs.BLS48581AggregateSignature{
|
|
Bitmask: []byte{0b11110000}, // 4 signers (same count as 3A, different bits, earlier timestamp)
|
|
},
|
|
},
|
|
}
|
|
|
|
reg.On("GetNextProver", [32]byte(computeAppPoseidonHash(frame2B.Header.Output)), mock.Anything).Return([]byte("prover2"), nil)
|
|
reg.On("GetOrderedProvers", [32]byte(computeAppPoseidonHash(frame2B.Header.Output)), mock.Anything).Return([][]byte{
|
|
[]byte("prover3"),
|
|
[]byte("prover2"),
|
|
[]byte("prover1"),
|
|
[]byte("prover4"),
|
|
[]byte("prover5"),
|
|
[]byte("prover6"),
|
|
[]byte("prover7"),
|
|
[]byte("prover8"),
|
|
}, nil)
|
|
|
|
frame4B := &protobufs.AppShardFrame{
|
|
Header: &protobufs.FrameHeader{
|
|
Address: address,
|
|
FrameNumber: 4,
|
|
Timestamp: 4900, // Earlier timestamp than 4A
|
|
Difficulty: 140,
|
|
Output: []byte("frame4B_output"),
|
|
ParentSelector: computeAppPoseidonHash(frame3B.Header.Output),
|
|
Prover: []byte("prover4"),
|
|
PublicKeySignatureBls48581: &protobufs.BLS48581AggregateSignature{
|
|
Bitmask: []byte{0b11110000}, // 4 signers (same count as 4A, different bits, earlier timestamp)
|
|
},
|
|
},
|
|
}
|
|
|
|
reg.On("GetNextProver", [32]byte(computeAppPoseidonHash(frame3B.Header.Output)), mock.Anything).Return([]byte("prover2"), nil)
|
|
reg.On("GetOrderedProvers", [32]byte(computeAppPoseidonHash(frame3B.Header.Output)), mock.Anything).Return([][]byte{
|
|
[]byte("prover4"),
|
|
[]byte("prover2"),
|
|
[]byte("prover1"),
|
|
[]byte("prover3"),
|
|
[]byte("prover5"),
|
|
[]byte("prover6"),
|
|
[]byte("prover7"),
|
|
[]byte("prover8"),
|
|
}, nil)
|
|
|
|
// Insert chain B in REVERSE order: 4B, 3B, 2B
|
|
// This should work because the time reel should handle out-of-order insertion
|
|
|
|
// Insert frame 4B first
|
|
err = atr.Insert(frame4B)
|
|
require.NoError(t, err, "inserting 4B should succeed even without its parents")
|
|
select {
|
|
case <-eventCh:
|
|
case <-time.After(50 * time.Millisecond):
|
|
}
|
|
|
|
// Head should still be chain A since 4B's lineage is incomplete
|
|
head, err = atr.GetHead()
|
|
require.NoError(t, err)
|
|
assert.Equal(t, uint64(4), head.Header.FrameNumber)
|
|
assert.Equal(t, []byte("frame4A_output"), head.Header.Output, "should still be chain A")
|
|
|
|
// Insert frame 3B
|
|
err = atr.Insert(frame3B)
|
|
require.NoError(t, err, "inserting 3B should succeed")
|
|
select {
|
|
case <-eventCh:
|
|
case <-time.After(50 * time.Millisecond):
|
|
}
|
|
|
|
// Head should still be chain A
|
|
head, err = atr.GetHead()
|
|
require.NoError(t, err)
|
|
assert.Equal(t, uint64(4), head.Header.FrameNumber)
|
|
assert.Equal(t, []byte("frame4A_output"), head.Header.Output, "should still be chain A")
|
|
|
|
// Insert frame 2B - this completes the chain B lineage
|
|
err = atr.Insert(frame2B)
|
|
require.NoError(t, err, "inserting 2B should succeed and complete chain B")
|
|
|
|
// Give time for reorganization
|
|
time.Sleep(50 * time.Millisecond)
|
|
|
|
// Check for reorganization event
|
|
headFrameNumber := uint64(2)
|
|
outer:
|
|
for {
|
|
select {
|
|
case event := <-eventCh:
|
|
if event.Type == TimeReelEventForkDetected {
|
|
assert.Contains(t, event.Message, "fork detected")
|
|
} else {
|
|
assert.Equal(t, TimeReelEventNewHead, event.Type)
|
|
assert.Equal(t, headFrameNumber, event.Frame.Header.FrameNumber)
|
|
headFrameNumber++
|
|
}
|
|
case <-time.After(200 * time.Millisecond):
|
|
break outer
|
|
}
|
|
}
|
|
|
|
// Now chain B should be head (stronger at frame 2, with complete lineage)
|
|
head, err = atr.GetHead()
|
|
require.NoError(t, err)
|
|
|
|
assert.Equal(t, uint64(4), head.Header.FrameNumber)
|
|
assert.Equal(t, []byte("frame4B_output"), head.Header.Output, "chain B should become head after complete insertion")
|
|
}
|
|
|
|
func TestAppTimeReel_MultipleProvers(t *testing.T) {
|
|
logger, _ := zap.NewDevelopment()
|
|
address := []byte("test_app_address")
|
|
s := setupTestClockStore(t)
|
|
atr, err := NewAppTimeReel(logger, address, createTestProverRegistry(true), s, true)
|
|
require.NoError(t, err)
|
|
|
|
ctx, cancel, _ := lifecycle.WithSignallerAndCancel(context.Background())
|
|
go atr.Start(ctx, func() {})
|
|
time.Sleep(100 * time.Millisecond)
|
|
defer cancel()
|
|
|
|
// Different provers create frames
|
|
provers := [][]byte{
|
|
[]byte("prover1"),
|
|
[]byte("prover2"),
|
|
[]byte("prover3"),
|
|
}
|
|
|
|
// Insert genesis
|
|
genesis := &protobufs.AppShardFrame{
|
|
Header: &protobufs.FrameHeader{
|
|
Address: address,
|
|
FrameNumber: 0,
|
|
Timestamp: 1000,
|
|
Difficulty: 100,
|
|
Output: []byte("genesis"),
|
|
ParentSelector: []byte{},
|
|
Prover: provers[0],
|
|
},
|
|
}
|
|
|
|
err = atr.Insert(genesis)
|
|
require.NoError(t, err)
|
|
|
|
// Build chain with alternating provers
|
|
prevOutput := genesis.Header.Output
|
|
for i := 1; i <= 3; i++ {
|
|
frame := &protobufs.AppShardFrame{
|
|
Header: &protobufs.FrameHeader{
|
|
Address: address,
|
|
FrameNumber: uint64(i),
|
|
Timestamp: int64(1000 + i*1000),
|
|
Difficulty: uint32(100 + i*10),
|
|
Output: []byte(fmt.Sprintf("frame%d", i)),
|
|
ParentSelector: computeAppPoseidonHash(prevOutput),
|
|
Prover: provers[i%len(provers)],
|
|
},
|
|
}
|
|
|
|
err = atr.Insert(frame)
|
|
require.NoError(t, err)
|
|
|
|
prevOutput = frame.Header.Output
|
|
}
|
|
|
|
// Verify final head
|
|
head, err := atr.GetHead()
|
|
require.NoError(t, err)
|
|
assert.Equal(t, uint64(3), head.Header.FrameNumber)
|
|
|
|
// Verify lineage
|
|
lineage, err := atr.GetLineage()
|
|
require.NoError(t, err)
|
|
assert.Len(t, lineage, 4)
|
|
|
|
// Check prover rotation
|
|
for i, frame := range lineage {
|
|
assert.Equal(t, provers[i%len(provers)], frame.Header.Prover)
|
|
}
|
|
}
|
|
|
|
// TestAppTimeReel_ComplexForkWithOutOfOrderInsertion tests the scenario:
|
|
// Insert 1, then insert 3', then insert 3, then insert 2, then insert 3”, then insert 2'.
|
|
// Expected: 1 -> 2' -> 3' should win with perfect prover distances
|
|
func TestAppTimeReel_ComplexForkWithOutOfOrderInsertion(t *testing.T) {
|
|
logger, _ := zap.NewDevelopment()
|
|
address := []byte("test_app_address")
|
|
|
|
// Set up prover registry with specific expected provers for each parent
|
|
proverRegistry := new(mocks.MockProverRegistry)
|
|
|
|
// For genesis (empty parent selector), frame 1's prover is expected
|
|
var genesisSelector [32]byte
|
|
proverRegistry.On("GetNextProver", genesisSelector, mock.Anything).Return([]byte("prover1"), nil)
|
|
proverRegistry.On("GetOrderedProvers", genesisSelector, mock.Anything).Return([][]byte{[]byte("prover1")}, nil)
|
|
|
|
// For frame 1's output as parent, frame 2' prover is the expected one
|
|
frame1Output := []byte("frame1_output")
|
|
computedSelector1 := computeAppPoseidonHash(frame1Output)
|
|
var selector1 [32]byte
|
|
copy(selector1[:], computedSelector1)
|
|
proverRegistry.On("GetNextProver", selector1, mock.Anything).Return([]byte("prover2prime"), nil)
|
|
proverRegistry.On("GetOrderedProvers", selector1, mock.Anything).Return([][]byte{
|
|
[]byte("prover2prime"), // distance 0
|
|
[]byte("prover2"), // distance 1
|
|
[]byte("prover3"), // distance 2
|
|
[]byte("prover3doubleprime"), // distance 3
|
|
}, nil)
|
|
|
|
// For frame 2's output as parent, frame 3 prover is expected
|
|
frame2Output := []byte("frame2_output")
|
|
computedSelector2 := computeAppPoseidonHash(frame2Output)
|
|
var selector2 [32]byte
|
|
copy(selector2[:], computedSelector2)
|
|
proverRegistry.On("GetNextProver", selector2, mock.Anything).Return([]byte("prover3"), nil)
|
|
proverRegistry.On("GetOrderedProvers", selector2, mock.Anything).Return([][]byte{
|
|
[]byte("prover3"), // distance 0
|
|
[]byte("prover3doubleprime"), // distance 1
|
|
[]byte("prover2prime"), // distance 2
|
|
[]byte("prover2"), // distance 3
|
|
}, nil)
|
|
|
|
// For frame 2' output as parent, frame 3' prover is expected
|
|
frame2PrimeOutput := []byte("frame2prime_output")
|
|
computedSelector2Prime := computeAppPoseidonHash(frame2PrimeOutput)
|
|
var selector2Prime [32]byte
|
|
copy(selector2Prime[:], computedSelector2Prime)
|
|
proverRegistry.On("GetNextProver", selector2Prime, mock.Anything).Return([]byte("prover3prime"), nil)
|
|
proverRegistry.On("GetOrderedProvers", selector2Prime, mock.Anything).Return([][]byte{
|
|
[]byte("prover3prime"), // distance 0
|
|
[]byte("prover3doubleprime"), // distance 1
|
|
[]byte("prover2prime"), // distance 2
|
|
[]byte("prover2"), // distance 3
|
|
}, nil)
|
|
|
|
// Default behavior for unknown parent selectors
|
|
proverRegistry.On("GetNextProver", mock.Anything, mock.Anything).Return(nil, errors.New("unknown parent selector"))
|
|
proverRegistry.On("GetOrderedProvers", mock.Anything, mock.Anything).Return(nil, errors.New("unknown parent selector"))
|
|
|
|
s := setupTestClockStore(t)
|
|
atr, err := NewAppTimeReel(logger, address, proverRegistry, s, 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 all events
|
|
var eventsMu sync.Mutex
|
|
events := make([]AppEvent, 0)
|
|
go func() {
|
|
for event := range eventCh {
|
|
eventsMu.Lock()
|
|
events = append(events, event)
|
|
eventsMu.Unlock()
|
|
t.Logf("Event: Type=%d, Frame=%d, Message=%s",
|
|
event.Type,
|
|
event.Frame.Header.FrameNumber,
|
|
event.Message)
|
|
}
|
|
}()
|
|
|
|
// Create all frames first
|
|
genesis := &protobufs.AppShardFrame{
|
|
Header: &protobufs.FrameHeader{
|
|
Address: address,
|
|
FrameNumber: 0,
|
|
Timestamp: time.Now().UnixMilli() - 5000,
|
|
Difficulty: 100,
|
|
Output: []byte("genesis_output"),
|
|
ParentSelector: []byte{},
|
|
Prover: []byte("prover0"),
|
|
},
|
|
}
|
|
|
|
frame1 := &protobufs.AppShardFrame{
|
|
Header: &protobufs.FrameHeader{
|
|
Address: address,
|
|
FrameNumber: 1,
|
|
Timestamp: time.Now().UnixMilli() - 4000,
|
|
Difficulty: 110,
|
|
Output: frame1Output,
|
|
ParentSelector: computeAppPoseidonHash(genesis.Header.Output),
|
|
Prover: []byte("prover1"), // Expected prover (distance 0)
|
|
PublicKeySignatureBls48581: &protobufs.BLS48581AggregateSignature{
|
|
Bitmask: []byte{0b00001111}, // 4 signers
|
|
},
|
|
},
|
|
}
|
|
|
|
frame2 := &protobufs.AppShardFrame{
|
|
Header: &protobufs.FrameHeader{
|
|
Address: address,
|
|
FrameNumber: 2,
|
|
Timestamp: time.Now().UnixMilli() - 3000,
|
|
Difficulty: 120,
|
|
Output: frame2Output,
|
|
ParentSelector: computeAppPoseidonHash(frame1.Header.Output),
|
|
Prover: []byte("prover2"), // Not expected prover (distance 1)
|
|
PublicKeySignatureBls48581: &protobufs.BLS48581AggregateSignature{
|
|
Bitmask: []byte{0b00000011}, // 4 signers
|
|
},
|
|
},
|
|
}
|
|
|
|
frame2Prime := &protobufs.AppShardFrame{
|
|
Header: &protobufs.FrameHeader{
|
|
Address: address,
|
|
FrameNumber: 2,
|
|
Timestamp: time.Now().UnixMilli() - 2900, // Later timestamp but expected prover
|
|
Difficulty: 120,
|
|
Output: frame2PrimeOutput,
|
|
ParentSelector: computeAppPoseidonHash(frame1.Header.Output),
|
|
Prover: []byte("prover2prime"), // Expected prover (distance 0)
|
|
PublicKeySignatureBls48581: &protobufs.BLS48581AggregateSignature{
|
|
Bitmask: []byte{0b11110000}, // 4 signers, different bits to avoid equivocation
|
|
},
|
|
},
|
|
}
|
|
|
|
frame3 := &protobufs.AppShardFrame{
|
|
Header: &protobufs.FrameHeader{
|
|
Address: address,
|
|
FrameNumber: 3,
|
|
Timestamp: time.Now().UnixMilli() - 2000,
|
|
Difficulty: 130,
|
|
Output: []byte("frame3_output"),
|
|
ParentSelector: computeAppPoseidonHash(frame2.Header.Output),
|
|
Prover: []byte("prover3"), // Expected prover for frame2 (distance 0)
|
|
PublicKeySignatureBls48581: &protobufs.BLS48581AggregateSignature{
|
|
Bitmask: []byte{0b00001100}, // 2 signers
|
|
},
|
|
},
|
|
}
|
|
|
|
frame3Prime := &protobufs.AppShardFrame{
|
|
Header: &protobufs.FrameHeader{
|
|
Address: address,
|
|
FrameNumber: 3,
|
|
Timestamp: time.Now().UnixMilli() - 1900,
|
|
Difficulty: 130,
|
|
Output: []byte("frame3prime_output"),
|
|
ParentSelector: computeAppPoseidonHash(frame2Prime.Header.Output),
|
|
Prover: []byte("prover3prime"), // Expected prover for frame2' (distance 0)
|
|
PublicKeySignatureBls48581: &protobufs.BLS48581AggregateSignature{
|
|
Bitmask: []byte{0b11110000}, // 4 signers, different bits
|
|
},
|
|
},
|
|
}
|
|
|
|
frame3DoublePrime := &protobufs.AppShardFrame{
|
|
Header: &protobufs.FrameHeader{
|
|
Address: address,
|
|
FrameNumber: 3,
|
|
Timestamp: time.Now().UnixMilli() - 1900,
|
|
Difficulty: 130,
|
|
Output: []byte("frame3doubleprime_output"),
|
|
ParentSelector: computeAppPoseidonHash(frame2Prime.Header.Output),
|
|
Prover: []byte("prover3doubleprime"), // Not expected prover for frame2' (distance 1)
|
|
PublicKeySignatureBls48581: &protobufs.BLS48581AggregateSignature{
|
|
Bitmask: []byte{0b00000011}, // 2 signers
|
|
},
|
|
},
|
|
}
|
|
|
|
// Now insert in the specified order: 1, 3', 3, 2, 3'', 2'
|
|
|
|
// Step 1: Insert genesis first (needed as base)
|
|
err = atr.Insert(genesis)
|
|
require.NoError(t, err)
|
|
time.Sleep(50 * time.Millisecond)
|
|
|
|
// Step 2: Insert frame 1
|
|
t.Log("Inserting frame 1")
|
|
err = atr.Insert(frame1)
|
|
require.NoError(t, err)
|
|
time.Sleep(50 * time.Millisecond)
|
|
|
|
// Step 3: Insert frame 3' (should go to pending since 2' doesn't exist yet)
|
|
t.Log("Inserting frame 3'")
|
|
err = atr.Insert(frame3Prime)
|
|
require.NoError(t, err)
|
|
time.Sleep(50 * time.Millisecond)
|
|
|
|
// Verify 3' is in pending
|
|
pending := atr.GetPendingFrames()
|
|
assert.True(t, len(pending) > 0, "frame 3' should be in pending")
|
|
|
|
// Step 4: Insert frame 3 (should also go to pending since 2 doesn't exist yet)
|
|
t.Log("Inserting frame 3")
|
|
err = atr.Insert(frame3)
|
|
require.NoError(t, err)
|
|
time.Sleep(50 * time.Millisecond)
|
|
|
|
// Step 5: Insert frame 2 (this should complete the 1->2->3 chain)
|
|
t.Log("Inserting frame 2")
|
|
err = atr.Insert(frame2)
|
|
require.NoError(t, err)
|
|
time.Sleep(100 * time.Millisecond) // Give more time for processing
|
|
|
|
// Check head should be 3 now
|
|
head, err := atr.GetHead()
|
|
require.NoError(t, err)
|
|
assert.Equal(t, uint64(3), head.Header.FrameNumber)
|
|
assert.Equal(t, []byte("frame3_output"), head.Header.Output, "chain 1->2->3 should be head")
|
|
|
|
// Step 6: Insert frame 3'' (another competing frame on 2')
|
|
t.Log("Inserting frame 3''")
|
|
err = atr.Insert(frame3DoublePrime)
|
|
require.NoError(t, err)
|
|
time.Sleep(50 * time.Millisecond)
|
|
|
|
// Step 7: Insert frame 2' (this completes the 1->2'->3' and 1->2'->3'' chains)
|
|
t.Log("Inserting frame 2'")
|
|
err = atr.Insert(frame2Prime)
|
|
require.NoError(t, err)
|
|
time.Sleep(200 * time.Millisecond) // Give ample time for fork choice evaluation
|
|
|
|
// Final verification: 1->2'->3' should win because:
|
|
// - Frame 1: distance 0 (expected prover)
|
|
// - Frame 2': distance 0 (expected prover) vs Frame 2: distance 1
|
|
// - Frame 3': distance 0 (expected prover) with 4 signers vs Frame 3'': distance 1 with 2 signers
|
|
head, err = atr.GetHead()
|
|
require.NoError(t, err)
|
|
assert.Equal(t, uint64(3), head.Header.FrameNumber)
|
|
assert.Equal(t, []byte("frame3prime_output"), head.Header.Output, "chain 1->2'->3' should win")
|
|
|
|
// Verify the lineage is correct
|
|
lineage, err := atr.GetLineage()
|
|
require.NoError(t, err)
|
|
require.Len(t, lineage, 4) // genesis -> 1 -> 2' -> 3'
|
|
assert.Equal(t, []byte("genesis_output"), lineage[0].Header.Output)
|
|
assert.Equal(t, frame1Output, lineage[1].Header.Output)
|
|
assert.Equal(t, frame2PrimeOutput, lineage[2].Header.Output)
|
|
assert.Equal(t, []byte("frame3prime_output"), lineage[3].Header.Output)
|
|
|
|
// Check that we got fork detection events
|
|
eventsMu.Lock()
|
|
forkEvents := 0
|
|
for _, event := range events {
|
|
if event.Type == TimeReelEventForkDetected {
|
|
forkEvents++
|
|
t.Logf("Fork event: %s", event.Message)
|
|
}
|
|
}
|
|
eventsMu.Unlock()
|
|
|
|
assert.Greater(t, forkEvents, 0, "should have detected at least one fork")
|
|
}
|
|
|
|
func TestAppTimeReel_TreePruning(t *testing.T) {
|
|
logger, _ := zap.NewDevelopment()
|
|
address := []byte("test_app_address")
|
|
s := setupTestClockStore(t)
|
|
atr, err := NewAppTimeReel(logger, address, createTestProverRegistry(true), s, 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.AppShardFrame{
|
|
Header: &protobufs.FrameHeader{
|
|
Address: address,
|
|
FrameNumber: 0,
|
|
Timestamp: 1000,
|
|
Difficulty: 100,
|
|
Output: []byte("genesis"),
|
|
ParentSelector: []byte{},
|
|
Prover: []byte("prover1"),
|
|
},
|
|
}
|
|
|
|
err = atr.Insert(genesis)
|
|
require.NoError(t, err)
|
|
|
|
// Build a long chain that will trigger pruning (370 frames total)
|
|
prevOutput := genesis.Header.Output
|
|
for i := 1; i <= 370; i++ {
|
|
frame := &protobufs.AppShardFrame{
|
|
Header: &protobufs.FrameHeader{
|
|
Address: address,
|
|
FrameNumber: uint64(i),
|
|
Timestamp: int64(1000 + i*1000),
|
|
Difficulty: uint32(100 + i),
|
|
Output: []byte(fmt.Sprintf("frame%d", i)),
|
|
ParentSelector: computeAppPoseidonHash(prevOutput),
|
|
Prover: []byte(fmt.Sprintf("prover%d", i%3+1)),
|
|
},
|
|
}
|
|
|
|
err = atr.Insert(frame)
|
|
require.NoError(t, err)
|
|
|
|
prevOutput = frame.Header.Output
|
|
}
|
|
|
|
// Verify head is at frame 370
|
|
head, err := atr.GetHead()
|
|
require.NoError(t, err)
|
|
assert.Equal(t, uint64(370), head.Header.FrameNumber)
|
|
|
|
// Get tree info to verify pruning occurred
|
|
info := atr.GetTreeInfo()
|
|
t.Logf("Tree info: %+v", info)
|
|
|
|
// Verify tree span is at most 360 frames
|
|
treeSpan := info["tree_span"].(uint64)
|
|
assert.LessOrEqual(t, treeSpan, uint64(360), "tree span should not exceed 360 frames")
|
|
|
|
// Verify minimum depth is reasonable (should have pruned old frames)
|
|
minDepth := info["min_depth"].(uint64)
|
|
maxDepth := info["max_depth"].(uint64)
|
|
headDepth := info["head_depth"].(uint64)
|
|
|
|
assert.Equal(t, uint64(370), headDepth, "head depth should be 370")
|
|
assert.Equal(t, headDepth, maxDepth, "max depth should equal head depth")
|
|
assert.GreaterOrEqual(t, minDepth, uint64(11), "min depth should be at least 11 (370-360+1)")
|
|
|
|
// Verify we can still get lineage (should be limited to available frames)
|
|
lineage, err := atr.GetLineage()
|
|
require.NoError(t, err)
|
|
assert.LessOrEqual(t, len(lineage), 360, "lineage should not exceed 360 frames")
|
|
|
|
// Verify we can't get very old frames
|
|
oldFrames, err := atr.GetFramesByNumber(0)
|
|
assert.Error(t, err, "should not be able to get genesis frame after pruning")
|
|
assert.Nil(t, oldFrames)
|
|
|
|
// Verify we can get recent frames
|
|
recentFrames, err := atr.GetFramesByNumber(370)
|
|
require.NoError(t, err)
|
|
assert.Len(t, recentFrames, 1)
|
|
assert.Equal(t, uint64(370), recentFrames[0].Header.FrameNumber)
|
|
}
|
|
|
|
func TestAppTimeReel_TreePruningWithForks(t *testing.T) {
|
|
logger, _ := zap.NewDevelopment()
|
|
address := []byte("test_app_address")
|
|
s := setupTestClockStore(t)
|
|
atr, err := NewAppTimeReel(logger, address, createTestProverRegistry(true), s, 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.AppShardFrame{
|
|
Header: &protobufs.FrameHeader{
|
|
Address: address,
|
|
FrameNumber: 0,
|
|
Timestamp: 1000,
|
|
Difficulty: 100,
|
|
Output: []byte("genesis"),
|
|
ParentSelector: []byte{},
|
|
Prover: []byte("prover1"),
|
|
},
|
|
}
|
|
|
|
err = atr.Insert(genesis)
|
|
require.NoError(t, err)
|
|
|
|
// Build main chain for 365 frames
|
|
prevOutput := genesis.Header.Output
|
|
var frame5 *protobufs.AppShardFrame
|
|
for i := 1; i <= 365; i++ {
|
|
frame := &protobufs.AppShardFrame{
|
|
Header: &protobufs.FrameHeader{
|
|
Address: address,
|
|
FrameNumber: uint64(i),
|
|
Timestamp: int64(1000 + i*1000),
|
|
Difficulty: uint32(100 + i),
|
|
Output: []byte(fmt.Sprintf("main_frame%d", i)),
|
|
ParentSelector: computeAppPoseidonHash(prevOutput),
|
|
Prover: []byte("main_prover"),
|
|
},
|
|
}
|
|
|
|
err = atr.Insert(frame)
|
|
require.NoError(t, err)
|
|
|
|
if i == 5 {
|
|
frame5 = frame
|
|
}
|
|
|
|
prevOutput = frame.Header.Output
|
|
}
|
|
|
|
// Create a fork at frame 6 that branches from frame 5
|
|
// This fork gets pruned when we continue the main chain past 365+360
|
|
forkFrame := &protobufs.AppShardFrame{
|
|
Header: &protobufs.FrameHeader{
|
|
Address: address,
|
|
FrameNumber: 6,
|
|
Timestamp: 6000,
|
|
Difficulty: 106,
|
|
Output: []byte("fork_frame6"),
|
|
ParentSelector: computeAppPoseidonHash(frame5.Header.Output),
|
|
Prover: []byte("fork_prover"),
|
|
PublicKeySignatureBls48581: &protobufs.BLS48581AggregateSignature{
|
|
Bitmask: []byte{0b00000001}, // Fewer signatures than main chain
|
|
},
|
|
},
|
|
}
|
|
|
|
err = atr.Insert(forkFrame)
|
|
require.NoError(t, err)
|
|
|
|
// Continue main chain for 375 more frames to trigger deep pruning
|
|
for i := 366; i <= 740; i++ {
|
|
frame := &protobufs.AppShardFrame{
|
|
Header: &protobufs.FrameHeader{
|
|
Address: address,
|
|
FrameNumber: uint64(i),
|
|
Timestamp: int64(1000 + i*1000),
|
|
Difficulty: uint32(100 + i),
|
|
Output: []byte(fmt.Sprintf("main_frame%d", i)),
|
|
ParentSelector: computeAppPoseidonHash(prevOutput),
|
|
Prover: []byte("main_prover"),
|
|
},
|
|
}
|
|
|
|
err = atr.Insert(frame)
|
|
require.NoError(t, err)
|
|
|
|
prevOutput = frame.Header.Output
|
|
}
|
|
|
|
// Verify pruning occurred and old fork was removed
|
|
info := atr.GetTreeInfo()
|
|
t.Logf("Tree info after deep pruning: %+v", info)
|
|
|
|
// Tree span should be exactly 360
|
|
treeSpan := info["tree_span"].(uint64)
|
|
assert.Equal(t, uint64(360), treeSpan, "tree span should be exactly 360 frames")
|
|
|
|
// Should have only one branch (old fork should be pruned)
|
|
branchCount := info["branch_count"].(int)
|
|
assert.Equal(t, 1, branchCount, "should have only one branch after pruning old forks")
|
|
|
|
// Minimum depth should be well past the old fork
|
|
minDepth := info["min_depth"].(uint64)
|
|
assert.Greater(t, minDepth, uint64(6), "minimum depth should be past the old fork depth")
|
|
|
|
// Head should still be the main chain
|
|
head, err := atr.GetHead()
|
|
require.NoError(t, err)
|
|
assert.Equal(t, uint64(740), head.Header.FrameNumber)
|
|
assert.Equal(t, []byte("main_frame740"), head.Header.Output)
|
|
}
|
|
|
|
// TestAppTimeReel_ForkChoiceInsertionOrder tests that fork choice works regardless of insertion order
|
|
func TestAppTimeReel_ForkChoiceInsertionOrder(t *testing.T) {
|
|
logger, _ := zap.NewDevelopment()
|
|
address := []byte("test_app_address")
|
|
reg := createTestProverRegistry(false)
|
|
s := setupTestClockStore(t)
|
|
atr, err := NewAppTimeReel(logger, address, reg, s, 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 any existing events
|
|
for {
|
|
select {
|
|
case <-eventCh:
|
|
case <-time.After(10 * time.Millisecond):
|
|
goto drained
|
|
}
|
|
}
|
|
drained:
|
|
|
|
// Insert genesis
|
|
genesis := &protobufs.AppShardFrame{
|
|
Header: &protobufs.FrameHeader{
|
|
Address: address,
|
|
FrameNumber: 0,
|
|
Timestamp: 1000,
|
|
Difficulty: 100,
|
|
Output: []byte("genesis_output"),
|
|
ParentSelector: []byte{},
|
|
Prover: []byte("prover1"),
|
|
},
|
|
}
|
|
reg.On("GetNextProver", [32]byte{}, mock.Anything).Return([]byte("prover1"), nil)
|
|
reg.On("GetOrderedProvers", [32]byte{}, mock.Anything).Return([][]byte{
|
|
[]byte("prover1"),
|
|
[]byte("prover2"),
|
|
[]byte("prover3"),
|
|
[]byte("prover4"),
|
|
[]byte("prover5"),
|
|
[]byte("prover6"),
|
|
[]byte("prover7"),
|
|
[]byte("prover8"),
|
|
}, nil)
|
|
|
|
err = atr.Insert(genesis)
|
|
require.NoError(t, err)
|
|
|
|
// Drain genesis event
|
|
select {
|
|
case <-eventCh:
|
|
case <-time.After(50 * time.Millisecond):
|
|
}
|
|
|
|
// Create two competing branches:
|
|
// Branch A: 0 -> 1A -> 2A (weaker)
|
|
// Branch B: 0 -> 1B -> 2B (stronger)
|
|
// Insert in REVERSE order to test fork choice works regardless of order
|
|
|
|
// First, insert the STRONGER branch tip (2B) without its parent
|
|
frame2B := &protobufs.AppShardFrame{
|
|
Header: &protobufs.FrameHeader{
|
|
Address: address,
|
|
FrameNumber: 2,
|
|
Timestamp: 3000,
|
|
Difficulty: 120,
|
|
Output: []byte("frame2B_output"),
|
|
ParentSelector: []byte("placeholder_for_1B"),
|
|
Prover: []byte("prover2"),
|
|
PublicKeySignatureBls48581: &protobufs.BLS48581AggregateSignature{
|
|
Bitmask: []byte{0b11111100}, // 6 signers (very strong)
|
|
},
|
|
},
|
|
}
|
|
|
|
// Insert the WEAKER branch (1A -> 2A) first, which should become head initially
|
|
frame1A := &protobufs.AppShardFrame{
|
|
Header: &protobufs.FrameHeader{
|
|
Address: address,
|
|
FrameNumber: 1,
|
|
Timestamp: 2000,
|
|
Difficulty: 110,
|
|
Output: []byte("frame1A_output"),
|
|
ParentSelector: computeAppPoseidonHash(genesis.Header.Output),
|
|
Prover: []byte("prover8"),
|
|
PublicKeySignatureBls48581: &protobufs.BLS48581AggregateSignature{
|
|
Bitmask: []byte{0b00000011}, // 2 signers (weak)
|
|
},
|
|
},
|
|
}
|
|
reg.On("GetNextProver", [32]byte(computeAppPoseidonHash(genesis.Header.Output)), mock.Anything).Return([]byte("prover2"), nil)
|
|
reg.On("GetOrderedProvers", [32]byte(computeAppPoseidonHash(genesis.Header.Output)), mock.Anything).Return([][]byte{
|
|
[]byte("prover2"),
|
|
[]byte("prover1"),
|
|
[]byte("prover3"),
|
|
[]byte("prover4"),
|
|
[]byte("prover5"),
|
|
[]byte("prover6"),
|
|
[]byte("prover7"),
|
|
[]byte("prover8"),
|
|
}, nil)
|
|
|
|
frame2A := &protobufs.AppShardFrame{
|
|
Header: &protobufs.FrameHeader{
|
|
Address: address,
|
|
FrameNumber: 2,
|
|
Timestamp: 3000,
|
|
Difficulty: 120,
|
|
Output: []byte("frame2A_output"),
|
|
ParentSelector: computeAppPoseidonHash(frame1A.Header.Output),
|
|
Prover: []byte("prover7"),
|
|
PublicKeySignatureBls48581: &protobufs.BLS48581AggregateSignature{
|
|
Bitmask: []byte{0b00000011}, // 2 signers (weak)
|
|
},
|
|
},
|
|
}
|
|
reg.On("GetNextProver", [32]byte(computeAppPoseidonHash(frame1A.Header.Output)), mock.Anything).Return([]byte("prover2"), nil)
|
|
reg.On("GetOrderedProvers", [32]byte(computeAppPoseidonHash(frame1A.Header.Output)), mock.Anything).Return([][]byte{
|
|
[]byte("prover2"),
|
|
[]byte("prover1"),
|
|
[]byte("prover3"),
|
|
[]byte("prover4"),
|
|
[]byte("prover5"),
|
|
[]byte("prover6"),
|
|
[]byte("prover7"),
|
|
[]byte("prover8"),
|
|
}, nil)
|
|
|
|
// Insert weak branch first
|
|
err = atr.Insert(frame1A)
|
|
require.NoError(t, err)
|
|
select {
|
|
case <-eventCh:
|
|
case <-time.After(50 * time.Millisecond):
|
|
}
|
|
|
|
err = atr.Insert(frame2A)
|
|
require.NoError(t, err)
|
|
select {
|
|
case <-eventCh:
|
|
case <-time.After(50 * time.Millisecond):
|
|
}
|
|
|
|
// Verify weak branch is head initially
|
|
head, err := atr.GetHead()
|
|
require.NoError(t, err)
|
|
assert.Equal(t, uint64(2), head.Header.FrameNumber)
|
|
assert.Equal(t, []byte("frame2A_output"), head.Header.Output)
|
|
|
|
// Now insert stronger branch 1B (which will allow 2B to be processed)
|
|
frame1B := &protobufs.AppShardFrame{
|
|
Header: &protobufs.FrameHeader{
|
|
Address: address,
|
|
FrameNumber: 1,
|
|
Timestamp: 2100,
|
|
Difficulty: 110,
|
|
Output: []byte("frame1B_output"),
|
|
ParentSelector: computeAppPoseidonHash(genesis.Header.Output),
|
|
Prover: []byte("prover2"),
|
|
PublicKeySignatureBls48581: &protobufs.BLS48581AggregateSignature{
|
|
Bitmask: []byte{0b11111100}, // 6 signers (strong)
|
|
},
|
|
},
|
|
}
|
|
|
|
// Update frame2B's parent selector now that we know frame1B's output
|
|
frame2B.Header.ParentSelector = computeAppPoseidonHash(frame1B.Header.Output)
|
|
reg.On("GetNextProver", [32]byte(computeAppPoseidonHash(frame1B.Header.Output)), mock.Anything).Return([]byte("prover2"), nil)
|
|
reg.On("GetOrderedProvers", [32]byte(computeAppPoseidonHash(frame1B.Header.Output)), mock.Anything).Return([][]byte{
|
|
[]byte("prover2"),
|
|
[]byte("prover1"),
|
|
[]byte("prover3"),
|
|
[]byte("prover4"),
|
|
[]byte("prover5"),
|
|
[]byte("prover6"),
|
|
[]byte("prover7"),
|
|
[]byte("prover8"),
|
|
}, nil)
|
|
|
|
// Insert stronger branch out of order: first 2B (goes to pending), then 1B
|
|
err = atr.Insert(frame2B)
|
|
require.NoError(t, err, "should accept frame 2B into pending")
|
|
|
|
// Head should still be weak branch
|
|
head, err = atr.GetHead()
|
|
require.NoError(t, err)
|
|
assert.Equal(t, []byte("frame2A_output"), head.Header.Output, "head should still be weak branch")
|
|
|
|
// Now insert 1B, which should complete the strong branch and trigger fork choice
|
|
err = atr.Insert(frame1B)
|
|
require.NoError(t, err)
|
|
|
|
// Give time for fork choice to process
|
|
time.Sleep(100 * time.Millisecond)
|
|
|
|
// Verify strong branch is now head
|
|
head, err = atr.GetHead()
|
|
require.NoError(t, err)
|
|
assert.Equal(t, uint64(2), head.Header.FrameNumber)
|
|
assert.Equal(t, []byte("frame2B_output"), head.Header.Output, "should switch to stronger branch")
|
|
|
|
// Check for fork choice event
|
|
select {
|
|
case event := <-eventCh:
|
|
assert.Equal(t, TimeReelEventForkDetected, event.Type)
|
|
assert.Contains(t, event.Message, "fork detected")
|
|
t.Logf("Fork choice event: %s", event.Message)
|
|
case <-time.After(100 * time.Millisecond):
|
|
t.Fatal("timeout waiting for fork choice event")
|
|
}
|
|
}
|
|
|
|
// TestAppTimeReel_ForkEventsWithReplay tests that fork events include common ancestor and replay
|
|
func TestAppTimeReel_ForkEventsWithReplay(t *testing.T) {
|
|
logger, _ := zap.NewDevelopment()
|
|
address := []byte("test_app_address")
|
|
s := setupTestClockStore(t)
|
|
atr, err := NewAppTimeReel(logger, address, createTestProverRegistry(true), s, 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 all events
|
|
var eventsMu sync.Mutex
|
|
events := make([]AppEvent, 0)
|
|
go func() {
|
|
for event := range eventCh {
|
|
eventsMu.Lock()
|
|
events = append(events, event)
|
|
eventsMu.Unlock()
|
|
}
|
|
}()
|
|
|
|
// Build initial chain: 0 -> 1 -> 2 -> 3
|
|
genesis := &protobufs.AppShardFrame{
|
|
Header: &protobufs.FrameHeader{
|
|
Address: address,
|
|
FrameNumber: 0,
|
|
Timestamp: 1000,
|
|
Difficulty: 100,
|
|
Output: []byte("genesis"),
|
|
ParentSelector: []byte{},
|
|
Prover: []byte("prover1"),
|
|
},
|
|
}
|
|
|
|
frame1 := &protobufs.AppShardFrame{
|
|
Header: &protobufs.FrameHeader{
|
|
Address: address,
|
|
FrameNumber: 1,
|
|
Timestamp: 2000,
|
|
Difficulty: 110,
|
|
Output: []byte("frame1"),
|
|
ParentSelector: computeAppPoseidonHash(genesis.Header.Output),
|
|
Prover: []byte("prover1"),
|
|
},
|
|
}
|
|
|
|
frame2 := &protobufs.AppShardFrame{
|
|
Header: &protobufs.FrameHeader{
|
|
Address: address,
|
|
FrameNumber: 2,
|
|
Timestamp: 3000,
|
|
Difficulty: 120,
|
|
Output: []byte("frame2"),
|
|
ParentSelector: computeAppPoseidonHash(frame1.Header.Output),
|
|
Prover: []byte("prover1"),
|
|
},
|
|
}
|
|
|
|
frame3 := &protobufs.AppShardFrame{
|
|
Header: &protobufs.FrameHeader{
|
|
Address: address,
|
|
FrameNumber: 3,
|
|
Timestamp: 4000,
|
|
Difficulty: 130,
|
|
Output: []byte("frame3"),
|
|
ParentSelector: computeAppPoseidonHash(frame2.Header.Output),
|
|
Prover: []byte("prover1"),
|
|
},
|
|
}
|
|
|
|
// Insert initial chain
|
|
for _, frame := range []*protobufs.AppShardFrame{genesis, frame1, frame2, frame3} {
|
|
err = atr.Insert(frame)
|
|
require.NoError(t, err)
|
|
time.Sleep(10 * time.Millisecond) // Allow events to be sent
|
|
}
|
|
|
|
// Clear events up to this point
|
|
eventsMu.Lock()
|
|
events = events[:0]
|
|
eventsMu.Unlock()
|
|
|
|
// Now create a stronger fork that branches from frame1: 1 -> 2' -> 3' -> 4'
|
|
// This should trigger a reorg back to frame1 (common ancestor)
|
|
frame2Prime := &protobufs.AppShardFrame{
|
|
Header: &protobufs.FrameHeader{
|
|
Address: address,
|
|
FrameNumber: 2,
|
|
Timestamp: 2900, // Earlier timestamp
|
|
Difficulty: 120,
|
|
Output: []byte("frame2_prime"),
|
|
ParentSelector: computeAppPoseidonHash(frame1.Header.Output),
|
|
Prover: []byte("prover2"),
|
|
PublicKeySignatureBls48581: &protobufs.BLS48581AggregateSignature{
|
|
Bitmask: []byte{0b11111111}, // 8 signers (much stronger)
|
|
},
|
|
},
|
|
}
|
|
|
|
frame3Prime := &protobufs.AppShardFrame{
|
|
Header: &protobufs.FrameHeader{
|
|
Address: address,
|
|
FrameNumber: 3,
|
|
Timestamp: 3900,
|
|
Difficulty: 130,
|
|
Output: []byte("frame3_prime"),
|
|
ParentSelector: computeAppPoseidonHash(frame2Prime.Header.Output),
|
|
Prover: []byte("prover2"),
|
|
PublicKeySignatureBls48581: &protobufs.BLS48581AggregateSignature{
|
|
Bitmask: []byte{0b11111111}, // 8 signers
|
|
},
|
|
},
|
|
}
|
|
|
|
frame4Prime := &protobufs.AppShardFrame{
|
|
Header: &protobufs.FrameHeader{
|
|
Address: address,
|
|
FrameNumber: 4,
|
|
Timestamp: 4900,
|
|
Difficulty: 140,
|
|
Output: []byte("frame4_prime"),
|
|
ParentSelector: computeAppPoseidonHash(frame3Prime.Header.Output),
|
|
Prover: []byte("prover2"),
|
|
PublicKeySignatureBls48581: &protobufs.BLS48581AggregateSignature{
|
|
Bitmask: []byte{0b11111111}, // 8 signers
|
|
},
|
|
},
|
|
}
|
|
|
|
// Insert stronger fork - this should trigger a reorganization
|
|
for _, frame := range []*protobufs.AppShardFrame{frame2Prime, frame3Prime, frame4Prime} {
|
|
err = atr.Insert(frame)
|
|
require.NoError(t, err)
|
|
time.Sleep(50 * time.Millisecond) // Allow events to propagate
|
|
}
|
|
|
|
// Verify final head is the stronger branch
|
|
head, err := atr.GetHead()
|
|
require.NoError(t, err)
|
|
assert.Equal(t, uint64(4), head.Header.FrameNumber)
|
|
assert.Equal(t, []byte("frame4_prime"), head.Header.Output)
|
|
|
|
// Wait for all events to be processed
|
|
time.Sleep(100 * time.Millisecond)
|
|
|
|
// Check events - should include:
|
|
// 1. Fork detected event indicating the reorg
|
|
// 2. The event should reference the common ancestor (frame1)
|
|
// 3. Should see new head events for frames 2', 3', 4'
|
|
eventsMu.Lock()
|
|
collectedEvents := make([]AppEvent, len(events))
|
|
copy(collectedEvents, events)
|
|
eventsMu.Unlock()
|
|
|
|
t.Logf("Collected %d events after fork insertion", len(collectedEvents))
|
|
for i, event := range collectedEvents {
|
|
t.Logf("Event %d: Type=%d, Message=%s", i, event.Type, event.Message)
|
|
}
|
|
|
|
// Find the fork detected event
|
|
forkEventFound := false
|
|
newHeadEventsCount := 0
|
|
for _, event := range collectedEvents {
|
|
if event.Type == TimeReelEventForkDetected {
|
|
forkEventFound = true
|
|
// The message should indicate this is a reorganization
|
|
assert.Contains(t, event.Message, "fork detected", "fork event should mention fork detected")
|
|
// Should have old head reference
|
|
assert.NotNil(t, event.OldHead, "fork event should have old head reference")
|
|
} else if event.Type == TimeReelEventNewHead {
|
|
newHeadEventsCount++
|
|
}
|
|
}
|
|
|
|
assert.True(t, forkEventFound, "should have received a fork detected event")
|
|
assert.GreaterOrEqual(t, newHeadEventsCount, 3, "should have received new head events for the replayed frames")
|
|
}
|
|
|
|
// TestAppTimeReel_ComprehensiveEquivocation tests equivocation detection thoroughly
|
|
func TestAppTimeReel_ComprehensiveEquivocation(t *testing.T) {
|
|
logger, _ := zap.NewDevelopment()
|
|
address := []byte("test_app_address")
|
|
s := setupTestClockStore(t)
|
|
atr, err := NewAppTimeReel(logger, address, createTestProverRegistry(true), s, 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 equivocation events
|
|
var equivocationEvents []AppEvent
|
|
var eventsMu sync.Mutex
|
|
go func() {
|
|
for event := range eventCh {
|
|
if event.Type == TimeReelEventEquivocationDetected {
|
|
eventsMu.Lock()
|
|
equivocationEvents = append(equivocationEvents, event)
|
|
eventsMu.Unlock()
|
|
}
|
|
}
|
|
}()
|
|
|
|
// Insert genesis
|
|
genesis := &protobufs.AppShardFrame{
|
|
Header: &protobufs.FrameHeader{
|
|
Address: address,
|
|
FrameNumber: 0,
|
|
Timestamp: 1000,
|
|
Difficulty: 100,
|
|
Output: []byte("genesis"),
|
|
ParentSelector: []byte{},
|
|
Prover: []byte("prover1"),
|
|
},
|
|
}
|
|
|
|
err = atr.Insert(genesis)
|
|
require.NoError(t, err)
|
|
|
|
// Insert valid frame 1
|
|
frame1Valid := &protobufs.AppShardFrame{
|
|
Header: &protobufs.FrameHeader{
|
|
Address: address,
|
|
FrameNumber: 1,
|
|
Timestamp: 2000,
|
|
Difficulty: 110,
|
|
Output: []byte("frame1_valid"),
|
|
ParentSelector: computeAppPoseidonHash(genesis.Header.Output),
|
|
Prover: []byte("prover1"),
|
|
PublicKeySignatureBls48581: &protobufs.BLS48581AggregateSignature{
|
|
Bitmask: []byte{0b00001111}, // Signers 0,1,2,3
|
|
},
|
|
},
|
|
}
|
|
|
|
err = atr.Insert(frame1Valid)
|
|
require.NoError(t, err)
|
|
|
|
// Test Case 1: Complete overlap - same signers, different content
|
|
frame1Equivocation1 := &protobufs.AppShardFrame{
|
|
Header: &protobufs.FrameHeader{
|
|
Address: address,
|
|
FrameNumber: 1,
|
|
Timestamp: 2000,
|
|
Difficulty: 110,
|
|
Output: []byte("frame1_evil_complete"), // Different output
|
|
ParentSelector: computeAppPoseidonHash(genesis.Header.Output),
|
|
Prover: []byte("prover1"),
|
|
PublicKeySignatureBls48581: &protobufs.BLS48581AggregateSignature{
|
|
Bitmask: []byte{0b00001111}, // Same signers 0,1,2,3
|
|
},
|
|
},
|
|
}
|
|
|
|
err = atr.Insert(frame1Equivocation1)
|
|
assert.NoError(t, err)
|
|
|
|
// Test Case 2: Partial overlap - some same signers
|
|
frame1Equivocation2 := &protobufs.AppShardFrame{
|
|
Header: &protobufs.FrameHeader{
|
|
Address: address,
|
|
FrameNumber: 1,
|
|
Timestamp: 2000,
|
|
Difficulty: 110,
|
|
Output: []byte("frame1_evil_partial"),
|
|
ParentSelector: computeAppPoseidonHash(genesis.Header.Output),
|
|
Prover: []byte("prover2"),
|
|
PublicKeySignatureBls48581: &protobufs.BLS48581AggregateSignature{
|
|
Bitmask: []byte{0b11000011}, // Signers 0,1,6,7 - overlap with 0,1
|
|
},
|
|
},
|
|
}
|
|
|
|
err = atr.Insert(frame1Equivocation2)
|
|
assert.NoError(t, err)
|
|
|
|
// Test Case 3: No overlap - should be allowed (fork)
|
|
frame1Fork := &protobufs.AppShardFrame{
|
|
Header: &protobufs.FrameHeader{
|
|
Address: address,
|
|
FrameNumber: 1,
|
|
Timestamp: 2000,
|
|
Difficulty: 110,
|
|
Output: []byte("frame1_fork"),
|
|
ParentSelector: computeAppPoseidonHash(genesis.Header.Output),
|
|
Prover: []byte("prover3"),
|
|
PublicKeySignatureBls48581: &protobufs.BLS48581AggregateSignature{
|
|
Bitmask: []byte{0b00110000}, // Signers 4,5 - no overlap
|
|
},
|
|
},
|
|
}
|
|
|
|
err = atr.Insert(frame1Fork)
|
|
assert.NoError(t, err, "should allow fork with no overlapping signers")
|
|
|
|
// Wait for events to be processed
|
|
time.Sleep(100 * time.Millisecond)
|
|
|
|
// Verify we received exactly 2 equivocation events
|
|
eventsMu.Lock()
|
|
equivCount := len(equivocationEvents)
|
|
eventsMu.Unlock()
|
|
|
|
assert.Equal(t, 3, equivCount, "should have received exactly 3 equivocation events")
|
|
|
|
// Verify event details
|
|
if len(equivocationEvents) >= 3 {
|
|
for i, event := range equivocationEvents {
|
|
assert.Equal(t, TimeReelEventEquivocationDetected, event.Type)
|
|
assert.Contains(t, event.Message, "equivocation at frame 1")
|
|
assert.NotNil(t, event.Frame)
|
|
assert.Equal(t, uint64(1), event.Frame.Header.FrameNumber)
|
|
t.Logf("Equivocation event %d: %s", i+1, event.Message)
|
|
}
|
|
}
|
|
}
|
|
|
|
// TestAppTimeReel_ProverRegistryForkChoice tests that fork choice prefers frames from expected provers
|
|
func TestAppTimeReel_ProverRegistryForkChoice(t *testing.T) {
|
|
logger, _ := zap.NewDevelopment()
|
|
address := []byte("test_app_address")
|
|
|
|
// Create a prover registry that expects specific provers
|
|
proverRegistry := new(mocks.MockProverRegistry)
|
|
|
|
// We'll compute the parent selector from genesis output
|
|
genesisOutput := []byte("genesis")
|
|
computedParentSelector := computeAppPoseidonHash(genesisOutput)
|
|
var parentSelector [32]byte
|
|
copy(parentSelector[:], computedParentSelector)
|
|
|
|
t.Logf("Genesis output: %x", genesisOutput)
|
|
t.Logf("Computed parent selector: %x", parentSelector)
|
|
|
|
// For genesis parent selector, prover1 is the expected prover
|
|
proverRegistry.On("GetNextProver", parentSelector, mock.Anything).Return([]byte("prover1"), nil)
|
|
proverRegistry.On("GetOrderedProvers", parentSelector, mock.Anything).Return([][]byte{[]byte("prover1")}, nil)
|
|
// Default behavior for unknown parent selectors
|
|
proverRegistry.On("GetNextProver", mock.Anything, mock.Anything).Return(nil, errors.New("unknown parent selector"))
|
|
proverRegistry.On("GetOrderedProvers", mock.Anything, mock.Anything).Return(nil, errors.New("unknown parent selector"))
|
|
|
|
s := setupTestClockStore(t)
|
|
atr, err := NewAppTimeReel(logger, address, proverRegistry, s, 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()
|
|
|
|
// Create genesis frame
|
|
genesis := &protobufs.AppShardFrame{
|
|
Header: &protobufs.FrameHeader{
|
|
Address: address,
|
|
FrameNumber: 0,
|
|
Timestamp: 1000,
|
|
Difficulty: 100,
|
|
Output: []byte("genesis"),
|
|
},
|
|
}
|
|
|
|
err = atr.Insert(genesis)
|
|
require.NoError(t, err)
|
|
|
|
// Drain genesis event
|
|
select {
|
|
case <-eventCh:
|
|
case <-time.After(100 * time.Millisecond):
|
|
t.Fatal("timeout waiting for genesis event")
|
|
}
|
|
|
|
// Create two competing frames at frame 1
|
|
frame1a := &protobufs.AppShardFrame{
|
|
Header: &protobufs.FrameHeader{
|
|
Address: address,
|
|
FrameNumber: 1,
|
|
Timestamp: 2000,
|
|
Difficulty: 100,
|
|
ParentSelector: computeAppPoseidonHash(genesis.Header.Output),
|
|
Output: []byte("frame1a"),
|
|
Prover: []byte("prover1"), // Expected prover
|
|
PublicKeySignatureBls48581: &protobufs.BLS48581AggregateSignature{
|
|
Signature: make([]byte, 74),
|
|
PublicKey: &protobufs.BLS48581G2PublicKey{
|
|
KeyValue: make([]byte, 585),
|
|
},
|
|
Bitmask: []byte{0xff, 0xff}, // 16 signers
|
|
},
|
|
},
|
|
}
|
|
|
|
frame1b := &protobufs.AppShardFrame{
|
|
Header: &protobufs.FrameHeader{
|
|
Address: address,
|
|
FrameNumber: 1,
|
|
Timestamp: 2000,
|
|
Difficulty: 100,
|
|
ParentSelector: computeAppPoseidonHash(genesis.Header.Output),
|
|
Output: []byte("frame1b"),
|
|
Prover: []byte("wrong_prover"), // Not the expected prover
|
|
PublicKeySignatureBls48581: &protobufs.BLS48581AggregateSignature{
|
|
Signature: make([]byte, 74),
|
|
PublicKey: &protobufs.BLS48581G2PublicKey{
|
|
KeyValue: make([]byte, 585),
|
|
},
|
|
Bitmask: []byte{0x00, 0x00, 0xff}, // Completely different signers to avoid equivocation
|
|
},
|
|
},
|
|
}
|
|
|
|
// Insert frame with wrong prover first
|
|
err = atr.Insert(frame1b)
|
|
require.NoError(t, err)
|
|
|
|
// Should become head initially
|
|
select {
|
|
case event := <-eventCh:
|
|
assert.Equal(t, TimeReelEventNewHead, event.Type)
|
|
assert.Equal(t, frame1b.Header.Output, event.Frame.Header.Output)
|
|
case <-time.After(100 * time.Millisecond):
|
|
t.Fatal("timeout waiting for frame1b event")
|
|
}
|
|
|
|
// Insert frame with correct prover
|
|
err = atr.Insert(frame1a)
|
|
require.NoError(t, err)
|
|
|
|
// Should trigger fork choice and frame1a should win
|
|
select {
|
|
case event := <-eventCh:
|
|
assert.Equal(t, TimeReelEventForkDetected, event.Type)
|
|
assert.Equal(t, frame1a.Header.Output, event.Frame.Header.Output)
|
|
assert.Equal(t, frame1b.Header.Output, event.OldHead.Header.Output)
|
|
case <-time.After(100 * time.Millisecond):
|
|
t.Fatal("timeout waiting for fork event")
|
|
}
|
|
|
|
// Verify head is now frame1a
|
|
head, err := atr.GetHead()
|
|
require.NoError(t, err)
|
|
assert.Equal(t, frame1a.Header.Output, head.Header.Output)
|
|
}
|
|
|
|
// TestAppTimeReel_ProverRegistryWithOrderedProvers tests fork choice with ordered provers
|
|
func TestAppTimeReel_ProverRegistryWithOrderedProvers(t *testing.T) {
|
|
logger, _ := zap.NewDevelopment()
|
|
address := []byte("test_app_address")
|
|
|
|
// Create a prover registry that returns ordered provers
|
|
proverRegistry := new(mocks.MockProverRegistry)
|
|
|
|
// For parent1, prover1 is the primary, prover2 is secondary, prover3 is tertiary
|
|
orderedProvers := [][]byte{
|
|
[]byte("prover1"),
|
|
[]byte("prover2"),
|
|
[]byte("prover3"),
|
|
}
|
|
|
|
// The parent selector gets computed from genesis output
|
|
genesisOutput := []byte("genesis")
|
|
computedParentSelector := computeAppPoseidonHash(genesisOutput)
|
|
var parentSelector [32]byte
|
|
copy(parentSelector[:], computedParentSelector)
|
|
|
|
proverRegistry.On("GetNextProver", parentSelector, mock.Anything).Return([]byte("prover1"), nil)
|
|
proverRegistry.On("GetOrderedProvers", parentSelector, mock.Anything).Return(orderedProvers, nil)
|
|
// For any other parent selector (like empty for genesis), return a default
|
|
proverRegistry.On("GetNextProver", mock.Anything, mock.Anything).Return([]byte("default_prover"), nil)
|
|
proverRegistry.On("GetOrderedProvers", mock.Anything, mock.Anything).Return([][]byte{[]byte("default_prover")}, nil)
|
|
|
|
s := setupTestClockStore(t)
|
|
atr, err := NewAppTimeReel(logger, address, proverRegistry, s, true)
|
|
require.NoError(t, err)
|
|
|
|
ctx, cancel, _ := lifecycle.WithSignallerAndCancel(context.Background())
|
|
go atr.Start(ctx, func() {})
|
|
time.Sleep(100 * time.Millisecond)
|
|
defer cancel()
|
|
|
|
// Create genesis frame
|
|
genesis := &protobufs.AppShardFrame{
|
|
Header: &protobufs.FrameHeader{
|
|
Address: address,
|
|
FrameNumber: 0,
|
|
Timestamp: 1000,
|
|
Difficulty: 100,
|
|
Output: []byte("genesis"),
|
|
},
|
|
}
|
|
|
|
err = atr.Insert(genesis)
|
|
require.NoError(t, err)
|
|
|
|
// Create three competing frames with different provers from the ordered list
|
|
frame1a := &protobufs.AppShardFrame{
|
|
Header: &protobufs.FrameHeader{
|
|
Address: address,
|
|
FrameNumber: 1,
|
|
Timestamp: 2000,
|
|
Difficulty: 100,
|
|
ParentSelector: computeAppPoseidonHash(genesis.Header.Output),
|
|
Output: []byte("frame1a"),
|
|
Prover: []byte("prover3"), // Tertiary prover - highest distance
|
|
PublicKeySignatureBls48581: &protobufs.BLS48581AggregateSignature{
|
|
Signature: make([]byte, 74),
|
|
PublicKey: &protobufs.BLS48581G2PublicKey{
|
|
KeyValue: make([]byte, 585),
|
|
},
|
|
Bitmask: []byte{0x00, 0x0f}, // Different signers to avoid equivocation
|
|
},
|
|
},
|
|
}
|
|
|
|
frame1b := &protobufs.AppShardFrame{
|
|
Header: &protobufs.FrameHeader{
|
|
Address: address,
|
|
FrameNumber: 1,
|
|
Timestamp: 2000,
|
|
Difficulty: 100,
|
|
ParentSelector: computeAppPoseidonHash(genesis.Header.Output),
|
|
Output: []byte("frame1b"),
|
|
Prover: []byte("prover2"), // Secondary prover - medium distance
|
|
PublicKeySignatureBls48581: &protobufs.BLS48581AggregateSignature{
|
|
Signature: make([]byte, 74),
|
|
PublicKey: &protobufs.BLS48581G2PublicKey{
|
|
KeyValue: make([]byte, 585),
|
|
},
|
|
Bitmask: []byte{0x0f, 0x00}, // Different signers to avoid equivocation
|
|
},
|
|
},
|
|
}
|
|
|
|
frame1c := &protobufs.AppShardFrame{
|
|
Header: &protobufs.FrameHeader{
|
|
Address: address,
|
|
FrameNumber: 1,
|
|
Timestamp: 2000,
|
|
Difficulty: 100,
|
|
ParentSelector: computeAppPoseidonHash(genesis.Header.Output),
|
|
Output: []byte("frame1c"),
|
|
Prover: []byte("prover1"), // Primary prover - lowest distance (best)
|
|
PublicKeySignatureBls48581: &protobufs.BLS48581AggregateSignature{
|
|
Signature: make([]byte, 74),
|
|
PublicKey: &protobufs.BLS48581G2PublicKey{
|
|
KeyValue: make([]byte, 585),
|
|
},
|
|
Bitmask: []byte{0xf0, 0x00}, // Different signers to avoid equivocation
|
|
},
|
|
},
|
|
}
|
|
|
|
// Get event channel to monitor fork events
|
|
eventCh := atr.GetEventCh()
|
|
|
|
// Insert in reverse order of preference
|
|
t.Logf("Inserting frame1a with prover: %s", frame1a.Header.Prover)
|
|
err = atr.Insert(frame1a)
|
|
require.NoError(t, err)
|
|
|
|
// Drain events for frame1a
|
|
drainEvents := func(label string) {
|
|
for {
|
|
select {
|
|
case event := <-eventCh:
|
|
t.Logf("Event after %s: Type=%d, Frame=%s", label, event.Type, event.Frame.Header.Output)
|
|
case <-time.After(50 * time.Millisecond):
|
|
t.Logf("No more events after %s", label)
|
|
return
|
|
}
|
|
}
|
|
}
|
|
|
|
drainEvents("frame1a")
|
|
|
|
// Check head after frame1a
|
|
head1, _ := atr.GetHead()
|
|
t.Logf("Head after frame1a: %s", head1.Header.Output)
|
|
|
|
t.Logf("Inserting frame1b with prover: %s", frame1b.Header.Prover)
|
|
err = atr.Insert(frame1b)
|
|
require.NoError(t, err)
|
|
|
|
drainEvents("frame1b")
|
|
|
|
// Check head after frame1b
|
|
head2, _ := atr.GetHead()
|
|
t.Logf("Head after frame1b: %s", head2.Header.Output)
|
|
|
|
t.Logf("Inserting frame1c with prover: %s", frame1c.Header.Prover)
|
|
err = atr.Insert(frame1c)
|
|
require.NoError(t, err)
|
|
|
|
drainEvents("frame1c")
|
|
|
|
// Check head after frame1c
|
|
head3, _ := atr.GetHead()
|
|
t.Logf("Head after frame1c: %s", head3.Header.Output)
|
|
|
|
// After inserting all three frames, fork choice should have selected frame1c
|
|
// because prover1 is the primary prover (distance 0)
|
|
head, err := atr.GetHead()
|
|
require.NoError(t, err)
|
|
t.Logf("Final head output: %s", head.Header.Output)
|
|
t.Logf("Expected output: %s", frame1c.Header.Output)
|
|
|
|
// Verify frame1c is the head
|
|
assert.Equal(t, frame1c.Header.Output, head.Header.Output, "frame1c should be selected as head due to lower prover distance")
|
|
}
|