From 14cd6d7dcc887829dbf93c238e5e3185983cd7bf Mon Sep 17 00:00:00 2001 From: hanish520 Date: Sun, 16 Nov 2025 12:33:13 -0800 Subject: [PATCH 1/8] fixed delayuntil in kauri begin --- protocol/comm/kauri.go | 25 +++++++++++++++++++++---- 1 file changed, 21 insertions(+), 4 deletions(-) diff --git a/protocol/comm/kauri.go b/protocol/comm/kauri.go index fffda5a4..d39806fd 100644 --- a/protocol/comm/kauri.go +++ b/protocol/comm/kauri.go @@ -67,6 +67,9 @@ func NewKauri( eventloop.Register(el, func(event WaitTimerExpiredEvent) { k.onWaitTimerExpired(event) }) + eventloop.Register(el, func(event WaitForConnectedEvent) { + k.onWaitForConnected(event) + }) return k } @@ -84,10 +87,9 @@ func (k *Kauri) Aggregate(proposal *hotstuff.ProposeMsg, pc hotstuff.PartialCert func (k *Kauri) begin(p *hotstuff.ProposeMsg, pc hotstuff.PartialCert) error { if !k.initDone { // TODO(meling): This is not correct use of DelayUntil, see issue #267 - eventloop.DelayUntil[network.ConnectedEvent](k.eventLoop, func() { - if err := k.begin(p, pc); err != nil { - k.logger.Error(err) - } + eventloop.DelayUntil[network.ConnectedEvent](k.eventLoop, WaitForConnectedEvent{ + pc: pc, + p: p, }) return nil } @@ -128,6 +130,16 @@ func (k *Kauri) waitToAggregate() { k.eventLoop.AddEvent(WaitTimerExpiredEvent{currentView: view}) } +// onWaitForConnected is invoked when begin is called before the replica is connected. +func (k *Kauri) onWaitForConnected(event WaitForConnectedEvent) { + k.logger.Debugf("WaitForConnectedEvent: %v", event) + if k.currentView > hotstuff.View(event.p.Block.View()) { + k.logger.Debug("Current view is higher than event view, not starting kauri") + return + } + k.begin(event.p, event.pc) +} + // onContributionRecv is invoked upon receiving the vote for aggregation. func (k *Kauri) onContributionRecv(event kauri.ContributionRecvEvent) { if k.currentView != hotstuff.View(event.Contribution.View) { @@ -196,4 +208,9 @@ type WaitTimerExpiredEvent struct { currentView hotstuff.View } +type WaitForConnectedEvent struct { + pc hotstuff.PartialCert + p *hotstuff.ProposeMsg +} + var _ Communication = (*Kauri)(nil) From b7d28a1ddd52afb204850668b1f723aed4f839f9 Mon Sep 17 00:00:00 2001 From: hanish520 Date: Tue, 18 Nov 2025 01:17:23 -0800 Subject: [PATCH 2/8] added tests generated from copilot and fixed comments --- protocol/comm/kauri.go | 6 +- protocol/comm/kauri_test.go | 493 ++++++++++++++++++++++++++++++++++++ 2 files changed, 497 insertions(+), 2 deletions(-) create mode 100644 protocol/comm/kauri_test.go diff --git a/protocol/comm/kauri.go b/protocol/comm/kauri.go index d39806fd..2ac33557 100644 --- a/protocol/comm/kauri.go +++ b/protocol/comm/kauri.go @@ -86,7 +86,6 @@ func (k *Kauri) Aggregate(proposal *hotstuff.ProposeMsg, pc hotstuff.PartialCert // begin starts dissemination of proposal and aggregation of votes. func (k *Kauri) begin(p *hotstuff.ProposeMsg, pc hotstuff.PartialCert) error { if !k.initDone { - // TODO(meling): This is not correct use of DelayUntil, see issue #267 eventloop.DelayUntil[network.ConnectedEvent](k.eventLoop, WaitForConnectedEvent{ pc: pc, p: p, @@ -137,7 +136,10 @@ func (k *Kauri) onWaitForConnected(event WaitForConnectedEvent) { k.logger.Debug("Current view is higher than event view, not starting kauri") return } - k.begin(event.p, event.pc) + err := k.begin(event.p, event.pc) + if err != nil { + k.logger.Errorf("Failed to begin kauri after connection: %v", err) + } } // onContributionRecv is invoked upon receiving the vote for aggregation. diff --git a/protocol/comm/kauri_test.go b/protocol/comm/kauri_test.go new file mode 100644 index 00000000..e05d7e5e --- /dev/null +++ b/protocol/comm/kauri_test.go @@ -0,0 +1,493 @@ +package comm + +import ( + "bytes" + "context" + "fmt" + "testing" + "time" + + "github.com/relab/hotstuff" + "github.com/relab/hotstuff/core" + "github.com/relab/hotstuff/core/eventloop" + "github.com/relab/hotstuff/core/logging" + clientpb "github.com/relab/hotstuff/internal/proto/clientpb" + hotstuffpb "github.com/relab/hotstuff/internal/proto/hotstuffpb" + kauripb "github.com/relab/hotstuff/internal/proto/kauripb" + "github.com/relab/hotstuff/internal/tree" + "github.com/relab/hotstuff/network" + pcomm "github.com/relab/hotstuff/protocol/comm/kauri" + "github.com/relab/hotstuff/security/blockchain" + "github.com/relab/hotstuff/security/cert" + "github.com/relab/hotstuff/security/crypto" +) + +// dummyQuorumSignature is a tiny QuorumSignature implementation for tests. +type dummyQuorumSignature struct { + b []byte + bf *crypto.Bitfield +} + +func (d *dummyQuorumSignature) ToBytes() []byte { return d.b } +func (d *dummyQuorumSignature) Participants() hotstuff.IDSet { return d.bf } + +// mockAuthority allows configuring Verify and Combine behavior per-test. +type mockAuthority struct { + VerifyFunc func(sig hotstuff.QuorumSignature, msg []byte) error + CombineFunc func(a, b hotstuff.QuorumSignature) (hotstuff.QuorumSignature, error) +} + +func (m *mockAuthority) Verify(sig hotstuff.QuorumSignature, msg []byte) error { + if m.VerifyFunc != nil { + return m.VerifyFunc(sig, msg) + } + return nil +} +func (m *mockAuthority) Combine(a, b hotstuff.QuorumSignature) (hotstuff.QuorumSignature, error) { + if m.CombineFunc != nil { + return m.CombineFunc(a, b) + } + // default: merge participants into a new dummyQuorumSignature + nb := &crypto.Bitfield{} + a.Participants().ForEach(func(id hotstuff.ID) { nb.Add(id) }) + b.Participants().ForEach(func(id hotstuff.ID) { nb.Add(id) }) + return &dummyQuorumSignature{b: []byte("combined"), bf: nb}, nil +} + +// fakeBase implements crypto.Base for injecting into cert.Authority in tests. +type fakeBase struct { + VerifyFunc func(sig hotstuff.QuorumSignature, msg []byte) error + CombineFunc func(signatures ...hotstuff.QuorumSignature) (hotstuff.QuorumSignature, error) +} + +func (f *fakeBase) Sign(message []byte) (hotstuff.QuorumSignature, error) { + return &dummyQuorumSignature{b: message, bf: &crypto.Bitfield{}}, nil +} +func (f *fakeBase) Combine(signatures ...hotstuff.QuorumSignature) (hotstuff.QuorumSignature, error) { + if f.CombineFunc != nil { + return f.CombineFunc(signatures...) + } + nb := &crypto.Bitfield{} + for _, s := range signatures { + s.Participants().ForEach(func(id hotstuff.ID) { nb.Add(id) }) + } + return &dummyQuorumSignature{b: []byte("combined"), bf: nb}, nil +} +func (f *fakeBase) Verify(signature hotstuff.QuorumSignature, message []byte) error { + if f.VerifyFunc != nil { + return f.VerifyFunc(signature, message) + } + return nil +} +func (f *fakeBase) BatchVerify(signature hotstuff.QuorumSignature, batch map[hotstuff.ID][]byte) error { + return nil +} + +// mockKauriSender implements core.KauriSender for tests. +type mockKauriSender struct { + proposeCh chan *hotstuff.ProposeMsg + contribCh chan struct{} + subFunc func(ids []hotstuff.ID) (core.Sender, error) +} + +func (m *mockKauriSender) NewView(id hotstuff.ID, msg hotstuff.SyncInfo) error { + return nil +} + +func (m *mockKauriSender) Vote(id hotstuff.ID, cert hotstuff.PartialCert) error { + return nil +} + +func (m *mockKauriSender) Timeout(msg hotstuff.TimeoutMsg) { +} + +func (m *mockKauriSender) Propose(p *hotstuff.ProposeMsg) { + if m.proposeCh != nil { + m.proposeCh <- p + } +} + +func (m *mockKauriSender) RequestBlock(ctx context.Context, hash hotstuff.Hash) (*hotstuff.Block, bool) { + return nil, false +} + +func (m *mockKauriSender) Sub(ids []hotstuff.ID) (core.Sender, error) { + if m.subFunc != nil { + return m.subFunc(ids) + } + return m, nil +} + +func (m *mockKauriSender) SendContributionToParent(view hotstuff.View, qc hotstuff.QuorumSignature) { + if m.contribCh != nil { + m.contribCh <- struct{}{} + } +} + +// Test that NewKauri panics if no tree is configured. +func TestNewKauriPanicsWhenNoTree(t *testing.T) { + el := eventloop.New(logging.NewWithDest(new(bytes.Buffer), "test"), 4) + cfg := core.NewRuntimeConfig(1, nil) + defer func() { _ = recover() }() + // should panic + _ = NewKauri(logging.NewWithDest(new(bytes.Buffer), "test"), el, cfg, nil, nil, &mockKauriSender{}) +} + +func TestSendProposalToChildren_NoChildren_SendsToParent(t *testing.T) { + el := eventloop.New(logging.NewWithDest(new(bytes.Buffer), "test"), 8) + // single-node tree -> no children + tr := tree.NewSimple(1, 2, []hotstuff.ID{1}) + cfg := core.NewRuntimeConfig(1, nil, core.WithKauriTree(tr)) + // ensure quorum size is 1 by adding one replica + cfg.AddReplica(&hotstuff.ReplicaInfo{ID: 1}) + + sender := &mockKauriSender{contribCh: make(chan struct{}, 1)} + bc := blockchain.New(el, logging.NewWithDest(new(bytes.Buffer), "test"), sender) + // store a block we'll reference + block := hotstuff.NewBlock(hotstuff.GetGenesis().Hash(), hotstuff.NewQuorumCert(nil, 0, hotstuff.GetGenesis().Hash()), &clientpb.Batch{}, 1, 1) + bc.Store(block) + + k := NewKauri(logging.NewWithDest(new(bytes.Buffer), "test"), el, cfg, bc, nil, sender) + // instead of relying on the event loop, directly trigger the wait timer handler + // mark as initialized + k.initDone = true + k.blockHash = block.Hash() + k.currentView = 1 + // set some agg contribution + bf := &crypto.Bitfield{} + bf.Add(1) + k.aggContrib = &dummyQuorumSignature{b: []byte("s"), bf: bf} + + if err := k.sendProposalToChildren(&hotstuff.ProposeMsg{Block: block}); err != nil { + t.Fatalf("unexpected err: %v", err) + } + select { + case <-sender.contribCh: + // ok + case <-time.After(50 * time.Millisecond): + t.Fatalf("expected contribution sent to parent") + } + if !k.aggSent { + t.Fatalf("expected aggSent to be true") + } +} + +func TestSendProposalToChildren_WithChildren_ProposesAndAggregates(t *testing.T) { + el := eventloop.New(logging.NewWithDest(new(bytes.Buffer), "test"), 16) + // two-node tree where node 1 has child 2 + tr := tree.NewSimple(1, 2, []hotstuff.ID{1, 2}) + tr.SetTreeHeightWaitTime(0) + cfg := core.NewRuntimeConfig(1, nil, core.WithKauriTree(tr)) + cfg.AddReplica(&hotstuff.ReplicaInfo{ID: 1}) + cfg.AddReplica(&hotstuff.ReplicaInfo{ID: 2}) + + proposeCh := make(chan *hotstuff.ProposeMsg, 1) + contribCh := make(chan struct{}, 1) + sender := &mockKauriSender{proposeCh: proposeCh, contribCh: contribCh} + // Sub should return a sender that sends on proposeCh + sender.subFunc = func(ids []hotstuff.ID) (core.Sender, error) { return sender, nil } + + bc := blockchain.New(el, logging.NewWithDest(new(bytes.Buffer), "test"), sender) + block := hotstuff.NewBlock(hotstuff.GetGenesis().Hash(), hotstuff.NewQuorumCert(nil, 0, hotstuff.GetGenesis().Hash()), &clientpb.Batch{}, 1, 1) + bc.Store(block) + + k := NewKauri(logging.NewWithDest(new(bytes.Buffer), "test"), el, cfg, bc, nil, sender) + k.initDone = true + k.blockHash = block.Hash() + k.currentView = 1 + bf := &crypto.Bitfield{} + bf.Add(2) + k.aggContrib = &dummyQuorumSignature{b: []byte("s"), bf: bf} + + if err := k.sendProposalToChildren(&hotstuff.ProposeMsg{Block: block}); err != nil { + t.Fatalf("unexpected err: %v", err) + } + // child should receive propose + select { + case <-proposeCh: + case <-time.After(50 * time.Millisecond): + t.Fatalf("expected child propose") + } + // Instead of relying on the event loop, directly call the handler that would be invoked + k.onWaitTimerExpired(WaitTimerExpiredEvent{currentView: k.currentView}) + select { + case <-contribCh: + case <-time.After(200 * time.Millisecond): + t.Fatalf("expected contribution to parent after aggregation wait") + } +} + +func TestMergeContribution_Behaviors(t *testing.T) { + el := eventloop.New(logging.NewWithDest(new(bytes.Buffer), "test"), 8) + tr := tree.NewSimple(1, 2, []hotstuff.ID{1}) + cfg := core.NewRuntimeConfig(1, nil, core.WithKauriTree(tr)) + cfg.AddReplica(&hotstuff.ReplicaInfo{ID: 1}) + sender := &mockKauriSender{contribCh: make(chan struct{}, 1)} + bc := blockchain.New(el, logging.NewWithDest(new(bytes.Buffer), "test"), sender) + + // block not present -> error + k := NewKauri(logging.NewWithDest(new(bytes.Buffer), "test"), el, cfg, bc, nil, sender) + k.blockHash = hotstuff.Hash{} // not stored except genesis + if err := k.mergeContribution(&dummyQuorumSignature{b: []byte("x"), bf: &crypto.Bitfield{}}); err == nil { + t.Fatalf("expected error when block missing") + } + + // now store a block and test Verify failure + block := hotstuff.NewBlock(hotstuff.GetGenesis().Hash(), hotstuff.NewQuorumCert(nil, 0, hotstuff.GetGenesis().Hash()), &clientpb.Batch{}, 2, 1) + bc.Store(block) + // inject authority that fails verify + fbFail := &fakeBase{VerifyFunc: func(sig hotstuff.QuorumSignature, msg []byte) error { return fmt.Errorf("verify failed") }} + k.auth = cert.NewAuthority(cfg, bc, fbFail) + k.blockHash = block.Hash() + if err := k.mergeContribution(&dummyQuorumSignature{b: []byte("x"), bf: &crypto.Bitfield{}}); err == nil { + t.Fatalf("expected verify error") + } + + // success: first contribution sets aggContrib + fbOK := &fakeBase{VerifyFunc: func(sig hotstuff.QuorumSignature, msg []byte) error { return nil }} + k.auth = cert.NewAuthority(cfg, bc, fbOK) + qs := &dummyQuorumSignature{b: []byte("s"), bf: &crypto.Bitfield{}} + qs.Participants().(*crypto.Bitfield).Add(1) + k.aggContrib = nil + k.currentView = 2 + k.blockHash = block.Hash() + if err := k.mergeContribution(qs); err != nil { + t.Fatalf("unexpected err: %v", err) + } + if k.aggContrib == nil { + t.Fatalf("expected aggContrib to be set") + } + + // merging: create another disjoint contribution and ensure Combine used and event emitted when quorum reached + // make quorum size = 1 for brevity by having 1 replica in cfg + cfg = core.NewRuntimeConfig(1, nil, core.WithKauriTree(tr)) + cfg.AddReplica(&hotstuff.ReplicaInfo{ID: 1}) + k.config = cfg + // make combined signature have participant count >= quorum + combined := &dummyQuorumSignature{b: []byte("c"), bf: &crypto.Bitfield{}} + combined.Participants().(*crypto.Bitfield).Add(1) + k.auth = cert.NewAuthority(cfg, bc, &fakeBase{CombineFunc: func(signatures ...hotstuff.QuorumSignature) (hotstuff.QuorumSignature, error) { return combined, nil }, VerifyFunc: func(sig hotstuff.QuorumSignature, msg []byte) error { return nil }}) + + // register handler to capture NewViewMsg event + got := make(chan struct{}, 1) + eventloop.Register(el, func(msg hotstuff.NewViewMsg) { got <- struct{}{} }) + // current aggContrib set to something else + old := &dummyQuorumSignature{b: []byte("old"), bf: &crypto.Bitfield{}} + old.Participants().(*crypto.Bitfield).Add(2) + k.aggContrib = old + // merge another signature + if err := k.mergeContribution(qs); err != nil { + t.Fatalf("unexpected err: %v", err) + } + // process one queued event (the NewViewMsg) manually using Tick + if !el.Tick(context.Background()) { + t.Fatalf("expected event loop to have an event to process") + } + select { + case <-got: + case <-time.After(200 * time.Millisecond): + t.Fatalf("expected NewViewMsg event after aggregation") + } +} + +func TestOnWaitForConnected_NoStartWhenCurrentHigher(t *testing.T) { + el := eventloop.New(logging.NewWithDest(new(bytes.Buffer), "test"), 8) + tr := tree.NewSimple(1, 2, []hotstuff.ID{1, 2}) + cfg := core.NewRuntimeConfig(1, nil, core.WithKauriTree(tr)) + cfg.AddReplica(&hotstuff.ReplicaInfo{ID: 1}) + cfg.AddReplica(&hotstuff.ReplicaInfo{ID: 2}) + + proposeCh := make(chan *hotstuff.ProposeMsg, 1) + sender := &mockKauriSender{proposeCh: proposeCh} + sender.subFunc = func(ids []hotstuff.ID) (core.Sender, error) { return sender, nil } + bc := blockchain.New(el, logging.NewWithDest(new(bytes.Buffer), "test"), sender) + + block := hotstuff.NewBlock(hotstuff.GetGenesis().Hash(), hotstuff.NewQuorumCert(nil, 0, hotstuff.GetGenesis().Hash()), &clientpb.Batch{}, 1, 1) + k := NewKauri(logging.NewWithDest(new(bytes.Buffer), "test"), el, cfg, bc, nil, sender) + k.initDone = true + k.currentView = 5 + + // event has a lower view -> should not start kauri + event := WaitForConnectedEvent{pc: hotstuff.PartialCert{}, p: &hotstuff.ProposeMsg{Block: block}} + k.onWaitForConnected(event) + select { + case <-proposeCh: + t.Fatalf("did not expect proposal to be sent when current view is higher") + default: + // ok + } +} + +func TestOnContributionRecv_ViewMismatch(t *testing.T) { + el := eventloop.New(logging.NewWithDest(new(bytes.Buffer), "test"), 8) + tr := tree.NewSimple(1, 2, []hotstuff.ID{1}) + cfg := core.NewRuntimeConfig(1, nil, core.WithKauriTree(tr)) + sender := &mockKauriSender{contribCh: make(chan struct{}, 1)} + bc := blockchain.New(el, logging.NewWithDest(new(bytes.Buffer), "test"), sender) + k := NewKauri(logging.NewWithDest(new(bytes.Buffer), "test"), el, cfg, bc, nil, sender) + k.currentView = 1 + // create event with different view + ev := pcomm.ContributionRecvEvent{Contribution: &kauripb.Contribution{ID: 2, View: 2}} + k.onContributionRecv(ev) + if len(k.senders) != 0 { + t.Fatalf("expected no senders appended on view mismatch") + } +} + +func TestSendProposalToChildren_SubError(t *testing.T) { + el := eventloop.New(logging.NewWithDest(new(bytes.Buffer), "test"), 8) + tr := tree.NewSimple(1, 2, []hotstuff.ID{1, 2}) + cfg := core.NewRuntimeConfig(1, nil, core.WithKauriTree(tr)) + cfg.AddReplica(&hotstuff.ReplicaInfo{ID: 1}) + cfg.AddReplica(&hotstuff.ReplicaInfo{ID: 2}) + + sender := &mockKauriSender{} + sender.subFunc = func(ids []hotstuff.ID) (core.Sender, error) { return nil, fmt.Errorf("sub failed") } + bc := blockchain.New(el, logging.NewWithDest(new(bytes.Buffer), "test"), sender) + block := hotstuff.NewBlock(hotstuff.GetGenesis().Hash(), hotstuff.NewQuorumCert(nil, 0, hotstuff.GetGenesis().Hash()), &clientpb.Batch{}, 1, 1) + k := NewKauri(logging.NewWithDest(new(bytes.Buffer), "test"), el, cfg, bc, nil, sender) + k.initDone = true + if err := k.sendProposalToChildren(&hotstuff.ProposeMsg{Block: block}); err == nil { + t.Fatalf("expected error when Sub fails") + } +} + +func TestWaitToAggregate_TriggersWaitExpiredAndResets(t *testing.T) { + el := eventloop.New(logging.NewWithDest(new(bytes.Buffer), "test"), 8) + tr := tree.NewSimple(1, 2, []hotstuff.ID{1}) + tr.SetTreeHeightWaitTime(0) + cfg := core.NewRuntimeConfig(1, nil, core.WithKauriTree(tr)) + cfg.AddReplica(&hotstuff.ReplicaInfo{ID: 1}) + contribCh := make(chan struct{}, 1) + sender := &mockKauriSender{contribCh: contribCh} + bc := blockchain.New(el, logging.NewWithDest(new(bytes.Buffer), "test"), sender) + + k := NewKauri(logging.NewWithDest(new(bytes.Buffer), "test"), el, cfg, bc, nil, sender) + k.currentView = 7 + k.aggSent = false + bf := &crypto.Bitfield{} + bf.Add(1) + k.aggContrib = &dummyQuorumSignature{b: []byte("s"), bf: bf} + + // call waitToAggregate directly (uses WaitTime==0) + k.waitToAggregate() + // process the added WaitTimerExpiredEvent + if !el.Tick(context.Background()) { + t.Fatalf("expected event loop to have an event to process") + } + select { + case <-contribCh: + case <-time.After(200 * time.Millisecond): + t.Fatalf("expected SendContributionToParent to be called on wait expiry") + } + if k.aggContrib != nil { + t.Fatalf("expected aggContrib to be reset after wait expiry") + } +} + +func TestDisseminate_DelayedUntilConnected(t *testing.T) { + el := eventloop.New(logging.NewWithDest(new(bytes.Buffer), "test"), 8) + tr := tree.NewSimple(1, 2, []hotstuff.ID{1}) + cfg := core.NewRuntimeConfig(1, nil, core.WithKauriTree(tr)) + cfg.AddReplica(&hotstuff.ReplicaInfo{ID: 1}) + + sender := &mockKauriSender{contribCh: make(chan struct{}, 1)} + bc := blockchain.New(el, logging.NewWithDest(new(bytes.Buffer), "test"), sender) + block := hotstuff.NewBlock(hotstuff.GetGenesis().Hash(), hotstuff.NewQuorumCert(nil, 0, hotstuff.GetGenesis().Hash()), &clientpb.Batch{}, 1, 1) + bc.Store(block) + + k := NewKauri(logging.NewWithDest(new(bytes.Buffer), "test"), el, cfg, bc, nil, sender) + // initDone false by default + qs := &dummyQuorumSignature{b: []byte("s"), bf: &crypto.Bitfield{}} + qs.Participants().(*crypto.Bitfield).Add(1) + pc := hotstuff.NewPartialCert(qs, block.Hash()) + + if err := k.Disseminate(&hotstuff.ProposeMsg{Block: block}, pc); err != nil { + t.Fatalf("unexpected err: %v", err) + } + // nothing sent yet + select { + case <-sender.contribCh: + t.Fatalf("did not expect contribution yet") + default: + } + // now trigger the ConnectedEvent which should release the delayed event + el.AddEvent(network.ConnectedEvent{}) + // simulate that the replica is connected (what the ReplicaConnectedEvent handler would have done) + k.initDone = true + // process connected and delayed WaitForConnectedEvent + if !el.Tick(context.Background()) || !el.Tick(context.Background()) { + t.Fatalf("expected two ticks to process connected and delayed event") + } + select { + case <-sender.contribCh: + case <-time.After(200 * time.Millisecond): + t.Fatalf("expected contribution after connection") + } +} + +func TestAggregate_DelayedUntilConnected(t *testing.T) { + el := eventloop.New(logging.NewWithDest(new(bytes.Buffer), "test"), 8) + tr := tree.NewSimple(1, 2, []hotstuff.ID{1}) + cfg := core.NewRuntimeConfig(1, nil, core.WithKauriTree(tr)) + cfg.AddReplica(&hotstuff.ReplicaInfo{ID: 1}) + + sender := &mockKauriSender{contribCh: make(chan struct{}, 1)} + bc := blockchain.New(el, logging.NewWithDest(new(bytes.Buffer), "test"), sender) + block := hotstuff.NewBlock(hotstuff.GetGenesis().Hash(), hotstuff.NewQuorumCert(nil, 0, hotstuff.GetGenesis().Hash()), &clientpb.Batch{}, 1, 1) + bc.Store(block) + + k := NewKauri(logging.NewWithDest(new(bytes.Buffer), "test"), el, cfg, bc, nil, sender) + qs := &dummyQuorumSignature{b: []byte("s"), bf: &crypto.Bitfield{}} + qs.Participants().(*crypto.Bitfield).Add(1) + pc := hotstuff.NewPartialCert(qs, block.Hash()) + + if err := k.Aggregate(&hotstuff.ProposeMsg{Block: block}, pc); err != nil { + t.Fatalf("unexpected err: %v", err) + } + el.AddEvent(network.ConnectedEvent{}) + k.initDone = true + if !el.Tick(context.Background()) || !el.Tick(context.Background()) { + t.Fatalf("expected two ticks to process connected and delayed event") + } + select { + case <-sender.contribCh: + case <-time.After(200 * time.Millisecond): + t.Fatalf("expected contribution after connection") + } +} + +func TestOnContributionRecv_IsSubSetCallsParent(t *testing.T) { + el := eventloop.New(logging.NewWithDest(new(bytes.Buffer), "test"), 8) + tr := tree.NewSimple(1, 2, []hotstuff.ID{1, 2}) + cfg := core.NewRuntimeConfig(1, nil, core.WithKauriTree(tr)) + cfg.AddReplica(&hotstuff.ReplicaInfo{ID: 1}) + cfg.AddReplica(&hotstuff.ReplicaInfo{ID: 2}) + + contribCh := make(chan struct{}, 1) + sender := &mockKauriSender{contribCh: contribCh} + bc := blockchain.New(el, logging.NewWithDest(new(bytes.Buffer), "test"), sender) + block := hotstuff.NewBlock(hotstuff.GetGenesis().Hash(), hotstuff.NewQuorumCert(nil, 0, hotstuff.GetGenesis().Hash()), &clientpb.Batch{}, 1, 1) + bc.Store(block) + + // authority that accepts any signature + k := NewKauri(logging.NewWithDest(new(bytes.Buffer), "test"), el, cfg, bc, cert.NewAuthority(cfg, bc, &fakeBase{}), sender) + k.currentView = 1 + k.blockHash = block.Hash() + + // construct a proto QuorumSignature with ECDSA sig so QuorumSignatureFromProto returns non-nil + protoSig := &hotstuffpb.QuorumSignature{} + ecd := &hotstuffpb.ECDSAMultiSignature{Sigs: []*hotstuffpb.ECDSASignature{{Signer: uint32(2), Sig: []byte("sig")}}} + protoSig.Sig = &hotstuffpb.QuorumSignature_ECDSASigs{ECDSASigs: ecd} + + ev := pcomm.ContributionRecvEvent{Contribution: &kauripb.Contribution{ID: 2, View: uint64(k.currentView), Signature: protoSig}} + k.onContributionRecv(ev) + select { + case <-contribCh: + case <-time.After(200 * time.Millisecond): + t.Fatalf("expected SendContributionToParent to be invoked when subtree satisfied") + } + if !k.aggSent { + t.Fatalf("expected aggSent to be true after subtree aggregation") + } +} From f730d45b4ac75ac6ac721fd58bb9eaf09154d035 Mon Sep 17 00:00:00 2001 From: Hein Meling Date: Wed, 26 Nov 2025 16:38:45 -0800 Subject: [PATCH 3/8] refactor: contribution handling to use kauripb.Contribution directly This removes the container kauri.ContributionRecvEvent which only wrapped the kauripb.Contribution message; we can simply pass the protobuf message directly via the event loop since events can be of 'any' type. --- protocol/comm/kauri.go | 8 ++++---- protocol/comm/kauri/service.go | 7 +------ 2 files changed, 5 insertions(+), 10 deletions(-) diff --git a/protocol/comm/kauri.go b/protocol/comm/kauri.go index 2ac33557..329834b2 100644 --- a/protocol/comm/kauri.go +++ b/protocol/comm/kauri.go @@ -9,6 +9,7 @@ import ( "github.com/relab/hotstuff/core/eventloop" "github.com/relab/hotstuff/core/logging" "github.com/relab/hotstuff/internal/proto/hotstuffpb" + "github.com/relab/hotstuff/internal/proto/kauripb" "github.com/relab/hotstuff/internal/tree" "github.com/relab/hotstuff/network" "github.com/relab/hotstuff/protocol/comm/kauri" @@ -61,7 +62,7 @@ func NewKauri( eventloop.Register(el, func(_ hotstuff.ReplicaConnectedEvent) { k.initDone = true // signal that we are connected }) - eventloop.Register(el, func(event kauri.ContributionRecvEvent) { + eventloop.Register(el, func(event *kauripb.Contribution) { k.onContributionRecv(event) }) eventloop.Register(el, func(event WaitTimerExpiredEvent) { @@ -143,11 +144,10 @@ func (k *Kauri) onWaitForConnected(event WaitForConnectedEvent) { } // onContributionRecv is invoked upon receiving the vote for aggregation. -func (k *Kauri) onContributionRecv(event kauri.ContributionRecvEvent) { - if k.currentView != hotstuff.View(event.Contribution.View) { +func (k *Kauri) onContributionRecv(contribution *kauripb.Contribution) { + if k.currentView != hotstuff.View(contribution.View) { return } - contribution := event.Contribution k.logger.Debugf("Processing the contribution from %d", contribution.ID) currentSignature := hotstuffpb.QuorumSignatureFromProto(contribution.Signature) err := k.mergeContribution(currentSignature) diff --git a/protocol/comm/kauri/service.go b/protocol/comm/kauri/service.go index d55ffabb..5a9bd40c 100644 --- a/protocol/comm/kauri/service.go +++ b/protocol/comm/kauri/service.go @@ -20,10 +20,5 @@ func RegisterService( } func (i kauriServiceImpl) SendContribution(_ gorums.ServerCtx, request *kauripb.Contribution) { - i.eventLoop.AddEvent(ContributionRecvEvent{Contribution: request}) -} - -// ContributionRecvEvent is raised when a contribution is received. -type ContributionRecvEvent struct { - Contribution *kauripb.Contribution + i.eventLoop.AddEvent(request) } From cd832672578fe029d0de018b256f1b1b7ac4c4b2 Mon Sep 17 00:00:00 2001 From: Hein Meling Date: Wed, 26 Nov 2025 20:55:32 -0800 Subject: [PATCH 4/8] refactor: simplify kauri_test.go --- protocol/comm/kauri_test.go | 94 ++++++++++++++----------------------- 1 file changed, 34 insertions(+), 60 deletions(-) diff --git a/protocol/comm/kauri_test.go b/protocol/comm/kauri_test.go index e05d7e5e..f833e22c 100644 --- a/protocol/comm/kauri_test.go +++ b/protocol/comm/kauri_test.go @@ -16,13 +16,12 @@ import ( kauripb "github.com/relab/hotstuff/internal/proto/kauripb" "github.com/relab/hotstuff/internal/tree" "github.com/relab/hotstuff/network" - pcomm "github.com/relab/hotstuff/protocol/comm/kauri" "github.com/relab/hotstuff/security/blockchain" "github.com/relab/hotstuff/security/cert" "github.com/relab/hotstuff/security/crypto" ) -// dummyQuorumSignature is a tiny QuorumSignature implementation for tests. +// dummyQuorumSignature is a minimal QuorumSignature implementation for tests. type dummyQuorumSignature struct { b []byte bf *crypto.Bitfield @@ -31,27 +30,13 @@ type dummyQuorumSignature struct { func (d *dummyQuorumSignature) ToBytes() []byte { return d.b } func (d *dummyQuorumSignature) Participants() hotstuff.IDSet { return d.bf } -// mockAuthority allows configuring Verify and Combine behavior per-test. -type mockAuthority struct { - VerifyFunc func(sig hotstuff.QuorumSignature, msg []byte) error - CombineFunc func(a, b hotstuff.QuorumSignature) (hotstuff.QuorumSignature, error) -} - -func (m *mockAuthority) Verify(sig hotstuff.QuorumSignature, msg []byte) error { - if m.VerifyFunc != nil { - return m.VerifyFunc(sig, msg) - } - return nil -} -func (m *mockAuthority) Combine(a, b hotstuff.QuorumSignature) (hotstuff.QuorumSignature, error) { - if m.CombineFunc != nil { - return m.CombineFunc(a, b) +// newDummyQuorumSignature creates a new dummyQuorumSignature with the given participants. +func newDummyQuorumSignature(b []byte, participants ...hotstuff.ID) *dummyQuorumSignature { + bf := &crypto.Bitfield{} + for _, id := range participants { + bf.Add(id) } - // default: merge participants into a new dummyQuorumSignature - nb := &crypto.Bitfield{} - a.Participants().ForEach(func(id hotstuff.ID) { nb.Add(id) }) - b.Participants().ForEach(func(id hotstuff.ID) { nb.Add(id) }) - return &dummyQuorumSignature{b: []byte("combined"), bf: nb}, nil + return &dummyQuorumSignature{b: b, bf: bf} } // fakeBase implements crypto.Base for injecting into cert.Authority in tests. @@ -63,6 +48,7 @@ type fakeBase struct { func (f *fakeBase) Sign(message []byte) (hotstuff.QuorumSignature, error) { return &dummyQuorumSignature{b: message, bf: &crypto.Bitfield{}}, nil } + func (f *fakeBase) Combine(signatures ...hotstuff.QuorumSignature) (hotstuff.QuorumSignature, error) { if f.CombineFunc != nil { return f.CombineFunc(signatures...) @@ -73,12 +59,14 @@ func (f *fakeBase) Combine(signatures ...hotstuff.QuorumSignature) (hotstuff.Quo } return &dummyQuorumSignature{b: []byte("combined"), bf: nb}, nil } + func (f *fakeBase) Verify(signature hotstuff.QuorumSignature, message []byte) error { if f.VerifyFunc != nil { return f.VerifyFunc(signature, message) } return nil } + func (f *fakeBase) BatchVerify(signature hotstuff.QuorumSignature, batch map[hotstuff.ID][]byte) error { return nil } @@ -154,9 +142,7 @@ func TestSendProposalToChildren_NoChildren_SendsToParent(t *testing.T) { k.blockHash = block.Hash() k.currentView = 1 // set some agg contribution - bf := &crypto.Bitfield{} - bf.Add(1) - k.aggContrib = &dummyQuorumSignature{b: []byte("s"), bf: bf} + k.aggContrib = newDummyQuorumSignature([]byte("s"), 1) if err := k.sendProposalToChildren(&hotstuff.ProposeMsg{Block: block}); err != nil { t.Fatalf("unexpected err: %v", err) @@ -195,9 +181,7 @@ func TestSendProposalToChildren_WithChildren_ProposesAndAggregates(t *testing.T) k.initDone = true k.blockHash = block.Hash() k.currentView = 1 - bf := &crypto.Bitfield{} - bf.Add(2) - k.aggContrib = &dummyQuorumSignature{b: []byte("s"), bf: bf} + k.aggContrib = newDummyQuorumSignature([]byte("s"), 2) if err := k.sendProposalToChildren(&hotstuff.ProposeMsg{Block: block}); err != nil { t.Fatalf("unexpected err: %v", err) @@ -228,7 +212,7 @@ func TestMergeContribution_Behaviors(t *testing.T) { // block not present -> error k := NewKauri(logging.NewWithDest(new(bytes.Buffer), "test"), el, cfg, bc, nil, sender) k.blockHash = hotstuff.Hash{} // not stored except genesis - if err := k.mergeContribution(&dummyQuorumSignature{b: []byte("x"), bf: &crypto.Bitfield{}}); err == nil { + if err := k.mergeContribution(newDummyQuorumSignature([]byte("x"))); err == nil { t.Fatalf("expected error when block missing") } @@ -239,15 +223,14 @@ func TestMergeContribution_Behaviors(t *testing.T) { fbFail := &fakeBase{VerifyFunc: func(sig hotstuff.QuorumSignature, msg []byte) error { return fmt.Errorf("verify failed") }} k.auth = cert.NewAuthority(cfg, bc, fbFail) k.blockHash = block.Hash() - if err := k.mergeContribution(&dummyQuorumSignature{b: []byte("x"), bf: &crypto.Bitfield{}}); err == nil { + if err := k.mergeContribution(newDummyQuorumSignature([]byte("x"))); err == nil { t.Fatalf("expected verify error") } // success: first contribution sets aggContrib fbOK := &fakeBase{VerifyFunc: func(sig hotstuff.QuorumSignature, msg []byte) error { return nil }} k.auth = cert.NewAuthority(cfg, bc, fbOK) - qs := &dummyQuorumSignature{b: []byte("s"), bf: &crypto.Bitfield{}} - qs.Participants().(*crypto.Bitfield).Add(1) + qs := newDummyQuorumSignature([]byte("s"), 1) k.aggContrib = nil k.currentView = 2 k.blockHash = block.Hash() @@ -264,25 +247,22 @@ func TestMergeContribution_Behaviors(t *testing.T) { cfg.AddReplica(&hotstuff.ReplicaInfo{ID: 1}) k.config = cfg // make combined signature have participant count >= quorum - combined := &dummyQuorumSignature{b: []byte("c"), bf: &crypto.Bitfield{}} - combined.Participants().(*crypto.Bitfield).Add(1) + combined := newDummyQuorumSignature([]byte("c"), 1) k.auth = cert.NewAuthority(cfg, bc, &fakeBase{CombineFunc: func(signatures ...hotstuff.QuorumSignature) (hotstuff.QuorumSignature, error) { return combined, nil }, VerifyFunc: func(sig hotstuff.QuorumSignature, msg []byte) error { return nil }}) // register handler to capture NewViewMsg event got := make(chan struct{}, 1) eventloop.Register(el, func(msg hotstuff.NewViewMsg) { got <- struct{}{} }) // current aggContrib set to something else - old := &dummyQuorumSignature{b: []byte("old"), bf: &crypto.Bitfield{}} - old.Participants().(*crypto.Bitfield).Add(2) + old := newDummyQuorumSignature([]byte("old"), 2) k.aggContrib = old // merge another signature if err := k.mergeContribution(qs); err != nil { t.Fatalf("unexpected err: %v", err) } + // process one queued event (the NewViewMsg) manually using Tick - if !el.Tick(context.Background()) { - t.Fatalf("expected event loop to have an event to process") - } + el.Tick(context.Background()) select { case <-got: case <-time.After(200 * time.Millisecond): @@ -326,9 +306,8 @@ func TestOnContributionRecv_ViewMismatch(t *testing.T) { bc := blockchain.New(el, logging.NewWithDest(new(bytes.Buffer), "test"), sender) k := NewKauri(logging.NewWithDest(new(bytes.Buffer), "test"), el, cfg, bc, nil, sender) k.currentView = 1 - // create event with different view - ev := pcomm.ContributionRecvEvent{Contribution: &kauripb.Contribution{ID: 2, View: 2}} - k.onContributionRecv(ev) + // create contribution with different view + k.onContributionRecv(&kauripb.Contribution{ID: 2, View: 2}) if len(k.senders) != 0 { t.Fatalf("expected no senders appended on view mismatch") } @@ -365,16 +344,13 @@ func TestWaitToAggregate_TriggersWaitExpiredAndResets(t *testing.T) { k := NewKauri(logging.NewWithDest(new(bytes.Buffer), "test"), el, cfg, bc, nil, sender) k.currentView = 7 k.aggSent = false - bf := &crypto.Bitfield{} - bf.Add(1) - k.aggContrib = &dummyQuorumSignature{b: []byte("s"), bf: bf} + k.aggContrib = newDummyQuorumSignature([]byte("s"), 1) // call waitToAggregate directly (uses WaitTime==0) k.waitToAggregate() + // process the added WaitTimerExpiredEvent - if !el.Tick(context.Background()) { - t.Fatalf("expected event loop to have an event to process") - } + el.Tick(context.Background()) select { case <-contribCh: case <-time.After(200 * time.Millisecond): @@ -398,8 +374,7 @@ func TestDisseminate_DelayedUntilConnected(t *testing.T) { k := NewKauri(logging.NewWithDest(new(bytes.Buffer), "test"), el, cfg, bc, nil, sender) // initDone false by default - qs := &dummyQuorumSignature{b: []byte("s"), bf: &crypto.Bitfield{}} - qs.Participants().(*crypto.Bitfield).Add(1) + qs := newDummyQuorumSignature([]byte("s"), 1) pc := hotstuff.NewPartialCert(qs, block.Hash()) if err := k.Disseminate(&hotstuff.ProposeMsg{Block: block}, pc); err != nil { @@ -415,10 +390,10 @@ func TestDisseminate_DelayedUntilConnected(t *testing.T) { el.AddEvent(network.ConnectedEvent{}) // simulate that the replica is connected (what the ReplicaConnectedEvent handler would have done) k.initDone = true + // process connected and delayed WaitForConnectedEvent - if !el.Tick(context.Background()) || !el.Tick(context.Background()) { - t.Fatalf("expected two ticks to process connected and delayed event") - } + el.Tick(context.Background()) + el.Tick(context.Background()) select { case <-sender.contribCh: case <-time.After(200 * time.Millisecond): @@ -438,8 +413,7 @@ func TestAggregate_DelayedUntilConnected(t *testing.T) { bc.Store(block) k := NewKauri(logging.NewWithDest(new(bytes.Buffer), "test"), el, cfg, bc, nil, sender) - qs := &dummyQuorumSignature{b: []byte("s"), bf: &crypto.Bitfield{}} - qs.Participants().(*crypto.Bitfield).Add(1) + qs := newDummyQuorumSignature([]byte("s"), 1) pc := hotstuff.NewPartialCert(qs, block.Hash()) if err := k.Aggregate(&hotstuff.ProposeMsg{Block: block}, pc); err != nil { @@ -447,9 +421,10 @@ func TestAggregate_DelayedUntilConnected(t *testing.T) { } el.AddEvent(network.ConnectedEvent{}) k.initDone = true - if !el.Tick(context.Background()) || !el.Tick(context.Background()) { - t.Fatalf("expected two ticks to process connected and delayed event") - } + + // process connected and delayed WaitForConnectedEvent + el.Tick(context.Background()) + el.Tick(context.Background()) select { case <-sender.contribCh: case <-time.After(200 * time.Millisecond): @@ -480,8 +455,7 @@ func TestOnContributionRecv_IsSubSetCallsParent(t *testing.T) { ecd := &hotstuffpb.ECDSAMultiSignature{Sigs: []*hotstuffpb.ECDSASignature{{Signer: uint32(2), Sig: []byte("sig")}}} protoSig.Sig = &hotstuffpb.QuorumSignature_ECDSASigs{ECDSASigs: ecd} - ev := pcomm.ContributionRecvEvent{Contribution: &kauripb.Contribution{ID: 2, View: uint64(k.currentView), Signature: protoSig}} - k.onContributionRecv(ev) + k.onContributionRecv(&kauripb.Contribution{ID: 2, View: uint64(k.currentView), Signature: protoSig}) select { case <-contribCh: case <-time.After(200 * time.Millisecond): From 17587595457d037b608670970db7f393f1c0d8be Mon Sep 17 00:00:00 2001 From: Hein Meling Date: Tue, 27 Jan 2026 23:05:24 +0100 Subject: [PATCH 5/8] feat: add support for KauriSender interface to MockSender --- internal/testutil/mocksender.go | 26 ++++++++++++++++++++++---- 1 file changed, 22 insertions(+), 4 deletions(-) diff --git a/internal/testutil/mocksender.go b/internal/testutil/mocksender.go index 3134ddec..0e9bbf8c 100644 --- a/internal/testutil/mocksender.go +++ b/internal/testutil/mocksender.go @@ -10,11 +10,18 @@ import ( "github.com/relab/hotstuff/security/blockchain" ) +// ContributionMsg represents a contribution sent to parent in Kauri protocol. +type ContributionMsg struct { + View hotstuff.View + QC hotstuff.QuorumSignature +} + type MockSender struct { - id hotstuff.ID - recipients []hotstuff.ID - messagesSent []any - blockChains []*blockchain.Blockchain + id hotstuff.ID + recipients []hotstuff.ID + messagesSent []any + contributions []ContributionMsg + blockChains []*blockchain.Blockchain } // NewMockSender returns a mock implementation of core.Sender that @@ -111,7 +118,18 @@ func (m *MockSender) Sub(ids []hotstuff.ID) (core.Sender, error) { }, nil } +// SendContributionToParent stores a contribution message for Kauri protocol testing. +func (m *MockSender) SendContributionToParent(view hotstuff.View, qc hotstuff.QuorumSignature) { + m.contributions = append(m.contributions, ContributionMsg{View: view, QC: qc}) +} + +// ContributionsSent returns a slice of contributions sent by the mock sender. +func (m *MockSender) ContributionsSent() []ContributionMsg { + return m.contributions +} + var _ core.Sender = (*MockSender)(nil) +var _ core.KauriSender = (*MockSender)(nil) func isSubset(a, b []hotstuff.ID) bool { set := make(map[hotstuff.ID]struct{}, len(a)) From 111785243f9bb16d1237dc514a1daf8b737ea5ee Mon Sep 17 00:00:00 2001 From: Hein Meling Date: Tue, 27 Jan 2026 23:32:36 +0100 Subject: [PATCH 6/8] test: refactored the kauri tests --- protocol/comm/kauri_test.go | 857 +++++++++++++++++++----------------- 1 file changed, 462 insertions(+), 395 deletions(-) diff --git a/protocol/comm/kauri_test.go b/protocol/comm/kauri_test.go index f833e22c..a5b4fef7 100644 --- a/protocol/comm/kauri_test.go +++ b/protocol/comm/kauri_test.go @@ -1,467 +1,534 @@ -package comm +package comm_test import ( - "bytes" "context" - "fmt" "testing" "time" "github.com/relab/hotstuff" "github.com/relab/hotstuff/core" "github.com/relab/hotstuff/core/eventloop" - "github.com/relab/hotstuff/core/logging" - clientpb "github.com/relab/hotstuff/internal/proto/clientpb" - hotstuffpb "github.com/relab/hotstuff/internal/proto/hotstuffpb" - kauripb "github.com/relab/hotstuff/internal/proto/kauripb" + "github.com/relab/hotstuff/internal/proto/clientpb" + "github.com/relab/hotstuff/internal/test" + "github.com/relab/hotstuff/internal/testutil" "github.com/relab/hotstuff/internal/tree" "github.com/relab/hotstuff/network" - "github.com/relab/hotstuff/security/blockchain" - "github.com/relab/hotstuff/security/cert" + "github.com/relab/hotstuff/protocol/comm" "github.com/relab/hotstuff/security/crypto" ) -// dummyQuorumSignature is a minimal QuorumSignature implementation for tests. -type dummyQuorumSignature struct { - b []byte - bf *crypto.Bitfield +// fullTreeSize calculates the number of nodes in a full tree with the given +// branch factor and 3 levels (root, intermediate, leaves). +func fullTreeSize(bf int) int { + return 1 + bf + bf*bf // level 0 + level 1 + level 2 } -func (d *dummyQuorumSignature) ToBytes() []byte { return d.b } -func (d *dummyQuorumSignature) Participants() hotstuff.IDSet { return d.bf } - -// newDummyQuorumSignature creates a new dummyQuorumSignature with the given participants. -func newDummyQuorumSignature(b []byte, participants ...hotstuff.ID) *dummyQuorumSignature { - bf := &crypto.Bitfield{} - for _, id := range participants { - bf.Add(id) - } - return &dummyQuorumSignature{b: b, bf: bf} +// treeConfig represents a tree configuration for table-driven tests. +type treeConfig struct { + branchFactor int + size int } -// fakeBase implements crypto.Base for injecting into cert.Authority in tests. -type fakeBase struct { - VerifyFunc func(sig hotstuff.QuorumSignature, msg []byte) error - CombineFunc func(signatures ...hotstuff.QuorumSignature) (hotstuff.QuorumSignature, error) +// standardTreeConfigs returns tree configurations for full 3-level trees +// with branch factors 2, 3, 4, and 5. +func standardTreeConfigs() []treeConfig { + return []treeConfig{ + {branchFactor: 2, size: fullTreeSize(2)}, // 7 nodes + {branchFactor: 3, size: fullTreeSize(3)}, // 13 nodes + {branchFactor: 4, size: fullTreeSize(4)}, // 21 nodes + {branchFactor: 5, size: fullTreeSize(5)}, // 31 nodes + } } -func (f *fakeBase) Sign(message []byte) (hotstuff.QuorumSignature, error) { - return &dummyQuorumSignature{b: message, bf: &crypto.Bitfield{}}, nil +// kauriTestSetup contains all components needed for Kauri tests. +type kauriTestSetup struct { + essentials *testutil.Essentials + kauri *comm.Kauri + tree *tree.Tree + sender *testutil.MockSender } -func (f *fakeBase) Combine(signatures ...hotstuff.QuorumSignature) (hotstuff.QuorumSignature, error) { - if f.CombineFunc != nil { - return f.CombineFunc(signatures...) +// wireUpKauri creates a Kauri instance with the specified configuration. +// It returns a setup struct containing the Kauri instance and related components. +func wireUpKauri( + t testing.TB, + id hotstuff.ID, + treeSize, branchFactor int, +) *kauriTestSetup { + t.Helper() + + treePositions := tree.DefaultTreePos(treeSize) + tr := tree.NewSimple(id, branchFactor, treePositions) + tr.SetTreeHeightWaitTime(0) // disable wait time for tests + + essentials := testutil.WireUpEssentials( + t, id, crypto.NameECDSA, + core.WithKauriTree(tr), + ) + + // Add all replicas to the configuration + for _, pos := range treePositions { + if pos != id { + essentials.RuntimeCfg().AddReplica(&hotstuff.ReplicaInfo{ID: pos}) + } } - nb := &crypto.Bitfield{} - for _, s := range signatures { - s.Participants().ForEach(func(id hotstuff.ID) { nb.Add(id) }) - } - return &dummyQuorumSignature{b: []byte("combined"), bf: nb}, nil -} -func (f *fakeBase) Verify(signature hotstuff.QuorumSignature, message []byte) error { - if f.VerifyFunc != nil { - return f.VerifyFunc(signature, message) + // Create a mock sender with all replica IDs as potential recipients + sender := testutil.NewMockSender(id, treePositions...) + + k := comm.NewKauri( + essentials.Logger(), + essentials.EventLoop(), + essentials.RuntimeCfg(), + essentials.Blockchain(), + essentials.Authority(), + sender, + ) + + return &kauriTestSetup{ + essentials: essentials, + kauri: k, + tree: tr, + sender: sender, } - return nil -} - -func (f *fakeBase) BatchVerify(signature hotstuff.QuorumSignature, batch map[hotstuff.ID][]byte) error { - return nil -} - -// mockKauriSender implements core.KauriSender for tests. -type mockKauriSender struct { - proposeCh chan *hotstuff.ProposeMsg - contribCh chan struct{} - subFunc func(ids []hotstuff.ID) (core.Sender, error) } -func (m *mockKauriSender) NewView(id hotstuff.ID, msg hotstuff.SyncInfo) error { - return nil -} - -func (m *mockKauriSender) Vote(id hotstuff.ID, cert hotstuff.PartialCert) error { - return nil +// simulateConnection simulates the replica being connected by triggering +// the ReplicaConnectedEvent through the event loop. +func simulateConnection(t testing.TB, setup *kauriTestSetup) { + t.Helper() + el := setup.essentials.EventLoop() + el.AddEvent(hotstuff.ReplicaConnectedEvent{}) + el.Tick(context.Background()) } -func (m *mockKauriSender) Timeout(msg hotstuff.TimeoutMsg) { +// createProposal creates a test proposal with a block based on genesis. +func createProposal(t testing.TB, essentials *testutil.Essentials, proposerID hotstuff.ID) *hotstuff.ProposeMsg { + t.Helper() + block := testutil.CreateBlock(t, essentials.Authority()) + return &hotstuff.ProposeMsg{ + ID: proposerID, + Block: block, + } } -func (m *mockKauriSender) Propose(p *hotstuff.ProposeMsg) { - if m.proposeCh != nil { - m.proposeCh <- p +// waitForEvent registers a handler for event type T and ticks the event loop +// until the event is received or timeout occurs. This is preferred over +// arbitrary tick counts. +func waitForEvent[T any](t testing.TB, el *eventloop.EventLoop, timeout time.Duration) { + t.Helper() + received := make(chan struct{}) + unregister := eventloop.Register(el, func(_ T) { + close(received) + }) + defer unregister() + + deadline := time.Now().Add(timeout) + for { + select { + case <-received: + return + default: + if time.Now().After(deadline) { + t.Fatalf("timeout waiting for event %T", *new(T)) + } + el.Tick(context.Background()) + time.Sleep(time.Millisecond) // yield to allow goroutines to run + } } } -func (m *mockKauriSender) RequestBlock(ctx context.Context, hash hotstuff.Hash) (*hotstuff.Block, bool) { - return nil, false +// TestNewKauriPanic tests that NewKauri panics when no tree is configured. +func TestNewKauriPanic(t *testing.T) { + defer func() { + if r := recover(); r == nil { + t.Error("NewKauri should panic when no tree is configured") + } + }() + + essentials := testutil.WireUpEssentials(t, 1, crypto.NameECDSA) + // This should panic because no tree is configured + _ = comm.NewKauri( + essentials.Logger(), + essentials.EventLoop(), + essentials.RuntimeCfg(), + essentials.Blockchain(), + essentials.Authority(), + essentials.MockSender(), + ) } -func (m *mockKauriSender) Sub(ids []hotstuff.ID) (core.Sender, error) { - if m.subFunc != nil { - return m.subFunc(ids) +// TestDisseminateFromLeaf tests that a leaf node sends its contribution +// directly to the parent without forwarding to children. +func TestDisseminateFromLeaf(t *testing.T) { + for _, tc := range standardTreeConfigs() { + t.Run(test.Name("bf", tc.branchFactor, "size", tc.size), func(t *testing.T) { + // Use the last node (a leaf) as the test replica + leafID := hotstuff.ID(tc.size) + setup := wireUpKauri(t, leafID, tc.size, tc.branchFactor) + simulateConnection(t, setup) + + proposal := createProposal(t, setup.essentials, 1) + setup.essentials.Blockchain().Store(proposal.Block) + pc := testutil.CreatePC(t, proposal.Block, setup.essentials.Authority()) + + err := setup.kauri.Disseminate(proposal, pc) + if err != nil { + t.Fatalf("Disseminate failed: %v", err) + } + + // Leaf should send contribution to parent, not propose to children + contributions := setup.sender.ContributionsSent() + if len(contributions) != 1 { + t.Errorf("expected 1 contribution from leaf, got %d", len(contributions)) + } + + // Verify no proposals were sent (leaf has no children) + messages := setup.sender.MessagesSent() + for _, msg := range messages { + if _, ok := msg.(hotstuff.ProposeMsg); ok { + t.Error("leaf node should not send proposals") + } + } + }) } - return m, nil } -func (m *mockKauriSender) SendContributionToParent(view hotstuff.View, qc hotstuff.QuorumSignature) { - if m.contribCh != nil { - m.contribCh <- struct{}{} +// TestDisseminateFromIntermediate tests that an intermediate node with children +// starts the aggregation timer and eventually sends its contribution to parent. +func TestDisseminateFromIntermediate(t *testing.T) { + for _, tc := range standardTreeConfigs() { + t.Run(test.Name("bf", tc.branchFactor, "size", tc.size), func(t *testing.T) { + // Use node 2 (first child of root, has its own children) + intermediateID := hotstuff.ID(2) + setup := wireUpKauri(t, intermediateID, tc.size, tc.branchFactor) + simulateConnection(t, setup) + + proposal := createProposal(t, setup.essentials, 1) + setup.essentials.Blockchain().Store(proposal.Block) + pc := testutil.CreatePC(t, proposal.Block, setup.essentials.Authority()) + + // Verify this node has children (precondition) + children := setup.tree.ReplicaChildren() + if len(children) == 0 { + t.Error("intermediate node should have children") + } + + err := setup.kauri.Disseminate(proposal, pc) + if err != nil { + t.Fatalf("Disseminate failed: %v", err) + } + + // No contribution yet (waiting for children) + contributions := setup.sender.ContributionsSent() + if len(contributions) != 0 { + t.Errorf("expected no contributions immediately, got %d", len(contributions)) + } + + // Wait for WaitTimerExpiredEvent to be processed + waitForEvent[comm.WaitTimerExpiredEvent](t, setup.essentials.EventLoop(), 100*time.Millisecond) + + // Now contribution should be sent (timer expired) + contributions = setup.sender.ContributionsSent() + if len(contributions) != 1 { + t.Errorf("expected 1 contribution after timer expiry, got %d", len(contributions)) + } + }) } } -// Test that NewKauri panics if no tree is configured. -func TestNewKauriPanicsWhenNoTree(t *testing.T) { - el := eventloop.New(logging.NewWithDest(new(bytes.Buffer), "test"), 4) - cfg := core.NewRuntimeConfig(1, nil) - defer func() { _ = recover() }() - // should panic - _ = NewKauri(logging.NewWithDest(new(bytes.Buffer), "test"), el, cfg, nil, nil, &mockKauriSender{}) -} - -func TestSendProposalToChildren_NoChildren_SendsToParent(t *testing.T) { - el := eventloop.New(logging.NewWithDest(new(bytes.Buffer), "test"), 8) - // single-node tree -> no children - tr := tree.NewSimple(1, 2, []hotstuff.ID{1}) - cfg := core.NewRuntimeConfig(1, nil, core.WithKauriTree(tr)) - // ensure quorum size is 1 by adding one replica - cfg.AddReplica(&hotstuff.ReplicaInfo{ID: 1}) - - sender := &mockKauriSender{contribCh: make(chan struct{}, 1)} - bc := blockchain.New(el, logging.NewWithDest(new(bytes.Buffer), "test"), sender) - // store a block we'll reference - block := hotstuff.NewBlock(hotstuff.GetGenesis().Hash(), hotstuff.NewQuorumCert(nil, 0, hotstuff.GetGenesis().Hash()), &clientpb.Batch{}, 1, 1) - bc.Store(block) - - k := NewKauri(logging.NewWithDest(new(bytes.Buffer), "test"), el, cfg, bc, nil, sender) - // instead of relying on the event loop, directly trigger the wait timer handler - // mark as initialized - k.initDone = true - k.blockHash = block.Hash() - k.currentView = 1 - // set some agg contribution - k.aggContrib = newDummyQuorumSignature([]byte("s"), 1) - - if err := k.sendProposalToChildren(&hotstuff.ProposeMsg{Block: block}); err != nil { - t.Fatalf("unexpected err: %v", err) - } - select { - case <-sender.contribCh: - // ok - case <-time.After(50 * time.Millisecond): - t.Fatalf("expected contribution sent to parent") - } - if !k.aggSent { - t.Fatalf("expected aggSent to be true") +// TestDisseminateFromRoot tests that the root node with children +// starts the aggregation timer and eventually sends its contribution to parent. +func TestDisseminateFromRoot(t *testing.T) { + for _, tc := range standardTreeConfigs() { + t.Run(test.Name("bf", tc.branchFactor, "size", tc.size), func(t *testing.T) { + rootID := hotstuff.ID(1) + setup := wireUpKauri(t, rootID, tc.size, tc.branchFactor) + simulateConnection(t, setup) + + proposal := createProposal(t, setup.essentials, rootID) + setup.essentials.Blockchain().Store(proposal.Block) + pc := testutil.CreatePC(t, proposal.Block, setup.essentials.Authority()) + + // Verify root has children (precondition for multi-node trees) + children := setup.tree.ReplicaChildren() + if len(children) == 0 { + t.Error("root should have children in multi-node tree") + } + + err := setup.kauri.Disseminate(proposal, pc) + if err != nil { + t.Fatalf("Disseminate failed: %v", err) + } + + // No contribution yet (waiting for children) + contributions := setup.sender.ContributionsSent() + if len(contributions) != 0 { + t.Errorf("expected no contributions immediately, got %d", len(contributions)) + } + + // Wait for WaitTimerExpiredEvent to be processed + waitForEvent[comm.WaitTimerExpiredEvent](t, setup.essentials.EventLoop(), 100*time.Millisecond) + + // Now contribution should be sent (timer expired) + contributions = setup.sender.ContributionsSent() + if len(contributions) != 1 { + t.Errorf("expected 1 contribution after timer expiry, got %d", len(contributions)) + } + }) } } -func TestSendProposalToChildren_WithChildren_ProposesAndAggregates(t *testing.T) { - el := eventloop.New(logging.NewWithDest(new(bytes.Buffer), "test"), 16) - // two-node tree where node 1 has child 2 - tr := tree.NewSimple(1, 2, []hotstuff.ID{1, 2}) - tr.SetTreeHeightWaitTime(0) - cfg := core.NewRuntimeConfig(1, nil, core.WithKauriTree(tr)) - cfg.AddReplica(&hotstuff.ReplicaInfo{ID: 1}) - cfg.AddReplica(&hotstuff.ReplicaInfo{ID: 2}) - - proposeCh := make(chan *hotstuff.ProposeMsg, 1) - contribCh := make(chan struct{}, 1) - sender := &mockKauriSender{proposeCh: proposeCh, contribCh: contribCh} - // Sub should return a sender that sends on proposeCh - sender.subFunc = func(ids []hotstuff.ID) (core.Sender, error) { return sender, nil } - - bc := blockchain.New(el, logging.NewWithDest(new(bytes.Buffer), "test"), sender) - block := hotstuff.NewBlock(hotstuff.GetGenesis().Hash(), hotstuff.NewQuorumCert(nil, 0, hotstuff.GetGenesis().Hash()), &clientpb.Batch{}, 1, 1) - bc.Store(block) - - k := NewKauri(logging.NewWithDest(new(bytes.Buffer), "test"), el, cfg, bc, nil, sender) - k.initDone = true - k.blockHash = block.Hash() - k.currentView = 1 - k.aggContrib = newDummyQuorumSignature([]byte("s"), 2) - - if err := k.sendProposalToChildren(&hotstuff.ProposeMsg{Block: block}); err != nil { - t.Fatalf("unexpected err: %v", err) - } - // child should receive propose - select { - case <-proposeCh: - case <-time.After(50 * time.Millisecond): - t.Fatalf("expected child propose") - } - // Instead of relying on the event loop, directly call the handler that would be invoked - k.onWaitTimerExpired(WaitTimerExpiredEvent{currentView: k.currentView}) - select { - case <-contribCh: - case <-time.After(200 * time.Millisecond): - t.Fatalf("expected contribution to parent after aggregation wait") +// TestAggregateFromLeaf tests that a leaf node sends its vote contribution +// to its parent. +func TestAggregateFromLeaf(t *testing.T) { + for _, tc := range standardTreeConfigs() { + t.Run(test.Name("bf", tc.branchFactor, "size", tc.size), func(t *testing.T) { + leafID := hotstuff.ID(tc.size) + setup := wireUpKauri(t, leafID, tc.size, tc.branchFactor) + simulateConnection(t, setup) + + proposal := createProposal(t, setup.essentials, 1) + setup.essentials.Blockchain().Store(proposal.Block) + pc := testutil.CreatePC(t, proposal.Block, setup.essentials.Authority()) + + err := setup.kauri.Aggregate(proposal, pc) + if err != nil { + t.Fatalf("Aggregate failed: %v", err) + } + + // Leaf should send contribution to parent + contributions := setup.sender.ContributionsSent() + if len(contributions) != 1 { + t.Errorf("expected 1 contribution from leaf, got %d", len(contributions)) + } + + // Verify the contribution has the correct view + if contributions[0].View != proposal.Block.View() { + t.Errorf("contribution view = %d, want %d", contributions[0].View, proposal.Block.View()) + } + }) } } -func TestMergeContribution_Behaviors(t *testing.T) { - el := eventloop.New(logging.NewWithDest(new(bytes.Buffer), "test"), 8) - tr := tree.NewSimple(1, 2, []hotstuff.ID{1}) - cfg := core.NewRuntimeConfig(1, nil, core.WithKauriTree(tr)) - cfg.AddReplica(&hotstuff.ReplicaInfo{ID: 1}) - sender := &mockKauriSender{contribCh: make(chan struct{}, 1)} - bc := blockchain.New(el, logging.NewWithDest(new(bytes.Buffer), "test"), sender) - - // block not present -> error - k := NewKauri(logging.NewWithDest(new(bytes.Buffer), "test"), el, cfg, bc, nil, sender) - k.blockHash = hotstuff.Hash{} // not stored except genesis - if err := k.mergeContribution(newDummyQuorumSignature([]byte("x"))); err == nil { - t.Fatalf("expected error when block missing") - } - - // now store a block and test Verify failure - block := hotstuff.NewBlock(hotstuff.GetGenesis().Hash(), hotstuff.NewQuorumCert(nil, 0, hotstuff.GetGenesis().Hash()), &clientpb.Batch{}, 2, 1) - bc.Store(block) - // inject authority that fails verify - fbFail := &fakeBase{VerifyFunc: func(sig hotstuff.QuorumSignature, msg []byte) error { return fmt.Errorf("verify failed") }} - k.auth = cert.NewAuthority(cfg, bc, fbFail) - k.blockHash = block.Hash() - if err := k.mergeContribution(newDummyQuorumSignature([]byte("x"))); err == nil { - t.Fatalf("expected verify error") - } - - // success: first contribution sets aggContrib - fbOK := &fakeBase{VerifyFunc: func(sig hotstuff.QuorumSignature, msg []byte) error { return nil }} - k.auth = cert.NewAuthority(cfg, bc, fbOK) - qs := newDummyQuorumSignature([]byte("s"), 1) - k.aggContrib = nil - k.currentView = 2 - k.blockHash = block.Hash() - if err := k.mergeContribution(qs); err != nil { - t.Fatalf("unexpected err: %v", err) - } - if k.aggContrib == nil { - t.Fatalf("expected aggContrib to be set") - } - - // merging: create another disjoint contribution and ensure Combine used and event emitted when quorum reached - // make quorum size = 1 for brevity by having 1 replica in cfg - cfg = core.NewRuntimeConfig(1, nil, core.WithKauriTree(tr)) - cfg.AddReplica(&hotstuff.ReplicaInfo{ID: 1}) - k.config = cfg - // make combined signature have participant count >= quorum - combined := newDummyQuorumSignature([]byte("c"), 1) - k.auth = cert.NewAuthority(cfg, bc, &fakeBase{CombineFunc: func(signatures ...hotstuff.QuorumSignature) (hotstuff.QuorumSignature, error) { return combined, nil }, VerifyFunc: func(sig hotstuff.QuorumSignature, msg []byte) error { return nil }}) - - // register handler to capture NewViewMsg event - got := make(chan struct{}, 1) - eventloop.Register(el, func(msg hotstuff.NewViewMsg) { got <- struct{}{} }) - // current aggContrib set to something else - old := newDummyQuorumSignature([]byte("old"), 2) - k.aggContrib = old - // merge another signature - if err := k.mergeContribution(qs); err != nil { - t.Fatalf("unexpected err: %v", err) - } - - // process one queued event (the NewViewMsg) manually using Tick - el.Tick(context.Background()) - select { - case <-got: - case <-time.After(200 * time.Millisecond): - t.Fatalf("expected NewViewMsg event after aggregation") +// TestDisseminateDelayedUntilConnected tests that Disseminate waits for +// connection before processing. +func TestDisseminateDelayedUntilConnected(t *testing.T) { + for _, tc := range standardTreeConfigs() { + t.Run(test.Name("bf", tc.branchFactor, "size", tc.size), func(t *testing.T) { + leafID := hotstuff.ID(tc.size) + setup := wireUpKauri(t, leafID, tc.size, tc.branchFactor) + + // Do NOT simulate connection yet + proposal := createProposal(t, setup.essentials, 1) + setup.essentials.Blockchain().Store(proposal.Block) + pc := testutil.CreatePC(t, proposal.Block, setup.essentials.Authority()) + + err := setup.kauri.Disseminate(proposal, pc) + if err != nil { + t.Fatalf("Disseminate failed: %v", err) + } + + // Nothing should be sent yet + contributions := setup.sender.ContributionsSent() + if len(contributions) != 0 { + t.Errorf("expected no contributions before connection, got %d", len(contributions)) + } + + // Simulate connection: ReplicaConnectedEvent first (sets initDone=true), + // then ConnectedEvent (triggers the delayed WaitForConnectedEvent) + el := setup.essentials.EventLoop() + el.AddEvent(hotstuff.ReplicaConnectedEvent{}) + el.Tick(context.Background()) // Process ReplicaConnectedEvent + + el.AddEvent(network.ConnectedEvent{}) + // Wait for WaitForConnectedEvent to be processed + waitForEvent[comm.WaitForConnectedEvent](t, el, 100*time.Millisecond) + + // Now contribution should be sent + contributions = setup.sender.ContributionsSent() + if len(contributions) != 1 { + t.Errorf("expected 1 contribution after connection, got %d", len(contributions)) + } + }) } } -func TestOnWaitForConnected_NoStartWhenCurrentHigher(t *testing.T) { - el := eventloop.New(logging.NewWithDest(new(bytes.Buffer), "test"), 8) - tr := tree.NewSimple(1, 2, []hotstuff.ID{1, 2}) - cfg := core.NewRuntimeConfig(1, nil, core.WithKauriTree(tr)) - cfg.AddReplica(&hotstuff.ReplicaInfo{ID: 1}) - cfg.AddReplica(&hotstuff.ReplicaInfo{ID: 2}) - - proposeCh := make(chan *hotstuff.ProposeMsg, 1) - sender := &mockKauriSender{proposeCh: proposeCh} - sender.subFunc = func(ids []hotstuff.ID) (core.Sender, error) { return sender, nil } - bc := blockchain.New(el, logging.NewWithDest(new(bytes.Buffer), "test"), sender) - - block := hotstuff.NewBlock(hotstuff.GetGenesis().Hash(), hotstuff.NewQuorumCert(nil, 0, hotstuff.GetGenesis().Hash()), &clientpb.Batch{}, 1, 1) - k := NewKauri(logging.NewWithDest(new(bytes.Buffer), "test"), el, cfg, bc, nil, sender) - k.initDone = true - k.currentView = 5 - - // event has a lower view -> should not start kauri - event := WaitForConnectedEvent{pc: hotstuff.PartialCert{}, p: &hotstuff.ProposeMsg{Block: block}} - k.onWaitForConnected(event) - select { - case <-proposeCh: - t.Fatalf("did not expect proposal to be sent when current view is higher") - default: - // ok +// TestAggregateDelayedUntilConnected tests that Aggregate waits for +// connection before processing. +func TestAggregateDelayedUntilConnected(t *testing.T) { + for _, tc := range standardTreeConfigs() { + t.Run(test.Name("bf", tc.branchFactor, "size", tc.size), func(t *testing.T) { + leafID := hotstuff.ID(tc.size) + setup := wireUpKauri(t, leafID, tc.size, tc.branchFactor) + + // Do NOT simulate connection yet + proposal := createProposal(t, setup.essentials, 1) + setup.essentials.Blockchain().Store(proposal.Block) + pc := testutil.CreatePC(t, proposal.Block, setup.essentials.Authority()) + + err := setup.kauri.Aggregate(proposal, pc) + if err != nil { + t.Fatalf("Aggregate failed: %v", err) + } + + // Nothing should be sent yet + contributions := setup.sender.ContributionsSent() + if len(contributions) != 0 { + t.Errorf("expected no contributions before connection, got %d", len(contributions)) + } + + // Simulate connection: ReplicaConnectedEvent first (sets initDone=true), + // then ConnectedEvent (triggers the delayed WaitForConnectedEvent) + el := setup.essentials.EventLoop() + el.AddEvent(hotstuff.ReplicaConnectedEvent{}) + el.Tick(context.Background()) // Process ReplicaConnectedEvent + + el.AddEvent(network.ConnectedEvent{}) + // Wait for WaitForConnectedEvent to be processed + waitForEvent[comm.WaitForConnectedEvent](t, el, 100*time.Millisecond) + + // Now contribution should be sent + contributions = setup.sender.ContributionsSent() + if len(contributions) != 1 { + t.Errorf("expected 1 contribution after connection, got %d", len(contributions)) + } + }) } } -func TestOnContributionRecv_ViewMismatch(t *testing.T) { - el := eventloop.New(logging.NewWithDest(new(bytes.Buffer), "test"), 8) - tr := tree.NewSimple(1, 2, []hotstuff.ID{1}) - cfg := core.NewRuntimeConfig(1, nil, core.WithKauriTree(tr)) - sender := &mockKauriSender{contribCh: make(chan struct{}, 1)} - bc := blockchain.New(el, logging.NewWithDest(new(bytes.Buffer), "test"), sender) - k := NewKauri(logging.NewWithDest(new(bytes.Buffer), "test"), el, cfg, bc, nil, sender) - k.currentView = 1 - // create contribution with different view - k.onContributionRecv(&kauripb.Contribution{ID: 2, View: 2}) - if len(k.senders) != 0 { - t.Fatalf("expected no senders appended on view mismatch") - } -} +// TestSingleNodeTree tests behavior when there is only one node in the tree. +func TestSingleNodeTree(t *testing.T) { + setup := wireUpKauri(t, 1, 1, 2) + simulateConnection(t, setup) -func TestSendProposalToChildren_SubError(t *testing.T) { - el := eventloop.New(logging.NewWithDest(new(bytes.Buffer), "test"), 8) - tr := tree.NewSimple(1, 2, []hotstuff.ID{1, 2}) - cfg := core.NewRuntimeConfig(1, nil, core.WithKauriTree(tr)) - cfg.AddReplica(&hotstuff.ReplicaInfo{ID: 1}) - cfg.AddReplica(&hotstuff.ReplicaInfo{ID: 2}) - - sender := &mockKauriSender{} - sender.subFunc = func(ids []hotstuff.ID) (core.Sender, error) { return nil, fmt.Errorf("sub failed") } - bc := blockchain.New(el, logging.NewWithDest(new(bytes.Buffer), "test"), sender) - block := hotstuff.NewBlock(hotstuff.GetGenesis().Hash(), hotstuff.NewQuorumCert(nil, 0, hotstuff.GetGenesis().Hash()), &clientpb.Batch{}, 1, 1) - k := NewKauri(logging.NewWithDest(new(bytes.Buffer), "test"), el, cfg, bc, nil, sender) - k.initDone = true - if err := k.sendProposalToChildren(&hotstuff.ProposeMsg{Block: block}); err == nil { - t.Fatalf("expected error when Sub fails") - } -} + proposal := createProposal(t, setup.essentials, 1) + setup.essentials.Blockchain().Store(proposal.Block) + pc := testutil.CreatePC(t, proposal.Block, setup.essentials.Authority()) -func TestWaitToAggregate_TriggersWaitExpiredAndResets(t *testing.T) { - el := eventloop.New(logging.NewWithDest(new(bytes.Buffer), "test"), 8) - tr := tree.NewSimple(1, 2, []hotstuff.ID{1}) - tr.SetTreeHeightWaitTime(0) - cfg := core.NewRuntimeConfig(1, nil, core.WithKauriTree(tr)) - cfg.AddReplica(&hotstuff.ReplicaInfo{ID: 1}) - contribCh := make(chan struct{}, 1) - sender := &mockKauriSender{contribCh: contribCh} - bc := blockchain.New(el, logging.NewWithDest(new(bytes.Buffer), "test"), sender) - - k := NewKauri(logging.NewWithDest(new(bytes.Buffer), "test"), el, cfg, bc, nil, sender) - k.currentView = 7 - k.aggSent = false - k.aggContrib = newDummyQuorumSignature([]byte("s"), 1) - - // call waitToAggregate directly (uses WaitTime==0) - k.waitToAggregate() - - // process the added WaitTimerExpiredEvent - el.Tick(context.Background()) - select { - case <-contribCh: - case <-time.After(200 * time.Millisecond): - t.Fatalf("expected SendContributionToParent to be called on wait expiry") + err := setup.kauri.Disseminate(proposal, pc) + if err != nil { + t.Fatalf("Disseminate failed: %v", err) } - if k.aggContrib != nil { - t.Fatalf("expected aggContrib to be reset after wait expiry") + + // Single node has no children, should send contribution immediately + contributions := setup.sender.ContributionsSent() + if len(contributions) != 1 { + t.Errorf("expected 1 contribution from single node, got %d", len(contributions)) } } -func TestDisseminate_DelayedUntilConnected(t *testing.T) { - el := eventloop.New(logging.NewWithDest(new(bytes.Buffer), "test"), 8) - tr := tree.NewSimple(1, 2, []hotstuff.ID{1}) - cfg := core.NewRuntimeConfig(1, nil, core.WithKauriTree(tr)) - cfg.AddReplica(&hotstuff.ReplicaInfo{ID: 1}) - - sender := &mockKauriSender{contribCh: make(chan struct{}, 1)} - bc := blockchain.New(el, logging.NewWithDest(new(bytes.Buffer), "test"), sender) - block := hotstuff.NewBlock(hotstuff.GetGenesis().Hash(), hotstuff.NewQuorumCert(nil, 0, hotstuff.GetGenesis().Hash()), &clientpb.Batch{}, 1, 1) - bc.Store(block) - - k := NewKauri(logging.NewWithDest(new(bytes.Buffer), "test"), el, cfg, bc, nil, sender) - // initDone false by default - qs := newDummyQuorumSignature([]byte("s"), 1) - pc := hotstuff.NewPartialCert(qs, block.Hash()) - - if err := k.Disseminate(&hotstuff.ProposeMsg{Block: block}, pc); err != nil { - t.Fatalf("unexpected err: %v", err) - } - // nothing sent yet - select { - case <-sender.contribCh: - t.Fatalf("did not expect contribution yet") - default: +// TestChildrenCount tests that the correct number of children receive proposals +// for different tree configurations. +func TestChildrenCount(t *testing.T) { + tests := []struct { + branchFactor int + size int + replicaID hotstuff.ID + wantChildren int + }{ + // Root nodes + {branchFactor: 2, size: 7, replicaID: 1, wantChildren: 2}, + {branchFactor: 3, size: 13, replicaID: 1, wantChildren: 3}, + {branchFactor: 4, size: 21, replicaID: 1, wantChildren: 4}, + {branchFactor: 5, size: 31, replicaID: 1, wantChildren: 5}, + // Intermediate nodes (node 2) + {branchFactor: 2, size: 7, replicaID: 2, wantChildren: 2}, + {branchFactor: 3, size: 13, replicaID: 2, wantChildren: 3}, + {branchFactor: 4, size: 21, replicaID: 2, wantChildren: 4}, + // Leaf nodes (last node) + {branchFactor: 2, size: 7, replicaID: 7, wantChildren: 0}, + {branchFactor: 3, size: 13, replicaID: 13, wantChildren: 0}, + {branchFactor: 4, size: 21, replicaID: 21, wantChildren: 0}, } - // now trigger the ConnectedEvent which should release the delayed event - el.AddEvent(network.ConnectedEvent{}) - // simulate that the replica is connected (what the ReplicaConnectedEvent handler would have done) - k.initDone = true - // process connected and delayed WaitForConnectedEvent - el.Tick(context.Background()) - el.Tick(context.Background()) - select { - case <-sender.contribCh: - case <-time.After(200 * time.Millisecond): - t.Fatalf("expected contribution after connection") + for _, tt := range tests { + t.Run(test.Name("bf", tt.branchFactor, "id", int(tt.replicaID)), func(t *testing.T) { + setup := wireUpKauri(t, tt.replicaID, tt.size, tt.branchFactor) + children := setup.tree.ReplicaChildren() + if len(children) != tt.wantChildren { + t.Errorf("ReplicaChildren() = %d, want %d", len(children), tt.wantChildren) + } + }) } } -func TestAggregate_DelayedUntilConnected(t *testing.T) { - el := eventloop.New(logging.NewWithDest(new(bytes.Buffer), "test"), 8) - tr := tree.NewSimple(1, 2, []hotstuff.ID{1}) - cfg := core.NewRuntimeConfig(1, nil, core.WithKauriTree(tr)) - cfg.AddReplica(&hotstuff.ReplicaInfo{ID: 1}) - - sender := &mockKauriSender{contribCh: make(chan struct{}, 1)} - bc := blockchain.New(el, logging.NewWithDest(new(bytes.Buffer), "test"), sender) - block := hotstuff.NewBlock(hotstuff.GetGenesis().Hash(), hotstuff.NewQuorumCert(nil, 0, hotstuff.GetGenesis().Hash()), &clientpb.Batch{}, 1, 1) - bc.Store(block) - - k := NewKauri(logging.NewWithDest(new(bytes.Buffer), "test"), el, cfg, bc, nil, sender) - qs := newDummyQuorumSignature([]byte("s"), 1) - pc := hotstuff.NewPartialCert(qs, block.Hash()) - - if err := k.Aggregate(&hotstuff.ProposeMsg{Block: block}, pc); err != nil { - t.Fatalf("unexpected err: %v", err) +// TestTreeNodePositions verifies that tree positions work correctly +// for different node roles. +func TestTreeNodePositions(t *testing.T) { + tests := []struct { + branchFactor int + size int + replicaID hotstuff.ID + wantIsRoot bool + wantIsLeaf bool + }{ + // bf=2, size=7 + {branchFactor: 2, size: 7, replicaID: 1, wantIsRoot: true, wantIsLeaf: false}, + {branchFactor: 2, size: 7, replicaID: 2, wantIsRoot: false, wantIsLeaf: false}, + {branchFactor: 2, size: 7, replicaID: 7, wantIsRoot: false, wantIsLeaf: true}, + // bf=4, size=21 + {branchFactor: 4, size: 21, replicaID: 1, wantIsRoot: true, wantIsLeaf: false}, + {branchFactor: 4, size: 21, replicaID: 5, wantIsRoot: false, wantIsLeaf: false}, + {branchFactor: 4, size: 21, replicaID: 21, wantIsRoot: false, wantIsLeaf: true}, } - el.AddEvent(network.ConnectedEvent{}) - k.initDone = true - // process connected and delayed WaitForConnectedEvent - el.Tick(context.Background()) - el.Tick(context.Background()) - select { - case <-sender.contribCh: - case <-time.After(200 * time.Millisecond): - t.Fatalf("expected contribution after connection") + for _, tt := range tests { + t.Run(test.Name("bf", tt.branchFactor, "id", int(tt.replicaID)), func(t *testing.T) { + setup := wireUpKauri(t, tt.replicaID, tt.size, tt.branchFactor) + + isRoot := setup.tree.IsRoot(tt.replicaID) + if isRoot != tt.wantIsRoot { + t.Errorf("IsRoot(%d) = %v, want %v", tt.replicaID, isRoot, tt.wantIsRoot) + } + + // A node is a leaf if it has no children + isLeaf := len(setup.tree.ReplicaChildren()) == 0 + if isLeaf != tt.wantIsLeaf { + t.Errorf("IsLeaf(%d) = %v, want %v", tt.replicaID, isLeaf, tt.wantIsLeaf) + } + }) } } -func TestOnContributionRecv_IsSubSetCallsParent(t *testing.T) { - el := eventloop.New(logging.NewWithDest(new(bytes.Buffer), "test"), 8) - tr := tree.NewSimple(1, 2, []hotstuff.ID{1, 2}) - cfg := core.NewRuntimeConfig(1, nil, core.WithKauriTree(tr)) - cfg.AddReplica(&hotstuff.ReplicaInfo{ID: 1}) - cfg.AddReplica(&hotstuff.ReplicaInfo{ID: 2}) - - contribCh := make(chan struct{}, 1) - sender := &mockKauriSender{contribCh: contribCh} - bc := blockchain.New(el, logging.NewWithDest(new(bytes.Buffer), "test"), sender) - block := hotstuff.NewBlock(hotstuff.GetGenesis().Hash(), hotstuff.NewQuorumCert(nil, 0, hotstuff.GetGenesis().Hash()), &clientpb.Batch{}, 1, 1) - bc.Store(block) - - // authority that accepts any signature - k := NewKauri(logging.NewWithDest(new(bytes.Buffer), "test"), el, cfg, bc, cert.NewAuthority(cfg, bc, &fakeBase{}), sender) - k.currentView = 1 - k.blockHash = block.Hash() - - // construct a proto QuorumSignature with ECDSA sig so QuorumSignatureFromProto returns non-nil - protoSig := &hotstuffpb.QuorumSignature{} - ecd := &hotstuffpb.ECDSAMultiSignature{Sigs: []*hotstuffpb.ECDSASignature{{Signer: uint32(2), Sig: []byte("sig")}}} - protoSig.Sig = &hotstuffpb.QuorumSignature_ECDSASigs{ECDSASigs: ecd} - - k.onContributionRecv(&kauripb.Contribution{ID: 2, View: uint64(k.currentView), Signature: protoSig}) - select { - case <-contribCh: - case <-time.After(200 * time.Millisecond): - t.Fatalf("expected SendContributionToParent to be invoked when subtree satisfied") - } - if !k.aggSent { - t.Fatalf("expected aggSent to be true after subtree aggregation") +// TestProposalViewTracking ensures that the view is correctly tracked +// across dissemination and aggregation. +func TestProposalViewTracking(t *testing.T) { + views := []hotstuff.View{1, 5, 10, 100} + + for _, view := range views { + t.Run(test.Name("view", int(view)), func(t *testing.T) { + leafID := hotstuff.ID(7) // leaf in bf=2, size=7 tree + setup := wireUpKauri(t, leafID, 7, 2) + simulateConnection(t, setup) + + // Create block with specific view + qc := testutil.CreateQC(t, hotstuff.GetGenesis(), setup.essentials.Authority()) + block := hotstuff.NewBlock( + hotstuff.GetGenesis().Hash(), + qc, + &clientpb.Batch{}, + view, + 1, + ) + setup.essentials.Blockchain().Store(block) + + proposal := &hotstuff.ProposeMsg{ID: 1, Block: block} + pc := testutil.CreatePC(t, block, setup.essentials.Authority()) + + err := setup.kauri.Disseminate(proposal, pc) + if err != nil { + t.Fatalf("Disseminate failed: %v", err) + } + + contributions := setup.sender.ContributionsSent() + if len(contributions) != 1 { + t.Fatalf("expected 1 contribution, got %d", len(contributions)) + } + + if contributions[0].View != view { + t.Errorf("contribution view = %d, want %d", contributions[0].View, view) + } + }) } } From a69f1ededcc31b264f8354d5dc4ec823f8016bea Mon Sep 17 00:00:00 2001 From: Hein Meling Date: Tue, 27 Jan 2026 23:46:35 +0100 Subject: [PATCH 7/8] chore: upgraded go.mod dependencies, except gorums Gorums contains breaking changes, so we need to do it separately. --- go.mod | 42 +++++++++++------------ go.sum | 104 ++++++++++++++++++++++++++++----------------------------- 2 files changed, 73 insertions(+), 73 deletions(-) diff --git a/go.mod b/go.mod index 10673fd8..43264eae 100644 --- a/go.mod +++ b/go.mod @@ -1,9 +1,9 @@ module github.com/relab/hotstuff -go 1.25.4 +go 1.25.6 require ( - cuelang.org/go v0.15.0 + cuelang.org/go v0.15.4 github.com/felixge/fgprof v0.9.5 github.com/google/go-cmp v0.7.0 github.com/kilic/bls12-381 v0.1.1-0.20210208205449-6045b0235e36 @@ -11,16 +11,16 @@ require ( github.com/relab/gorums v0.10.0 github.com/relab/iago v0.0.0-20251028232537-e5b08eb0c08b github.com/relab/wrfs v0.0.0-20220416082020-a641cd350078 - github.com/spf13/cobra v1.10.1 + github.com/spf13/cobra v1.10.2 github.com/spf13/viper v1.21.0 go-hep.org/x/hep v0.38.1 - go.uber.org/zap v1.27.0 - golang.org/x/term v0.37.0 + go.uber.org/zap v1.27.1 + golang.org/x/term v0.39.0 golang.org/x/time v0.14.0 gonum.org/v1/plot v0.16.0 - google.golang.org/genproto/googleapis/rpc v0.0.0-20251111163417-95abcf5c77ba - google.golang.org/grpc v1.76.0 - google.golang.org/protobuf v1.36.10 + google.golang.org/genproto/googleapis/rpc v0.0.0-20260126211449-d11affda4bed + google.golang.org/grpc v1.78.0 + google.golang.org/protobuf v1.36.11 ) require ( @@ -34,9 +34,9 @@ require ( github.com/cockroachdb/apd/v3 v3.2.1 // indirect github.com/emicklei/proto v1.14.2 // indirect github.com/fsnotify/fsnotify v1.9.0 // indirect - github.com/go-viper/mapstructure/v2 v2.4.0 // indirect + github.com/go-viper/mapstructure/v2 v2.5.0 // indirect github.com/golang/freetype v0.0.0-20170609003504-e2365dfdc4a0 // indirect - github.com/google/pprof v0.0.0-20251007162407-5df77e3f7d1d // indirect + github.com/google/pprof v0.0.0-20260115054156-294ebfa9ad83 // indirect github.com/google/uuid v1.6.0 // indirect github.com/inconshreveable/mousetrap v1.1.0 // indirect github.com/kevinburke/ssh_config v1.4.0 // indirect @@ -47,8 +47,8 @@ require ( github.com/pelletier/go-toml/v2 v2.2.4 // indirect github.com/pkg/sftp v1.13.10 // indirect github.com/pmezard/go-difflib v1.0.1-0.20181226105442-5d4384ee4fb2 // indirect - github.com/protocolbuffers/txtpbfmt v0.0.0-20251016062345-16587c79cd91 // indirect - github.com/relab/container v0.0.0-20251028224705-baa7b7c5c895 // indirect + github.com/protocolbuffers/txtpbfmt v0.0.0-20251124094003-fcb97cc64c7b // indirect + github.com/relab/container v0.0.0-20260109140004-4adfae874bb5 // indirect github.com/rogpeppe/go-internal v1.14.1 // indirect github.com/sagikazarmark/locafero v0.12.0 // indirect github.com/spf13/afero v1.15.0 // indirect @@ -58,16 +58,16 @@ require ( github.com/tetratelabs/wazero v1.10.1 // indirect go.uber.org/multierr v1.11.0 // indirect go.yaml.in/yaml/v3 v3.0.4 // indirect - golang.org/x/crypto v0.44.0 // indirect - golang.org/x/image v0.33.0 // indirect - golang.org/x/mod v0.30.0 // indirect - golang.org/x/net v0.47.0 // indirect + golang.org/x/crypto v0.47.0 // indirect + golang.org/x/image v0.35.0 // indirect + golang.org/x/mod v0.31.0 // indirect + golang.org/x/net v0.49.0 // indirect golang.org/x/oauth2 v0.33.0 // indirect - golang.org/x/sync v0.18.0 // indirect - golang.org/x/sys v0.38.0 // indirect - golang.org/x/text v0.31.0 // indirect - golang.org/x/tools v0.39.0 // indirect - gonum.org/v1/gonum v0.16.0 // indirect + golang.org/x/sync v0.19.0 // indirect + golang.org/x/sys v0.40.0 // indirect + golang.org/x/text v0.33.0 // indirect + golang.org/x/tools v0.40.0 // indirect + gonum.org/v1/gonum v0.17.0 // indirect gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c // indirect gopkg.in/yaml.v3 v3.0.1 // indirect ) diff --git a/go.sum b/go.sum index 3242603b..a3b7fe71 100644 --- a/go.sum +++ b/go.sum @@ -12,8 +12,8 @@ codeberg.org/gonuts/binary v0.3.2 h1:7kSBmdRwbUv5fI8LaGp/gV+ow2OTi7EnRKO/pQ6YBJo codeberg.org/gonuts/binary v0.3.2/go.mod h1:hf+kigzXMZzpPTDOuSnTz+ppy5p037QluUFVtJ3OjWI= cuelabs.dev/go/oci/ociregistry v0.0.0-20251112093024-b12090c4dee0 h1:j1eQEmO1XkBX+KkWCTgiOfdLTNfzX0VxYR3NLvx76yM= cuelabs.dev/go/oci/ociregistry v0.0.0-20251112093024-b12090c4dee0/go.mod h1:4WWeZNxUO1vRoZWAHIG0KZOd6dA25ypyWuwD3ti0Tdc= -cuelang.org/go v0.15.0 h1:0jlWNxLp1In6dWJtywTXei7w0cqfHSTiCk/6Z+FUvxI= -cuelang.org/go v0.15.0/go.mod h1:NYw6n4akZcTjA7QQwJ1/gqWrrhsN4aZwhcAL0jv9rZE= +cuelang.org/go v0.15.4 h1:lrkTDhqy8dveHgX1ZLQ6WmgbhD8+rXa0fD25hxEKYhw= +cuelang.org/go v0.15.4/go.mod h1:NYw6n4akZcTjA7QQwJ1/gqWrrhsN4aZwhcAL0jv9rZE= git.sr.ht/~sbinet/cmpimg v0.1.0 h1:E0zPRk2muWuCqSKSVZIWsgtU9pjsw3eKHi8VmQeScxo= git.sr.ht/~sbinet/cmpimg v0.1.0/go.mod h1:FU12psLbF4TfNXkKH2ZZQ29crIqoiqTZmeQ7dkp/pxE= git.sr.ht/~sbinet/epok v0.5.0 h1:eQcocQpGQVYWLiA93dkIgngH0jjjDiTdj7rS3vQLp6w= @@ -51,8 +51,8 @@ github.com/go-logr/stdr v1.2.2 h1:hSWxHoqTgW2S2qGc0LTAI563KZ5YKYRhT3MFKZMbjag= github.com/go-logr/stdr v1.2.2/go.mod h1:mMo/vtBO5dYbehREoey6XUKy/eSumjCCveDpRre4VKE= github.com/go-quicktest/qt v1.101.0 h1:O1K29Txy5P2OK0dGo59b7b0LR6wKfIhttaAhHUyn7eI= github.com/go-quicktest/qt v1.101.0/go.mod h1:14Bz/f7NwaXPtdYEgzsx46kqSxVwTbzVZsDC26tQJow= -github.com/go-viper/mapstructure/v2 v2.4.0 h1:EBsztssimR/CONLSZZ04E8qAkxNYq4Qp9LvH92wZUgs= -github.com/go-viper/mapstructure/v2 v2.4.0/go.mod h1:oJDH3BJKyqBA2TXFhDsKDGDTlndYOZ6rGS0BRZIxGhM= +github.com/go-viper/mapstructure/v2 v2.5.0 h1:vM5IJoUAy3d7zRSVtIwQgBj7BiWtMPfmPEgAXnvj1Ro= +github.com/go-viper/mapstructure/v2 v2.5.0/go.mod h1:oJDH3BJKyqBA2TXFhDsKDGDTlndYOZ6rGS0BRZIxGhM= github.com/gobwas/httphead v0.1.0/go.mod h1:O/RXo79gxV8G+RqlR/otEwx4Q36zl9rqC5u12GKvMCM= github.com/gobwas/pool v0.2.1/go.mod h1:q8bcK0KcYlCgd9e7WYLm9LpyS+YeLd8JVDW6WezmKEw= github.com/gobwas/ws v1.2.1/go.mod h1:hRKAFb8wOxFROYNsT1bqfWnhX+b5MFeJM9r2ZSwg/KY= @@ -63,8 +63,8 @@ github.com/golang/protobuf v1.5.4/go.mod h1:lnTiLA8Wa4RWRcIUkrtSVa5nRhsEGBg48fD6 github.com/google/go-cmp v0.7.0 h1:wk8382ETsv4JYUZwIsn6YpYiWiBsYLSJiTsyBybVuN8= github.com/google/go-cmp v0.7.0/go.mod h1:pXiqmnSA92OHEEa9HXL2W4E7lf9JzCmGVUdgjX3N/iU= github.com/google/pprof v0.0.0-20240227163752-401108e1b7e7/go.mod h1:czg5+yv1E0ZGTi6S6vVK1mke0fV+FaUhNGcd6VRS9Ik= -github.com/google/pprof v0.0.0-20251007162407-5df77e3f7d1d h1:KJIErDwbSHjnp/SGzE5ed8Aol7JsKiI5X7yWKAtzhM0= -github.com/google/pprof v0.0.0-20251007162407-5df77e3f7d1d/go.mod h1:I6V7YzU0XDpsHqbsyrghnFZLO1gwK6NPTNvmetQIk9U= +github.com/google/pprof v0.0.0-20260115054156-294ebfa9ad83 h1:z2ogiKUYzX5Is6zr/vP9vJGqPwcdqsWjOt+V8J7+bTc= +github.com/google/pprof v0.0.0-20260115054156-294ebfa9ad83/go.mod h1:MxpfABSjhmINe3F1It9d+8exIHFvUqtLIRCdOGNXqiI= github.com/google/shlex v0.0.0-20191202100458-e7afc7fbc510 h1:El6M4kTTCOh6aBiKaUGG7oYTSPP8MxqL4YI3kZKwcP4= github.com/google/shlex v0.0.0-20191202100458-e7afc7fbc510/go.mod h1:pupxD2MaaD3pAXIBCelhxNneeOaAeabZDe5s4K6zSpQ= github.com/google/uuid v1.6.0 h1:NIvaJDMOsjHA8n1jAhLSgzrAzy1Hgr+hNrb57e+94F0= @@ -111,10 +111,10 @@ github.com/pkg/sftp v1.13.10/go.mod h1:bJ1a7uDhrX/4OII+agvy28lzRvQrmIQuaHrcI1Hbe github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= github.com/pmezard/go-difflib v1.0.1-0.20181226105442-5d4384ee4fb2 h1:Jamvg5psRIccs7FGNTlIRMkT8wgtp5eCXdBlqhYGL6U= github.com/pmezard/go-difflib v1.0.1-0.20181226105442-5d4384ee4fb2/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= -github.com/protocolbuffers/txtpbfmt v0.0.0-20251016062345-16587c79cd91 h1:s1LvMaU6mVwoFtbxv/rCZKE7/fwDmDY684FfUe4c1Io= -github.com/protocolbuffers/txtpbfmt v0.0.0-20251016062345-16587c79cd91/go.mod h1:JSbkp0BviKovYYt9XunS95M3mLPibE9bGg+Y95DsEEY= -github.com/relab/container v0.0.0-20251028224705-baa7b7c5c895 h1:IiS1KzQwmZsL5fnrvSpdMypSKqu7wcqSO7DGjWTr6bs= -github.com/relab/container v0.0.0-20251028224705-baa7b7c5c895/go.mod h1:oLZXG1NirJWzF2fMEeMUC6OLiMn6RtCChzUz1jtF/qs= +github.com/protocolbuffers/txtpbfmt v0.0.0-20251124094003-fcb97cc64c7b h1:fPVI9E6QNFYI0Ph3XpKUDrcAvbCifHvqYJcntFLPog8= +github.com/protocolbuffers/txtpbfmt v0.0.0-20251124094003-fcb97cc64c7b/go.mod h1:JSbkp0BviKovYYt9XunS95M3mLPibE9bGg+Y95DsEEY= +github.com/relab/container v0.0.0-20260109140004-4adfae874bb5 h1:ImfKSqvvsCWtQ2ibKkZZMgE8+incQzgfdRKEcxTW3g4= +github.com/relab/container v0.0.0-20260109140004-4adfae874bb5/go.mod h1:oLZXG1NirJWzF2fMEeMUC6OLiMn6RtCChzUz1jtF/qs= github.com/relab/gorums v0.10.0 h1:kerc6DAD7n4NNghoyXlyA6QzZzyIDJ7HJz7xwCTvsX0= github.com/relab/gorums v0.10.0/go.mod h1:9cov4XXpxDY0Lz8Orc9P+R2DuSWr2RPzwsgiy67xZTM= github.com/relab/iago v0.0.0-20251028232537-e5b08eb0c08b h1:pSBDurPN1xX6YlS8Sg/oh7PeMjPuGzKToKbpLRNmghE= @@ -130,8 +130,8 @@ github.com/spf13/afero v1.15.0 h1:b/YBCLWAJdFWJTN9cLhiXXcD7mzKn9Dm86dNnfyQw1I= github.com/spf13/afero v1.15.0/go.mod h1:NC2ByUVxtQs4b3sIUphxK0NioZnmxgyCrfzeuq8lxMg= github.com/spf13/cast v1.10.0 h1:h2x0u2shc1QuLHfxi+cTJvs30+ZAHOGRic8uyGTDWxY= github.com/spf13/cast v1.10.0/go.mod h1:jNfB8QC9IA6ZuY2ZjDp0KtFO2LZZlg4S/7bzP6qqeHo= -github.com/spf13/cobra v1.10.1 h1:lJeBwCfmrnXthfAupyUTzJ/J4Nc1RsHC/mSRU2dll/s= -github.com/spf13/cobra v1.10.1/go.mod h1:7SmJGaTHFVBY0jW4NXGluQoLvhqFQM+6XSKD+P4XaB0= +github.com/spf13/cobra v1.10.2 h1:DMTTonx5m65Ic0GOoRY2c16WCbHxOOw6xxezuLaBpcU= +github.com/spf13/cobra v1.10.2/go.mod h1:7C1pvHqHw5A4vrJfjNwvOdzYu0Gml16OCs2GRiTUUS4= github.com/spf13/pflag v1.0.9/go.mod h1:McXfInJRrz4CZXVZOBLb0bTZqETkiAhM9Iw0y3An2Bg= github.com/spf13/pflag v1.0.10 h1:4EBh2KAYBwaONj6b2Ye1GiHfwjqyROoF4RwYO+vPwFk= github.com/spf13/pflag v1.0.10/go.mod h1:McXfInJRrz4CZXVZOBLb0bTZqETkiAhM9Iw0y3An2Bg= @@ -152,47 +152,47 @@ github.com/tetratelabs/wazero v1.10.1/go.mod h1:DRm5twOQ5Gr1AoEdSi0CLjDQF1J9ZAuy github.com/yuin/goldmark v1.2.1/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74= go-hep.org/x/hep v0.38.1 h1:GdTcYD8aJdmyr+IExx6S99iNcbqNnIwf3nJKEuR6FRE= go-hep.org/x/hep v0.38.1/go.mod h1:raiZKDRcnG57o6DQSB7s2mSGyvZF+OSGSzN56akTnPg= -go.opentelemetry.io/auto/sdk v1.1.0 h1:cH53jehLUN6UFLY71z+NDOiNJqDdPRaXzTel0sJySYA= -go.opentelemetry.io/auto/sdk v1.1.0/go.mod h1:3wSPjt5PWp2RhlCcmmOial7AvC4DQqZb7a7wCow3W8A= -go.opentelemetry.io/otel v1.37.0 h1:9zhNfelUvx0KBfu/gb+ZgeAfAgtWrfHJZcAqFC228wQ= -go.opentelemetry.io/otel v1.37.0/go.mod h1:ehE/umFRLnuLa/vSccNq9oS1ErUlkkK71gMcN34UG8I= -go.opentelemetry.io/otel/metric v1.37.0 h1:mvwbQS5m0tbmqML4NqK+e3aDiO02vsf/WgbsdpcPoZE= -go.opentelemetry.io/otel/metric v1.37.0/go.mod h1:04wGrZurHYKOc+RKeye86GwKiTb9FKm1WHtO+4EVr2E= -go.opentelemetry.io/otel/sdk v1.37.0 h1:ItB0QUqnjesGRvNcmAcU0LyvkVyGJ2xftD29bWdDvKI= -go.opentelemetry.io/otel/sdk v1.37.0/go.mod h1:VredYzxUvuo2q3WRcDnKDjbdvmO0sCzOvVAiY+yUkAg= -go.opentelemetry.io/otel/sdk/metric v1.37.0 h1:90lI228XrB9jCMuSdA0673aubgRobVZFhbjxHHspCPc= -go.opentelemetry.io/otel/sdk/metric v1.37.0/go.mod h1:cNen4ZWfiD37l5NhS+Keb5RXVWZWpRE+9WyVCpbo5ps= -go.opentelemetry.io/otel/trace v1.37.0 h1:HLdcFNbRQBE2imdSEgm/kwqmQj1Or1l/7bW6mxVK7z4= -go.opentelemetry.io/otel/trace v1.37.0/go.mod h1:TlgrlQ+PtQO5XFerSPUYG0JSgGyryXewPGyayAWSBS0= +go.opentelemetry.io/auto/sdk v1.2.1 h1:jXsnJ4Lmnqd11kwkBV2LgLoFMZKizbCi5fNZ/ipaZ64= +go.opentelemetry.io/auto/sdk v1.2.1/go.mod h1:KRTj+aOaElaLi+wW1kO/DZRXwkF4C5xPbEe3ZiIhN7Y= +go.opentelemetry.io/otel v1.38.0 h1:RkfdswUDRimDg0m2Az18RKOsnI8UDzppJAtj01/Ymk8= +go.opentelemetry.io/otel v1.38.0/go.mod h1:zcmtmQ1+YmQM9wrNsTGV/q/uyusom3P8RxwExxkZhjM= +go.opentelemetry.io/otel/metric v1.38.0 h1:Kl6lzIYGAh5M159u9NgiRkmoMKjvbsKtYRwgfrA6WpA= +go.opentelemetry.io/otel/metric v1.38.0/go.mod h1:kB5n/QoRM8YwmUahxvI3bO34eVtQf2i4utNVLr9gEmI= +go.opentelemetry.io/otel/sdk v1.38.0 h1:l48sr5YbNf2hpCUj/FoGhW9yDkl+Ma+LrVl8qaM5b+E= +go.opentelemetry.io/otel/sdk v1.38.0/go.mod h1:ghmNdGlVemJI3+ZB5iDEuk4bWA3GkTpW+DOoZMYBVVg= +go.opentelemetry.io/otel/sdk/metric v1.38.0 h1:aSH66iL0aZqo//xXzQLYozmWrXxyFkBJ6qT5wthqPoM= +go.opentelemetry.io/otel/sdk/metric v1.38.0/go.mod h1:dg9PBnW9XdQ1Hd6ZnRz689CbtrUp0wMMs9iPcgT9EZA= +go.opentelemetry.io/otel/trace v1.38.0 h1:Fxk5bKrDZJUH+AMyyIXGcFAPah0oRcT+LuNtJrmcNLE= +go.opentelemetry.io/otel/trace v1.38.0/go.mod h1:j1P9ivuFsTceSWe1oY+EeW3sc+Pp42sO++GHkg4wwhs= go.uber.org/goleak v1.3.0 h1:2K3zAYmnTNqV73imy9J1T3WC+gmCePx2hEGkimedGto= go.uber.org/goleak v1.3.0/go.mod h1:CoHD4mav9JJNrW/WLlf7HGZPjdw8EucARQHekz1X6bE= go.uber.org/multierr v1.11.0 h1:blXXJkSxSSfBVBlC76pxqeO+LN3aDfLQo+309xJstO0= go.uber.org/multierr v1.11.0/go.mod h1:20+QtiLqy0Nd6FdQB9TLXag12DsQkrbs3htMFfDN80Y= -go.uber.org/zap v1.27.0 h1:aJMhYGrd5QSmlpLMr2MftRKl7t8J8PTZPA732ud/XR8= -go.uber.org/zap v1.27.0/go.mod h1:GB2qFLM7cTU87MWRP2mPIjqfIDnGu+VIO4V/SdhGo2E= +go.uber.org/zap v1.27.1 h1:08RqriUEv8+ArZRYSTXy1LeBScaMpVSTBhCeaZYfMYc= +go.uber.org/zap v1.27.1/go.mod h1:GB2qFLM7cTU87MWRP2mPIjqfIDnGu+VIO4V/SdhGo2E= go.yaml.in/yaml/v3 v3.0.4 h1:tfq32ie2Jv2UxXFdLJdh3jXuOzWiL1fo0bu/FbuKpbc= go.yaml.in/yaml/v3 v3.0.4/go.mod h1:DhzuOOF2ATzADvBadXxruRBLzYTpT36CKvDb3+aBEFg= golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w= golang.org/x/crypto v0.0.0-20191011191535-87dc89f01550/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI= golang.org/x/crypto v0.0.0-20200622213623-75b288015ac9/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto= -golang.org/x/crypto v0.44.0 h1:A97SsFvM3AIwEEmTBiaxPPTYpDC47w720rdiiUvgoAU= -golang.org/x/crypto v0.44.0/go.mod h1:013i+Nw79BMiQiMsOPcVCB5ZIJbYkerPrGnOa00tvmc= -golang.org/x/image v0.33.0 h1:LXRZRnv1+zGd5XBUVRFmYEphyyKJjQjCRiOuAP3sZfQ= -golang.org/x/image v0.33.0/go.mod h1:DD3OsTYT9chzuzTQt+zMcOlBHgfoKQb1gry8p76Y1sc= +golang.org/x/crypto v0.47.0 h1:V6e3FRj+n4dbpw86FJ8Fv7XVOql7TEwpHapKoMJ/GO8= +golang.org/x/crypto v0.47.0/go.mod h1:ff3Y9VzzKbwSSEzWqJsJVBnWmRwRSHt/6Op5n9bQc4A= +golang.org/x/image v0.35.0 h1:LKjiHdgMtO8z7Fh18nGY6KDcoEtVfsgLDPeLyguqb7I= +golang.org/x/image v0.35.0/go.mod h1:MwPLTVgvxSASsxdLzKrl8BRFuyqMyGhLwmC+TO1Sybk= golang.org/x/mod v0.3.0/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA= -golang.org/x/mod v0.30.0 h1:fDEXFVZ/fmCKProc/yAXXUijritrDzahmwwefnjoPFk= -golang.org/x/mod v0.30.0/go.mod h1:lAsf5O2EvJeSFMiBxXDki7sCgAxEUcZHXoXMKT4GJKc= +golang.org/x/mod v0.31.0 h1:HaW9xtz0+kOcWKwli0ZXy79Ix+UW/vOfmWI5QVd2tgI= +golang.org/x/mod v0.31.0/go.mod h1:43JraMp9cGx1Rx3AqioxrbrhNsLl2l/iNAvuBkrezpg= golang.org/x/net v0.0.0-20190404232315-eb5bcb51f2a3/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg= golang.org/x/net v0.0.0-20190620200207-3b0461eec859/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= golang.org/x/net v0.0.0-20201021035429-f5854403a974/go.mod h1:sp8m0HH+o8qH0wwXwYZr8TS3Oi6o0r6Gce1SSxlDquU= -golang.org/x/net v0.47.0 h1:Mx+4dIFzqraBXUugkia1OOvlD6LemFo1ALMHjrXDOhY= -golang.org/x/net v0.47.0/go.mod h1:/jNxtkgq5yWUGYkaZGqo27cfGZ1c5Nen03aYrrKpVRU= +golang.org/x/net v0.49.0 h1:eeHFmOGUTtaaPSGNmjBKpbng9MulQsJURQUAfUwY++o= +golang.org/x/net v0.49.0/go.mod h1:/ysNB2EvaqvesRkuLAyjI1ycPZlQHM3q01F02UY/MV8= golang.org/x/oauth2 v0.33.0 h1:4Q+qn+E5z8gPRJfmRy7C2gGG3T4jIprK6aSYgTXGRpo= golang.org/x/oauth2 v0.33.0/go.mod h1:lzm5WQJQwKZ3nwavOZ3IS5Aulzxi68dUSgRHujetwEA= golang.org/x/sync v0.0.0-20190423024810-112230192c58/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= golang.org/x/sync v0.0.0-20201020160332-67f06af15bc9/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= -golang.org/x/sync v0.18.0 h1:kr88TuHDroi+UVf+0hZnirlk8o8T+4MrK6mr60WkH/I= -golang.org/x/sync v0.18.0/go.mod h1:9KTHXmSnoGruLpwFjVSX0lNNA75CykiMECbovNTZqGI= +golang.org/x/sync v0.19.0 h1:vV+1eWNmZ5geRlYjzm2adRgW2/mcpevXNg50YZtPCE4= +golang.org/x/sync v0.19.0/go.mod h1:9KTHXmSnoGruLpwFjVSX0lNNA75CykiMECbovNTZqGI= golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= golang.org/x/sys v0.0.0-20190412213103-97732733099d/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20200930185726-fdedc70b468f/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= @@ -200,34 +200,34 @@ golang.org/x/sys v0.0.0-20210119212857-b64e53b001e4/go.mod h1:h1NjWce9XRLGQEsW7w golang.org/x/sys v0.0.0-20210124154548-22da62e12c0c/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20220310020820-b874c991c1a5/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.6.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= -golang.org/x/sys v0.38.0 h1:3yZWxaJjBmCWXqhN1qh02AkOnCQ1poK6oF+a7xWL6Gc= -golang.org/x/sys v0.38.0/go.mod h1:OgkHotnGiDImocRcuBABYBEXf8A9a87e/uXjp9XT3ks= -golang.org/x/term v0.37.0 h1:8EGAD0qCmHYZg6J17DvsMy9/wJ7/D/4pV/wfnld5lTU= -golang.org/x/term v0.37.0/go.mod h1:5pB4lxRNYYVZuTLmy8oR2BH8dflOR+IbTYFD8fi3254= +golang.org/x/sys v0.40.0 h1:DBZZqJ2Rkml6QMQsZywtnjnnGvHza6BTfYFWY9kjEWQ= +golang.org/x/sys v0.40.0/go.mod h1:OgkHotnGiDImocRcuBABYBEXf8A9a87e/uXjp9XT3ks= +golang.org/x/term v0.39.0 h1:RclSuaJf32jOqZz74CkPA9qFuVTX7vhLlpfj/IGWlqY= +golang.org/x/term v0.39.0/go.mod h1:yxzUCTP/U+FzoxfdKmLaA0RV1WgE0VY7hXBwKtY/4ww= golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= golang.org/x/text v0.3.3/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ= -golang.org/x/text v0.31.0 h1:aC8ghyu4JhP8VojJ2lEHBnochRno1sgL6nEi9WGFGMM= -golang.org/x/text v0.31.0/go.mod h1:tKRAlv61yKIjGGHX/4tP1LTbc13YSec1pxVEWXzfoeM= +golang.org/x/text v0.33.0 h1:B3njUFyqtHDUI5jMn1YIr5B0IE2U0qck04r6d4KPAxE= +golang.org/x/text v0.33.0/go.mod h1:LuMebE6+rBincTi9+xWTY8TztLzKHc/9C1uBCG27+q8= golang.org/x/time v0.14.0 h1:MRx4UaLrDotUKUdCIqzPC48t1Y9hANFKIRpNx+Te8PI= golang.org/x/time v0.14.0/go.mod h1:eL/Oa2bBBK0TkX57Fyni+NgnyQQN4LitPmob2Hjnqw4= golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ= golang.org/x/tools v0.0.0-20191119224855-298f0cb1881e/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo= golang.org/x/tools v0.1.0/go.mod h1:xkSsbof2nBLbhDlRMhhhyNLN/zl3eTqcnHD5viDpcZ0= -golang.org/x/tools v0.39.0 h1:ik4ho21kwuQln40uelmciQPp9SipgNDdrafrYA4TmQQ= -golang.org/x/tools v0.39.0/go.mod h1:JnefbkDPyD8UU2kI5fuf8ZX4/yUeh9W877ZeBONxUqQ= +golang.org/x/tools v0.40.0 h1:yLkxfA+Qnul4cs9QA3KnlFu0lVmd8JJfoq+E41uSutA= +golang.org/x/tools v0.40.0/go.mod h1:Ik/tzLRlbscWpqqMRjyWYDisX8bG13FrdXp3o4Sr9lc= golang.org/x/xerrors v0.0.0-20190717185122-a985d3407aa7/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= golang.org/x/xerrors v0.0.0-20191011141410-1b5146add898/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= golang.org/x/xerrors v0.0.0-20200804184101-5ec99f83aff1/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= -gonum.org/v1/gonum v0.16.0 h1:5+ul4Swaf3ESvrOnidPp4GZbzf0mxVQpDCYUQE7OJfk= -gonum.org/v1/gonum v0.16.0/go.mod h1:fef3am4MQ93R2HHpKnLk4/Tbh/s0+wqD5nfa6Pnwy4E= +gonum.org/v1/gonum v0.17.0 h1:VbpOemQlsSMrYmn7T2OUvQ4dqxQXU+ouZFQsZOx50z4= +gonum.org/v1/gonum v0.17.0/go.mod h1:El3tOrEuMpv2UdMrbNlKEh9vd86bmQ6vqIcDwxEOc1E= gonum.org/v1/plot v0.16.0 h1:dK28Qx/Ky4VmPUN/2zeW0ELyM6ucDnBAj5yun7M9n1g= gonum.org/v1/plot v0.16.0/go.mod h1:Xz6U1yDMi6Ni6aaXILqmVIb6Vro8E+K7Q/GeeH+Pn0c= -google.golang.org/genproto/googleapis/rpc v0.0.0-20251111163417-95abcf5c77ba h1:UKgtfRM7Yh93Sya0Fo8ZzhDP4qBckrrxEr2oF5UIVb8= -google.golang.org/genproto/googleapis/rpc v0.0.0-20251111163417-95abcf5c77ba/go.mod h1:7i2o+ce6H/6BluujYR+kqX3GKH+dChPTQU19wjRPiGk= -google.golang.org/grpc v1.76.0 h1:UnVkv1+uMLYXoIz6o7chp59WfQUYA2ex/BXQ9rHZu7A= -google.golang.org/grpc v1.76.0/go.mod h1:Ju12QI8M6iQJtbcsV+awF5a4hfJMLi4X0JLo94ULZ6c= -google.golang.org/protobuf v1.36.10 h1:AYd7cD/uASjIL6Q9LiTjz8JLcrh/88q5UObnmY3aOOE= -google.golang.org/protobuf v1.36.10/go.mod h1:HTf+CrKn2C3g5S8VImy6tdcUvCska2kB7j23XfzDpco= +google.golang.org/genproto/googleapis/rpc v0.0.0-20260126211449-d11affda4bed h1:Yyog7dFpq0nVFnxj1NymkvC4RDIzc7KILL6vNAgLbCs= +google.golang.org/genproto/googleapis/rpc v0.0.0-20260126211449-d11affda4bed/go.mod h1:j9x/tPzZkyxcgEFkiKEEGxfvyumM01BEtsW8xzOahRQ= +google.golang.org/grpc v1.78.0 h1:K1XZG/yGDJnzMdd/uZHAkVqJE+xIDOcmdSFZkBUicNc= +google.golang.org/grpc v1.78.0/go.mod h1:I47qjTo4OKbMkjA/aOOwxDIiPSBofUtQUI5EfpWvW7U= +google.golang.org/protobuf v1.36.11 h1:fV6ZwhNocDyBLK0dj+fg8ektcVegBBuEolpbTQyBNVE= +google.golang.org/protobuf v1.36.11/go.mod h1:HTf+CrKn2C3g5S8VImy6tdcUvCska2kB7j23XfzDpco= gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c h1:Hei/4ADfdWqJk1ZMxUNpqntNwaWcugrBjAiHlqqRiVk= gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c/go.mod h1:JHkPIbrfpd72SG/EVd6muEfDQjcINNoR0C8j2r3qZ4Q= From 8510925bca305b1fb0e5e413f035aeef128095aa Mon Sep 17 00:00:00 2001 From: Hein Meling Date: Tue, 27 Jan 2026 23:51:13 +0100 Subject: [PATCH 8/8] chore: regenerated proto files (no real change; just version number) --- internal/proto/clientpb/client.pb.go | 4 ++-- internal/proto/clientpb/client_gorums.pb.go | 2 +- internal/proto/hotstuffpb/hotstuff.pb.go | 4 ++-- internal/proto/hotstuffpb/hotstuff_gorums.pb.go | 2 +- internal/proto/kauripb/kauri.pb.go | 4 ++-- internal/proto/kauripb/kauri_gorums.pb.go | 2 +- internal/proto/orchestrationpb/orchestration.pb.go | 4 ++-- metrics/types/types.pb.go | 4 ++-- 8 files changed, 13 insertions(+), 13 deletions(-) diff --git a/internal/proto/clientpb/client.pb.go b/internal/proto/clientpb/client.pb.go index b38b8d7d..6ed28beb 100644 --- a/internal/proto/clientpb/client.pb.go +++ b/internal/proto/clientpb/client.pb.go @@ -1,7 +1,7 @@ // Code generated by protoc-gen-go. DO NOT EDIT. // versions: -// protoc-gen-go v1.36.10 -// protoc v6.33.0 +// protoc-gen-go v1.36.11 +// protoc v6.33.4 // source: internal/proto/clientpb/client.proto package clientpb diff --git a/internal/proto/clientpb/client_gorums.pb.go b/internal/proto/clientpb/client_gorums.pb.go index 534bfd2e..cd75440d 100644 --- a/internal/proto/clientpb/client_gorums.pb.go +++ b/internal/proto/clientpb/client_gorums.pb.go @@ -1,7 +1,7 @@ // Code generated by protoc-gen-gorums. DO NOT EDIT. // versions: // protoc-gen-gorums v0.10.0-devel -// protoc v6.33.0 +// protoc v6.33.4 // source: internal/proto/clientpb/client.proto package clientpb diff --git a/internal/proto/hotstuffpb/hotstuff.pb.go b/internal/proto/hotstuffpb/hotstuff.pb.go index a01e409a..dad5fc92 100644 --- a/internal/proto/hotstuffpb/hotstuff.pb.go +++ b/internal/proto/hotstuffpb/hotstuff.pb.go @@ -1,7 +1,7 @@ // Code generated by protoc-gen-go. DO NOT EDIT. // versions: -// protoc-gen-go v1.36.10 -// protoc v6.33.0 +// protoc-gen-go v1.36.11 +// protoc v6.33.4 // source: internal/proto/hotstuffpb/hotstuff.proto package hotstuffpb diff --git a/internal/proto/hotstuffpb/hotstuff_gorums.pb.go b/internal/proto/hotstuffpb/hotstuff_gorums.pb.go index e3779e4b..f0545f18 100644 --- a/internal/proto/hotstuffpb/hotstuff_gorums.pb.go +++ b/internal/proto/hotstuffpb/hotstuff_gorums.pb.go @@ -1,7 +1,7 @@ // Code generated by protoc-gen-gorums. DO NOT EDIT. // versions: // protoc-gen-gorums v0.10.0-devel -// protoc v6.33.0 +// protoc v6.33.4 // source: internal/proto/hotstuffpb/hotstuff.proto package hotstuffpb diff --git a/internal/proto/kauripb/kauri.pb.go b/internal/proto/kauripb/kauri.pb.go index 3c7ed731..9d9b9214 100644 --- a/internal/proto/kauripb/kauri.pb.go +++ b/internal/proto/kauripb/kauri.pb.go @@ -1,7 +1,7 @@ // Code generated by protoc-gen-go. DO NOT EDIT. // versions: -// protoc-gen-go v1.36.10 -// protoc v6.33.0 +// protoc-gen-go v1.36.11 +// protoc v6.33.4 // source: internal/proto/kauripb/kauri.proto package kauripb diff --git a/internal/proto/kauripb/kauri_gorums.pb.go b/internal/proto/kauripb/kauri_gorums.pb.go index a33ac252..8e2a2690 100644 --- a/internal/proto/kauripb/kauri_gorums.pb.go +++ b/internal/proto/kauripb/kauri_gorums.pb.go @@ -1,7 +1,7 @@ // Code generated by protoc-gen-gorums. DO NOT EDIT. // versions: // protoc-gen-gorums v0.10.0-devel -// protoc v6.33.0 +// protoc v6.33.4 // source: internal/proto/kauripb/kauri.proto package kauripb diff --git a/internal/proto/orchestrationpb/orchestration.pb.go b/internal/proto/orchestrationpb/orchestration.pb.go index 286fd826..5d026aac 100644 --- a/internal/proto/orchestrationpb/orchestration.pb.go +++ b/internal/proto/orchestrationpb/orchestration.pb.go @@ -1,7 +1,7 @@ // Code generated by protoc-gen-go. DO NOT EDIT. // versions: -// protoc-gen-go v1.36.10 -// protoc v6.33.0 +// protoc-gen-go v1.36.11 +// protoc v6.33.4 // source: internal/proto/orchestrationpb/orchestration.proto package orchestrationpb diff --git a/metrics/types/types.pb.go b/metrics/types/types.pb.go index 3d2c9526..51fba661 100644 --- a/metrics/types/types.pb.go +++ b/metrics/types/types.pb.go @@ -1,7 +1,7 @@ // Code generated by protoc-gen-go. DO NOT EDIT. // versions: -// protoc-gen-go v1.36.10 -// protoc v6.33.0 +// protoc-gen-go v1.36.11 +// protoc v6.33.4 // source: metrics/types/types.proto package types