From 345d6772192438e1d0158dfe018f75b62e211c64 Mon Sep 17 00:00:00 2001 From: Yacov Manevich Date: Fri, 16 Aug 2024 21:24:06 +0200 Subject: [PATCH 1/8] Implement traversal based early termination This commit makes the early termination logic of the snowflake poll consider also transitive voting. A block `b0` is transitively voted by all votes for block `b1` if `b0` is an ancestor of `b1`. This transitive termination logic also works for shard common block prefixes. If block `b0` is a direct ancestor of `b1` and `b2` and the latter have a shared prefix, then the logic in this commit may not early terminate the vote if a future vote might improve the confidence of the shared prefix, as it correspons to a snowflake instance. Signed-off-by: Yacov Manevich --- chains/manager.go | 8 +- .../snowman/poll/early_term_no_traversal.go | 191 ++++++++++++++---- .../poll/early_term_no_traversal_test.go | 93 ++++++++- snow/consensus/snowman/poll/graph.go | 154 ++++++++++++++ snow/consensus/snowman/poll/graph_test.go | 185 +++++++++++++++++ snow/consensus/snowman/poll/prefix.go | 186 +++++++++++++++++ snow/consensus/snowman/poll/prefix_test.go | 137 +++++++++++++ snow/consensus/snowman/topological.go | 11 + snow/context.go | 4 + snow/engine/snowman/config.go | 2 +- snow/engine/snowman/config_test.go | 5 +- snow/engine/snowman/engine.go | 3 +- 12 files changed, 935 insertions(+), 44 deletions(-) create mode 100644 snow/consensus/snowman/poll/graph.go create mode 100644 snow/consensus/snowman/poll/graph_test.go create mode 100644 snow/consensus/snowman/poll/prefix.go create mode 100644 snow/consensus/snowman/poll/prefix_test.go diff --git a/chains/manager.go b/chains/manager.go index 8d1eb4feea76..60eade167c11 100644 --- a/chains/manager.go +++ b/chains/manager.go @@ -920,7 +920,8 @@ func (m *manager) createAvalancheChain( return nil, fmt.Errorf("couldn't initialize snow base message handler: %w", err) } - var snowmanConsensus smcon.Consensus = &smcon.Topological{} + topological := &smcon.Topological{} + var snowmanConsensus smcon.Consensus = topological if m.TracingEnabled { snowmanConsensus = smcon.Trace(snowmanConsensus, m.Tracer) } @@ -936,6 +937,7 @@ func (m *manager) createAvalancheChain( ConnectedValidators: connectedValidators, Params: consensusParams, Consensus: snowmanConsensus, + BlockTraversal: topological, } var snowmanEngine common.Engine snowmanEngine, err = smeng.New(snowmanEngineConfig) @@ -1313,7 +1315,8 @@ func (m *manager) createSnowmanChain( return nil, fmt.Errorf("couldn't initialize snow base message handler: %w", err) } - var consensus smcon.Consensus = &smcon.Topological{} + topological := &smcon.Topological{} + var consensus smcon.Consensus = topological if m.TracingEnabled { consensus = smcon.Trace(consensus, m.Tracer) } @@ -1321,6 +1324,7 @@ func (m *manager) createSnowmanChain( // Create engine, bootstrapper and state-syncer in this order, // to make sure start callbacks are duly initialized engineConfig := smeng.Config{ + BlockTraversal: topological, Ctx: ctx, AllGetsServer: snowGetHandler, VM: vm, diff --git a/snow/consensus/snowman/poll/early_term_no_traversal.go b/snow/consensus/snowman/poll/early_term_no_traversal.go index df09157b04df..ca19b8ad02a5 100644 --- a/snow/consensus/snowman/poll/early_term_no_traversal.go +++ b/snow/consensus/snowman/poll/early_term_no_traversal.go @@ -4,6 +4,7 @@ package poll import ( + "bytes" "errors" "fmt" "time" @@ -11,6 +12,7 @@ import ( "github.com/prometheus/client_golang/prometheus" "github.com/ava-labs/avalanchego/ids" + "github.com/ava-labs/avalanchego/snow" "github.com/ava-labs/avalanchego/utils/bag" ) @@ -38,7 +40,7 @@ var ( } ) -type earlyTermNoTraversalMetrics struct { +type earlyTermTraversalMetrics struct { durExhaustedPolls prometheus.Gauge durEarlyFailPolls prometheus.Gauge durEarlyAlphaPrefPolls prometheus.Gauge @@ -50,7 +52,7 @@ type earlyTermNoTraversalMetrics struct { countEarlyAlphaConfPolls prometheus.Counter } -func newEarlyTermNoTraversalMetrics(reg prometheus.Registerer) (*earlyTermNoTraversalMetrics, error) { +func newEarlyTermTraversalMetrics(reg prometheus.Registerer) (*earlyTermTraversalMetrics, error) { pollCountVec := prometheus.NewCounterVec(prometheus.CounterOpts{ Name: "poll_count", Help: "Total # of terminated polls by reason", @@ -66,7 +68,7 @@ func newEarlyTermNoTraversalMetrics(reg prometheus.Registerer) (*earlyTermNoTrav return nil, fmt.Errorf("%w: %w", errPollDurationVectorMetrics, err) } - return &earlyTermNoTraversalMetrics{ + return &earlyTermTraversalMetrics{ durExhaustedPolls: durPollsVec.With(exhaustedLabel), durEarlyFailPolls: durPollsVec.With(earlyFailLabel), durEarlyAlphaPrefPolls: durPollsVec.With(earlyAlphaPrefLabel), @@ -78,54 +80,57 @@ func newEarlyTermNoTraversalMetrics(reg prometheus.Registerer) (*earlyTermNoTrav }, nil } -func (m *earlyTermNoTraversalMetrics) observeExhausted(duration time.Duration) { +func (m *earlyTermTraversalMetrics) observeExhausted(duration time.Duration) { m.durExhaustedPolls.Add(float64(duration.Nanoseconds())) m.countExhaustedPolls.Inc() } -func (m *earlyTermNoTraversalMetrics) observeEarlyFail(duration time.Duration) { +func (m *earlyTermTraversalMetrics) observeEarlyFail(duration time.Duration) { m.durEarlyFailPolls.Add(float64(duration.Nanoseconds())) m.countEarlyFailPolls.Inc() } -func (m *earlyTermNoTraversalMetrics) observeEarlyAlphaPref(duration time.Duration) { +func (m *earlyTermTraversalMetrics) observeEarlyAlphaPref(duration time.Duration) { m.durEarlyAlphaPrefPolls.Add(float64(duration.Nanoseconds())) m.countEarlyAlphaPrefPolls.Inc() } -func (m *earlyTermNoTraversalMetrics) observeEarlyAlphaConf(duration time.Duration) { +func (m *earlyTermTraversalMetrics) observeEarlyAlphaConf(duration time.Duration) { m.durEarlyAlphaConfPolls.Add(float64(duration.Nanoseconds())) m.countEarlyAlphaConfPolls.Inc() } -type earlyTermNoTraversalFactory struct { +type earlyTermTraversalFactory struct { alphaPreference int alphaConfidence int - - metrics *earlyTermNoTraversalMetrics + bt snow.BlockTraversal + metrics *earlyTermTraversalMetrics } -// NewEarlyTermNoTraversalFactory returns a factory that returns polls with +// NewEarlyTermTraversalFactory returns a factory that returns polls with // early termination, without doing DAG traversals -func NewEarlyTermNoTraversalFactory( +func NewEarlyTermTraversalFactory( alphaPreference int, alphaConfidence int, reg prometheus.Registerer, + bt snow.BlockTraversal, ) (Factory, error) { - metrics, err := newEarlyTermNoTraversalMetrics(reg) + metrics, err := newEarlyTermTraversalMetrics(reg) if err != nil { return nil, err } - return &earlyTermNoTraversalFactory{ + return &earlyTermTraversalFactory{ + bt: bt, alphaPreference: alphaPreference, alphaConfidence: alphaConfidence, metrics: metrics, }, nil } -func (f *earlyTermNoTraversalFactory) New(vdrs bag.Bag[ids.NodeID]) Poll { - return &earlyTermNoTraversalPoll{ +func (f *earlyTermTraversalFactory) New(vdrs bag.Bag[ids.NodeID]) Poll { + return &earlyTermTraversalPoll{ + bt: f.bt, polled: vdrs, alphaPreference: f.alphaPreference, alphaConfidence: f.alphaConfidence, @@ -134,22 +139,21 @@ func (f *earlyTermNoTraversalFactory) New(vdrs bag.Bag[ids.NodeID]) Poll { } } -// earlyTermNoTraversalPoll finishes when any remaining validators can't change -// the result of the poll. However, does not terminate tightly with this bound. -// It terminates as quickly as it can without performing any DAG traversals. -type earlyTermNoTraversalPoll struct { +// earlyTermTraversalPoll finishes when any remaining validators can't change +// the result of the poll for all the votes and transitive votes. +type earlyTermTraversalPoll struct { votes bag.Bag[ids.ID] polled bag.Bag[ids.NodeID] alphaPreference int alphaConfidence int - - metrics *earlyTermNoTraversalMetrics - start time.Time - finished bool + bt snow.BlockTraversal + metrics *earlyTermTraversalMetrics + start time.Time + finished bool } // Vote registers a response for this poll -func (p *earlyTermNoTraversalPoll) Vote(vdr ids.NodeID, vote ids.ID) { +func (p *earlyTermTraversalPoll) Vote(vdr ids.NodeID, vote ids.ID) { count := p.polled.Count(vdr) // make sure that a validator can't respond multiple times p.polled.Remove(vdr) @@ -159,7 +163,7 @@ func (p *earlyTermNoTraversalPoll) Vote(vdr ids.NodeID, vote ids.ID) { } // Drop any future response for this poll -func (p *earlyTermNoTraversalPoll) Drop(vdr ids.NodeID) { +func (p *earlyTermTraversalPoll) Drop(vdr ids.NodeID) { p.polled.Remove(vdr) } @@ -172,35 +176,83 @@ func (p *earlyTermNoTraversalPoll) Drop(vdr ids.NodeID) { // impossible for it to achieve an alphaConfidence majority after applying // transitive voting. // 4. A single element has achieved an alphaConfidence majority. -func (p *earlyTermNoTraversalPoll) Finished() bool { +func (p *earlyTermTraversalPoll) Finished() bool { if p.finished { return true } remaining := p.polled.Len() + received := p.votes.Len() + maxPossibleVotes := received + remaining + if remaining == 0 { p.finished = true p.metrics.observeExhausted(time.Since(p.start)) return true // Case 1 } - received := p.votes.Len() - maxPossibleVotes := received + remaining if maxPossibleVotes < p.alphaPreference { p.finished = true p.metrics.observeEarlyFail(time.Since(p.start)) return true // Case 2 } - _, freq := p.votes.Mode() - if freq >= p.alphaPreference && maxPossibleVotes < p.alphaConfidence { + // v + // / + // u + // We build a vote graph where each vertex represents a block ID. + // A vertex 'v' is a parent of vertex 'u' if the ID of 'u' corresponds + // to a block that is the successive block of the corresponding block for 'v'. + votesGraph := buildVotesGraph(p.bt.GetParent, p.votes) + + // If vertex 'v' is a parent of vertex 'u', then a vote for the ID of vertex 'u' + // should also be considered as a vote for the ID of the vertex 'v'. + transitiveVotes := computeTransitiveVoteCountGraph(&votesGraph, p.votes) + + // v + // / \ + // u w + // If two competing blocks 'u', 'w' are potential successors to a block 'v', + // snowman would instantiate a unary snowflake instance on the prefix of 'u' and 'w'. + // The prefix inherits the votes for the IDs of 'u' and 'w'. + // We therefore compute the transitive votes for all prefixes of IDs + // for each bifurcation in the transitive vote graph. + transitiveVotesForPrefixes := transitiveVotesForPrefixes(&votesGraph, transitiveVotes) + + // We wish to compute the votes for snowflake instances, no matter if they correspond to an actual block ID, + // or a unary snowflake instance for a shared prefix between a bifurcation of two competing blocks. + // For that, only the number of votes and existence of such snowflake instances matters, and nothing else. + voteCountsForIDsOrPrefixes := aggregateVotesFromPrefixesAndIDs(transitiveVotesForPrefixes, transitiveVotes) + + // Given the aforementioned votes, we wish to see whether there exists a snowflake instance + // that can benefit from waiting for more invocations of Vote(). + // We therefore check each amount of votes separately and see if voting for that snowflake instance + // should terminate, as it cannot be improved by further voting. + weCantImproveVoteForSomeIDOrPrefix := make(booleans, len(voteCountsForIDsOrPrefixes)) + for i, votesForID := range voteCountsForIDsOrPrefixes { + shouldTerminate := p.shouldTerminateDueToConfidence(votesForID, maxPossibleVotes, remaining) + weCantImproveVoteForSomeIDOrPrefix[i] = shouldTerminate + } + + // We should terminate the poll only when voting for all snowflake instances should terminate. + if weCantImproveVoteForSomeIDOrPrefix.allTrue() && len(weCantImproveVoteForSomeIDOrPrefix) > 0 { p.finished = true + } + + return p.finished +} + +func (p *earlyTermTraversalPoll) shouldTerminateDueToConfidence(freq int, maxPossibleVotes, remaining int) bool { + if freq+remaining < p.alphaPreference { + return true // Case 2 + } + + if freq >= p.alphaPreference && maxPossibleVotes < p.alphaConfidence { p.metrics.observeEarlyAlphaPref(time.Since(p.start)) return true // Case 3 } if freq >= p.alphaConfidence { - p.finished = true p.metrics.observeEarlyAlphaConf(time.Since(p.start)) return true // Case 4 } @@ -209,11 +261,11 @@ func (p *earlyTermNoTraversalPoll) Finished() bool { } // Result returns the result of this poll -func (p *earlyTermNoTraversalPoll) Result() bag.Bag[ids.ID] { +func (p *earlyTermTraversalPoll) Result() bag.Bag[ids.ID] { return p.votes } -func (p *earlyTermNoTraversalPoll) PrefixedString(prefix string) string { +func (p *earlyTermTraversalPoll) PrefixedString(prefix string) string { return fmt.Sprintf( "waiting on %s\n%sreceived %s", p.polled.PrefixedString(prefix), @@ -222,6 +274,75 @@ func (p *earlyTermNoTraversalPoll) PrefixedString(prefix string) string { ) } -func (p *earlyTermNoTraversalPoll) String() string { +func (p *earlyTermTraversalPoll) String() string { return p.PrefixedString("") } + +func aggregateVotesFromPrefixesAndIDs(transitiveVotesForPrefixes map[string]int, transitiveVotes bag.Bag[ids.ID]) []int { + voteCountsForIDsOrPrefixes := make([]int, 0, len(transitiveVotesForPrefixes)+len(transitiveVotes.List())) + + for _, id := range transitiveVotes.List() { + votesForID := transitiveVotes.Count(id) + voteCountsForIDsOrPrefixes = append(voteCountsForIDsOrPrefixes, votesForID) + } + + for _, voteCount := range transitiveVotesForPrefixes { + voteCountsForIDsOrPrefixes = append(voteCountsForIDsOrPrefixes, voteCount) + } + return voteCountsForIDsOrPrefixes +} + +func transitiveVotesForPrefixes(votesGraph *voteGraph, transitiveVotes bag.Bag[ids.ID]) map[string]int { + votesForPrefix := make(map[string]int) + votesGraph.traverse(func(v *voteVertex) { + descendanstIDs := descendantIDsOfVertex(v) + pg := longestSharedPrefixes(descendanstIDs) + // Each shared prefix is associated to a bunch of IDs. + // Sum up all the transitive votes for these blocks, + // and return all such shared prefixes indexed by the underlying transitive descendant IDs.. + pg.bifurcationsWithCommonPrefix(func(ids []ids.ID, _ []uint8) { + key := concatIDs(ids) + count := sumVotesFromIDs(ids, transitiveVotes) + votesForPrefix[key] = count + }) + }) + return votesForPrefix +} + +func descendantIDsOfVertex(v *voteVertex) []ids.ID { + descendanstIDs := make([]ids.ID, len(v.descendants)) + for i, child := range v.descendants { + descendanstIDs[i] = child.id + } + return descendanstIDs +} + +func concatIDs(ids []ids.ID) string { + var bb bytes.Buffer + for _, id := range ids { + bb.WriteString(id.String()) + bb.WriteString(" ") + } + return bb.String() +} + +func sumVotesFromIDs(ids []ids.ID, transitiveVotes bag.Bag[ids.ID]) int { + var count int + for _, id := range ids { + count += transitiveVotes.Count(id) + } + return count +} + +// booleans represents zero or more booleans +type booleans []bool + +// allTrue returns whether all booleans are true +func (bs booleans) allTrue() bool { + for _, b := range bs { + if !b { + return false + } + } + return true +} diff --git a/snow/consensus/snowman/poll/early_term_no_traversal_test.go b/snow/consensus/snowman/poll/early_term_no_traversal_test.go index 232169c01d41..b67d9396e608 100644 --- a/snow/consensus/snowman/poll/early_term_no_traversal_test.go +++ b/snow/consensus/snowman/poll/early_term_no_traversal_test.go @@ -9,15 +9,26 @@ import ( "github.com/prometheus/client_golang/prometheus" "github.com/stretchr/testify/require" + "github.com/ava-labs/avalanchego/ids" "github.com/ava-labs/avalanchego/utils/bag" ) +type parentGetter func(id ids.ID) ids.ID + +func (p parentGetter) GetParent(id ids.ID) ids.ID { + return p(id) +} + func newEarlyTermNoTraversalTestFactory(require *require.Assertions, alpha int) Factory { - factory, err := NewEarlyTermNoTraversalFactory(alpha, alpha, prometheus.NewRegistry()) + factory, err := NewEarlyTermTraversalFactory(alpha, alpha, prometheus.NewRegistry(), parentGetter(returnSelfID)) require.NoError(err) return factory } +func returnSelfID(id ids.ID) ids.ID { + return id +} + func TestEarlyTermNoTraversalResults(t *testing.T) { require := require.New(t) @@ -99,7 +110,7 @@ func TestEarlyTermNoTraversalTerminatesEarlyWithAlphaPreference(t *testing.T) { alphaPreference := 3 alphaConfidence := 5 - factory, err := NewEarlyTermNoTraversalFactory(alphaPreference, alphaConfidence, prometheus.NewRegistry()) + factory, err := NewEarlyTermTraversalFactory(alphaPreference, alphaConfidence, prometheus.NewRegistry(), parentGetter(returnSelfID)) require.NoError(err) poll := factory.New(vdrs) @@ -124,7 +135,7 @@ func TestEarlyTermNoTraversalTerminatesEarlyWithAlphaConfidence(t *testing.T) { alphaPreference := 3 alphaConfidence := 3 - factory, err := NewEarlyTermNoTraversalFactory(alphaPreference, alphaConfidence, prometheus.NewRegistry()) + factory, err := NewEarlyTermTraversalFactory(alphaPreference, alphaConfidence, prometheus.NewRegistry(), parentGetter(returnSelfID)) require.NoError(err) poll := factory.New(vdrs) @@ -149,7 +160,15 @@ func TestEarlyTermNoTraversalForSharedAncestor(t *testing.T) { vdrs := bag.Of(vdr1, vdr2, vdr3, vdr4) // k = 4 alpha := 4 - factory := newEarlyTermNoTraversalTestFactory(require, alpha) + g := ancestryGraph{ + blkID2: blkID1, + blkID3: blkID1, + blkID4: blkID1, + } + + factory, err := NewEarlyTermTraversalFactory(alpha, alpha, prometheus.NewRegistry(), g) + require.NoError(err) + poll := factory.New(vdrs) poll.Vote(vdr1, blkID2) @@ -196,3 +215,69 @@ func TestEarlyTermNoTraversalDropWithWeightedResponses(t *testing.T) { poll.Drop(vdr2) require.True(poll.Finished()) } + +type ancestryGraph map[ids.ID]ids.ID + +func (ag ancestryGraph) GetParent(id ids.ID) ids.ID { + parent, ok := ag[id] + if !ok { + return ids.Empty + } + return parent +} + +func TestTransitiveVotesForPrefixes(t *testing.T) { + require := require.New(t) + + g := &voteVertex{ + id: ids.ID{1}, + descendants: []*voteVertex{ + {id: ids.ID{2}}, + {id: ids.ID{4}}, + }, + } + wireParents(g) + getParent := getParent(g) + votes := bag.Of(ids.ID{1}, ids.ID{1}, ids.ID{2}, ids.ID{4}) + vg := buildVotesGraph(getParent, votes) + transitiveVotes := transitiveVotesForPrefixes(&vg, votes) + + var voteCount int + for _, count := range transitiveVotes { + voteCount = count + } + + require.Len(transitiveVotes, 1) + require.Equal(2, voteCount) +} + +func TestEarlyTermYesTraversal(t *testing.T) { + require := require.New(t) + + vdrs := bag.Of(vdr1, vdr2, vdr3, vdr4, vdr5) // k = 5 + alphaPreference := 2 + alphaConfidence := 3 + + // blkID1 + // blkID2 blkID4 + g := ancestryGraph{ + blkID4: blkID1, + blkID2: blkID1, + } + + factory, err := NewEarlyTermTraversalFactory(alphaPreference, alphaConfidence, prometheus.NewRegistry(), g) + require.NoError(err) + poll := factory.New(vdrs) + + poll.Vote(vdr1, blkID1) + require.False(poll.Finished()) + + poll.Vote(vdr2, blkID1) + require.False(poll.Finished()) + + poll.Vote(vdr3, blkID2) + require.False(poll.Finished()) + + poll.Vote(vdr4, blkID4) + require.False(poll.Finished()) +} diff --git a/snow/consensus/snowman/poll/graph.go b/snow/consensus/snowman/poll/graph.go new file mode 100644 index 000000000000..9a9edac49469 --- /dev/null +++ b/snow/consensus/snowman/poll/graph.go @@ -0,0 +1,154 @@ +// Copyright (C) 2019-2024, Ava Labs, Inc. All rights reserved. +// See the file LICENSE for licensing terms. + +package poll + +import ( + "github.com/ava-labs/avalanchego/ids" + "github.com/ava-labs/avalanchego/utils/bag" +) + +// voteVertex encapsulates an ID and points to its parent and descendants. +type voteVertex struct { + id ids.ID + parent *voteVertex + descendants []*voteVertex +} + +// traverse invokes f() on this voteVertex and all its descendants in pre-order traversal. +func (v *voteVertex) traverse(f func(*voteVertex)) { + f(v) + for _, u := range v.descendants { + u.traverse(f) + } +} + +// voteGraph is a collection of vote vertices. +// it must be initialized via buildVotesGraph() and not explicitly. +type voteGraph struct { + vertexCount int + leaves []*voteVertex + roots []*voteVertex +} + +// traverse traverses over all vertices in the voteGraph in pre-order traversal. +func (vg *voteGraph) traverse(f func(*voteVertex)) { + for _, root := range vg.roots { + root.traverse(f) + } +} + +// topologicalSortTraversal invokes f() on all vote vertices in the voteGraph according to +// the topological order of the vertices. +func (vg *voteGraph) topologicalSortTraversal(f func(*voteVertex)) { + // We hold a counter for each vertex + childCount := make(map[ids.ID]int, vg.vertexCount) + vg.traverse(func(v *voteVertex) { + childCount[v.id] = len(v.descendants) + }) + + leaves := make([]*voteVertex, len(vg.leaves)) + copy(leaves, vg.leaves) + + // Iterate from leaves to roots and recursively apply the predicate over each vertex + for len(leaves) > 0 { + newLeaves := make([]*voteVertex, 0, len(leaves)) + for _, leaf := range leaves { + f(leaf) + if leaf.parent == nil { + continue + } + childCount[leaf.parent.id]-- + if childCount[leaf.parent.id] == 0 { + newLeaves = append(newLeaves, leaf.parent) + } + } + leaves = newLeaves + } +} + +func clone(votes bag.Bag[ids.ID]) bag.Bag[ids.ID] { + var votesClone bag.Bag[ids.ID] + for _, id := range votes.List() { + votesClone.AddCount(id, votes.Count(id)) + } + return votesClone +} + +func findLeaves(id2Vertex map[ids.ID]*voteVertex) []*voteVertex { + leaves := make([]*voteVertex, 0, len(id2Vertex)) + for _, v := range id2Vertex { + if len(v.descendants) == 0 { + leaves = append(leaves, v) + } + } + return leaves +} + +// buildVotesGraph receives as input a function that returns the ID of a block, or Empty if unknown, +// as well as a bag of IDs (The bag is for enforcing uniqueness among the IDs in contrast to a list). +// It returns a voteGraph where each vertex corresponds to an ID and is linked to vertices +// according to what getParent() returns for each ID. +func buildVotesGraph(getParent func(ids.ID) ids.ID, votes bag.Bag[ids.ID]) voteGraph { + idList := votes.List() + + id2Vertex := make(map[ids.ID]*voteVertex, len(idList)) + roots := make([]*voteVertex, 0, len(idList)) + + // Build a graph out of the vertices that correspond to the IDs of the votes. + for _, id := range idList { + _, ok := id2Vertex[id] + if ok || id == ids.Empty { + continue + } + v := &voteVertex{id: id, descendants: make([]*voteVertex, 0, 2)} + id2Vertex[id] = v + } + + // Add the parents of the IDs to the graph, for those that are not already there. + for _, id := range idList { + parent := getParent(id) + // If this parent isn't found, it must be already finalized, so don't add it to the graph. + if parent == ids.Empty { + continue + } + _, ok := id2Vertex[parent] + // The parent is not finalized, so we can vote on it, therefore add it to the graph. + if !ok { + v := &voteVertex{id: parent, descendants: make([]*voteVertex, 0, 2)} + id2Vertex[parent] = v + } + } + + for id, v := range id2Vertex { + parent := getParent(id) + + u, ok := id2Vertex[parent] + if ok { + v.parent = u + u.descendants = append(u.descendants, v) + } else { + roots = append(roots, v) + } + } + + leaves := findLeaves(id2Vertex) + + return voteGraph{leaves: leaves, roots: roots, vertexCount: len(id2Vertex)} +} + +// computeTransitiveVoteCountGraph receives a vote graph and corresponding votes for each vertex ID. +// Returns a new bag where element represents the number of votes transitive descendents in the graph. +func computeTransitiveVoteCountGraph(graph *voteGraph, votes bag.Bag[ids.ID]) bag.Bag[ids.ID] { + transitiveClosureVotes := clone(votes) + + // Traverse from the leaves to the roots and recursively add the number of votes of descendents to each parent. + graph.topologicalSortTraversal(func(v *voteVertex) { + if v.parent == nil { + return + } + transitiveClosureVotes.AddCount(v.parent.id, transitiveClosureVotes.Count(v.id)) + }) + + return transitiveClosureVotes +} diff --git a/snow/consensus/snowman/poll/graph_test.go b/snow/consensus/snowman/poll/graph_test.go new file mode 100644 index 000000000000..620be14a265c --- /dev/null +++ b/snow/consensus/snowman/poll/graph_test.go @@ -0,0 +1,185 @@ +// Copyright (C) 2019-2024, Ava Labs, Inc. All rights reserved. +// See the file LICENSE for licensing terms. + +package poll + +import ( + "testing" + + "github.com/stretchr/testify/require" + + "github.com/ava-labs/avalanchego/ids" + "github.com/ava-labs/avalanchego/utils/bag" +) + +func TestBuildVotesGraph(t *testing.T) { + g := &voteVertex{ + id: ids.ID{1}, + descendants: []*voteVertex{ + {id: ids.ID{11}, descendants: []*voteVertex{ + {id: ids.ID{111}}, + {id: ids.ID{112}}, + {id: ids.ID{113}}, + }}, + {id: ids.ID{12}}, + {id: ids.ID{13}}, + }, + } + wireParents(g) + + getParent := getParent(g) + + var votes bag.Bag[ids.ID] + g.traverse(func(v *voteVertex) { + votes.Add(v.id) + }) + + parents := make(map[ids.ID]ids.ID) + children := make(map[ids.ID]*bag.Bag[ids.ID]) + + votesGraph := buildVotesGraph(getParent, votes) + votesGraph.traverse(func(v *voteVertex) { + if v.parent == nil { + parents[v.id] = ids.Empty + } else { + parents[v.id] = v.parent.id + } + + if len(v.descendants) == 0 { + return + } + + var childrenIDs bag.Bag[ids.ID] + for _, child := range v.descendants { + childrenIDs.Add(child.id) + } + children[v.id] = &childrenIDs + }) + + require.Equal(t, map[ids.ID]ids.ID{ + {1}: ids.Empty, + {11}: {1}, + {12}: {1}, + {13}: {1}, + {111}: {11}, + {112}: {11}, + {113}: {11}, + }, parents) + + expected1 := bag.Of(ids.ID{11}, ids.ID{12}, ids.ID{13}) + expected11 := bag.Of(ids.ID{111}, ids.ID{112}, ids.ID{113}) + + require.Len(t, children, 2) + require.True(t, children[ids.ID{1}].Equals(expected1)) + require.True(t, children[ids.ID{11}].Equals(expected11)) +} + +func getParent(g *voteVertex) func(id ids.ID) ids.ID { + return func(id ids.ID) ids.ID { + var result ids.ID + g.traverse(func(v *voteVertex) { + if v.id.Compare(id) == 0 { + if v.parent == nil { + result = ids.Empty + } else { + result = v.parent.id + } + } + }) + return result + } +} + +func TestComputeTransitiveVoteCountGraph(t *testing.T) { + g := &voteVertex{ + id: ids.ID{1}, + descendants: []*voteVertex{ + {id: ids.ID{11}, descendants: []*voteVertex{ + {id: ids.ID{111}}, + {id: ids.ID{112}}, + {id: ids.ID{113}}, + }}, + {id: ids.ID{12}}, + {id: ids.ID{13}}, + }, + } + wireParents(g) + var votes bag.Bag[ids.ID] + g.traverse(func(v *voteVertex) { + votes.Add(v.id) + }) + + getParent := getParent(g) + votesGraph := buildVotesGraph(getParent, votes) + transitiveVotes := computeTransitiveVoteCountGraph(&votesGraph, votes) + + expected := len(transitiveVotes.List()) + actual := votes.Len() + + require.Equal(t, expected, actual) + + for id, expectedVotes := range map[ids.ID]int{ + {12}: 1, + {13}: 1, + {111}: 1, + {112}: 1, + {113}: 1, + {11}: 4, + {1}: 7, + } { + require.Equal(t, expectedVotes, transitiveVotes.Count(id)) + } +} + +func TestTopologicalSortTraversal(t *testing.T) { + v1 := &voteVertex{id: ids.ID{1}} + v2 := &voteVertex{id: ids.ID{2}} + v3 := &voteVertex{id: ids.ID{3}} + v4 := &voteVertex{id: ids.ID{4}} + v5 := &voteVertex{id: ids.ID{5}} + v6 := &voteVertex{id: ids.ID{6}} + v7 := &voteVertex{id: ids.ID{7}} + v8 := &voteVertex{id: ids.ID{8}} + + // + // v7 v8 + // v3 v5 v4 v6 + // v1 v2 + + v1.parent = v3 + v2.parent = v4 + v3.parent = v7 + v4.parent = v8 + v5.parent = v7 + v6.parent = v8 + + v3.descendants = []*voteVertex{v1} + v4.descendants = []*voteVertex{v2} + v7.descendants = []*voteVertex{v3, v5} + v8.descendants = []*voteVertex{v4, v6} + + vg := voteGraph{leaves: []*voteVertex{v1, v2, v5, v6}, roots: []*voteVertex{v7, v8}, vertexCount: 8} + + order := make(map[ids.ID]int) + var currentOrder int + + vg.topologicalSortTraversal(func(v *voteVertex) { + order[v.id] = currentOrder + currentOrder++ + }) + + require.Less(t, order[v1.id], order[v3.id]) + require.Less(t, order[v3.id], order[v7.id]) + require.Less(t, order[v5.id], order[v7.id]) + require.Less(t, order[v2.id], order[v4.id]) + require.Less(t, order[v4.id], order[v8.id]) + require.Less(t, order[v6.id], order[v8.id]) +} + +func wireParents(v *voteVertex) { + v.traverse(func(vertex *voteVertex) { + for _, child := range vertex.descendants { + child.parent = vertex + } + }) +} diff --git a/snow/consensus/snowman/poll/prefix.go b/snow/consensus/snowman/poll/prefix.go new file mode 100644 index 000000000000..7b08440d82e8 --- /dev/null +++ b/snow/consensus/snowman/poll/prefix.go @@ -0,0 +1,186 @@ +// Copyright (C) 2019-2024, Ava Labs, Inc. All rights reserved. +// See the file LICENSE for licensing terms. + +package poll + +import ( + "fmt" + + "github.com/ava-labs/avalanchego/ids" +) + +// longestSharedPrefixes creates a prefixGroup that is the root of a graph +// of prefixGroup vertices. +// When iterating the graph, each prefixGroup vertex represents a shared bit prefix +// of IDs, and the members field contains all IDs from the given idList that their bit prefix +// matches the prefix field. +func longestSharedPrefixes(idList []ids.ID) *prefixGroup { + originPG := &prefixGroup{members: idList} + + pgs := make([]*prefixGroup, 0, len(idList)) + pgs = append(pgs, originPG) + + // Try to split each prefix group. + // Continue until all prefix groups cannot be split anymore. + for { + var couldSplit bool + for _, pg := range pgs { + if !pg.canSplit() { + continue + } + + couldSplit = true + + pg0, pg1 := pg.split() + + if pg0 != nil && pg1 != nil { + pgs = append(pgs, pg0) + pgs = append(pgs, pg1) + continue + } + + // Else, there is no bifurcation, + // so swallow up your descendant + descendant := determineDescendant(pg) + + // Backup the parent + parent := pg.parent + // Become your descendant, but keep your original parent + *pg = *descendant + pg.parent = parent + } + + if !couldSplit { + break + } + } + + return originPG +} + +func determineDescendant(pg *prefixGroup) *prefixGroup { + var descendant *prefixGroup + + if pg.zg == nil && pg.og == nil { + // If both are nil, it's a programming error, so panic. + panic("programming error: both zero group and one group are nil") + } + + if pg.zg != nil { + descendant = pg.zg + } + + if pg.og != nil { + descendant = pg.og + } + return descendant +} + +// prefixGroup represents a bunch of IDs (stored in the members field), +// with a bit prefix. +// Each time the prefixGroup is split, it is divided into one or more prefixGroups +// according to the next bit index in the index field. +// Successively splitting prefixGroups yields a graph, with the first prefixGroup as the root. +type prefixGroup struct { + // only used for tests to be readable. + vertexName rune + // the bit index this prefixGroup would be split according to upon next invocation of split(). + index int + // the bits of the members of this prefixGroup from the first bit to the bit index. + prefix []uint8 + // the IDs of the prefixGroup + members []ids.ID + // prefixGroups that correspond to zero and one being the first bit of their members, respectively. + zg, og *prefixGroup + // the prefixGroup that this prefixGroup was split from. + parent *prefixGroup + // was this prefixGroup split before. Used to prevent a prefixGroup to be split more than once, + // otherwise longestSharedPrefixes() would run indefinitely. + wasSplit bool +} + +// bifurcationsWithCommonPrefix invokes f() on this and descendant prefixGroups +// which represent common prefixes and not an ID in its entirety. +func (pg *prefixGroup) bifurcationsWithCommonPrefix(f func([]ids.ID, []uint8)) { + pg.traverse(func(prefixGroup *prefixGroup) { + if prefixGroup.isBifurcation() && len(prefixGroup.prefix) > 0 { + f(prefixGroup.members, prefixGroup.prefix) + } + }) +} + +// isBifurcation returns whether this prefixGroup has both zero and one bit descendants. +func (pg *prefixGroup) isBifurcation() bool { + return pg.zg != nil && pg.og != nil +} + +// canSplit returns whether this prefixGroup can be split. +func (pg *prefixGroup) canSplit() bool { + return !pg.wasSplit && len(pg.members) > 1 +} + +// traverse invokes f() on this prefixGroup and all descendants in pre-order traversal. +func (pg *prefixGroup) traverse(f func(*prefixGroup)) { + f(pg) + if pg.zg != nil { + pg.zg.traverse(f) + } + if pg.og != nil { + pg.og.traverse(f) + } +} + +// split splits the prefixGroup into two prefixGroups according +// to members and the next internal bit. +// All members in the current prefixGroup with bit zero in the next bit index are returned +// in the left result, and similarly for the bit one for the right result. +// Invariant: As long as the current prefixGroup can be split (canSplit() returns true), +// The method will never return (nil, nil), as if canSplit() returns true, the prefixGroup +// has at least two members, which means they either differ in the next bit index +func (pg *prefixGroup) split() (*prefixGroup, *prefixGroup) { + zg := &prefixGroup{ + parent: pg, + index: pg.index + 1, + prefix: make([]uint8, len(pg.prefix)+1), + members: make([]ids.ID, 0, len(pg.members)), + } + og := &prefixGroup{ + parent: pg, + index: pg.index + 1, + prefix: make([]uint8, len(pg.prefix)+1), + members: make([]ids.ID, 0, len(pg.members)), + } + + copy(zg.prefix, pg.prefix) + copy(og.prefix, pg.prefix) + zg.prefix[len(zg.prefix)-1] = 0 + og.prefix[len(og.prefix)-1] = 1 + + // Split members according to their next bit + for _, member := range pg.members { + bit := member.Bit(uint(pg.index)) + switch bit { + case 0: + zg.members = append(zg.members, member) + case 1: + og.members = append(og.members, member) + default: + // This is a sanity check which ensures that both zg.members and og.members cannot be empty. + panic(fmt.Sprintf("programming error: the %d bit of %s is %d", pg.index, member, bit)) + } + } + + if len(og.members) == 0 { + og = nil + } + + if len(zg.members) == 0 { + zg = nil + } + + pg.og = og + pg.zg = zg + pg.wasSplit = true + + return zg, og +} diff --git a/snow/consensus/snowman/poll/prefix_test.go b/snow/consensus/snowman/poll/prefix_test.go new file mode 100644 index 000000000000..942fdd913190 --- /dev/null +++ b/snow/consensus/snowman/poll/prefix_test.go @@ -0,0 +1,137 @@ +// Copyright (C) 2019-2024, Ava Labs, Inc. All rights reserved. +// See the file LICENSE for licensing terms. + +package poll + +import ( + "bytes" + "fmt" + "testing" + + "github.com/stretchr/testify/require" + + "github.com/ava-labs/avalanchego/ids" +) + +func nameVertices(pg *prefixGroup) { + nextVertexName := 'a' + pg.traverse(func(pg *prefixGroup) { + pg.vertexName = nextVertexName + nextVertexName++ + }) +} + +func TestSharedPrefixes(t *testing.T) { + for _, tst := range []struct { + name string + input []ids.ID + expectedEdges string + expectedMembers map[string][]ids.ID + expectedBifurcationsWithCommonPrefix [][]uint8 + expectedBifurcationsWithCommonPrefixMembers [][]ids.ID + }{ + { + name: "no shared prefix", + input: []ids.ID{{0xff, 0x0f}, {0x00, 0x1f}}, + expectedEdges: `(a,b)(a,c)`, + expectedMembers: map[string][]ids.ID{ + "a": {{0xff, 0x0f}, {0x00, 0x1f}}, + "b": {{0x00, 0x1f}}, + "c": {{0xff, 0x0f}}, + }, + expectedBifurcationsWithCommonPrefix: [][]uint8{}, + expectedBifurcationsWithCommonPrefixMembers: [][]ids.ID{}, + }, + { + name: "shared prefix for simple pair", + input: []ids.ID{blkID2, blkID4}, + expectedEdges: `(a,b)(a,c)`, + expectedMembers: map[string][]ids.ID{ + "a": {blkID2, blkID4}, + "b": {blkID4}, + "c": {blkID2}, + }, + expectedBifurcationsWithCommonPrefix: [][]uint8{{0x0}}, + expectedBifurcationsWithCommonPrefixMembers: [][]ids.ID{{blkID2, blkID4}}, + }, + { + name: "shared prefix for pair", + input: []ids.ID{{0xf0, 0x0f}, {0xf0, 0x1f}}, + expectedEdges: `(a,b)(a,c)`, + expectedMembers: map[string][]ids.ID{ + "a": {{0xf0, 0x0f}, {0xf0, 0x1f}}, + "b": {{0xf0, 0x0f}}, + "c": {{0xf0, 0x1f}}, + }, + expectedBifurcationsWithCommonPrefix: [][]uint8{{0, 0, 0, 0, 1, 1, 1, 1, 1, 1, 1, 1}}, + expectedBifurcationsWithCommonPrefixMembers: [][]ids.ID{{{0xf0, 0x0f}, {0xf0, 0x1f}}}, + }, + { + name: "shared prefix pair out of three descendants", + input: []ids.ID{{0xf0, 0xff}, {0xff, 0xf0}, {0x0f, 0xff}}, + expectedEdges: `(a,b)(a,c)(c,d)(c,e)`, + expectedMembers: map[string][]ids.ID{ + "a": {{0xf0, 0xff}, {0xff, 0xf0}, {0x0f, 0xff}}, + "b": {{0xf0, 0xff}}, + "c": {{0xff, 0xf0}, {0x0f, 0xff}}, + "d": {{0x0f, 0xff}}, + "e": {{0xff, 0xf0}}, + }, + expectedBifurcationsWithCommonPrefix: [][]uint8{{1, 1, 1, 1}}, + expectedBifurcationsWithCommonPrefixMembers: [][]ids.ID{{{0xff, 0xf0}, {0x0f, 0xff}}}, + }, + { + name: "shared prefix for quad", + input: []ids.ID{{0xff, 0xff}, {0xff, 0xf0}, {0x0f, 0xff}, {0x0f, 0xf0}}, + expectedEdges: `(a,b)(a,e)(b,c)(b,d)(e,f)(e,g)`, + expectedBifurcationsWithCommonPrefixMembers: [][]ids.ID{ + {{0xff, 0xff}, {0xff, 0xf0}, {0x0f, 0xff}, {0x0f, 0xf0}}, + {{0x0f, 0xff}, {0x0f, 0xf0}}, + {{0xff, 0xff}, {0xff, 0xf0}}, + }, + expectedMembers: map[string][]ids.ID{ + "a": {{0xff, 0xff}, {0xff, 0xf0}, {0x0f, 0xff}, {0x0f, 0xf0}}, + "e": {{0xff, 0xff}, {0xff, 0xf0}}, + "b": {{0x0f, 0xff}, {0x0f, 0xf0}}, + "d": {{0x0f, 0xff}}, + "c": {{0x0f, 0xf0}}, + "f": {{0xff, 0xf0}}, + "g": {{0xff, 0xff}}, + }, + expectedBifurcationsWithCommonPrefix: [][]uint8{ + {1, 1, 1, 1}, {1, 1, 1, 1, 0, 0, 0, 0}, {1, 1, 1, 1, 1, 1, 1, 1}, + }, + }, + } { + t.Run(tst.name, func(t *testing.T) { + pg := longestSharedPrefixes(tst.input) + nameVertices(pg) + + edges := bytes.Buffer{} + members := make(map[string][]ids.ID) + + pg.traverse(func(pg *prefixGroup) { + members[string(pg.vertexName)] = pg.members + if pg.zg != nil { + fmt.Fprintf(&edges, "(%s,%s)", string(pg.vertexName), string(pg.zg.vertexName)) + } + if pg.og != nil { + fmt.Fprintf(&edges, "(%s,%s)", string(pg.vertexName), string(pg.og.vertexName)) + } + }) + + bifurcationsWithCommonPrefixMembers := make([][]ids.ID, 0, len(tst.expectedBifurcationsWithCommonPrefixMembers)) + bifurcationsWithCommonPrefix := make([][]uint8, 0, len(tst.expectedBifurcationsWithCommonPrefix)) + + pg.bifurcationsWithCommonPrefix(func(members []ids.ID, prefix []uint8) { + bifurcationsWithCommonPrefixMembers = append(bifurcationsWithCommonPrefixMembers, members) + bifurcationsWithCommonPrefix = append(bifurcationsWithCommonPrefix, prefix) + }) + + require.Equal(t, tst.expectedEdges, edges.String()) + require.Equal(t, tst.expectedMembers, members) + require.Equal(t, tst.expectedBifurcationsWithCommonPrefix, bifurcationsWithCommonPrefix) + require.Equal(t, tst.expectedBifurcationsWithCommonPrefixMembers, bifurcationsWithCommonPrefixMembers) + }) + } +} diff --git a/snow/consensus/snowman/topological.go b/snow/consensus/snowman/topological.go index 9888652f0764..69a2f5c922c4 100644 --- a/snow/consensus/snowman/topological.go +++ b/snow/consensus/snowman/topological.go @@ -677,3 +677,14 @@ func (ts *Topological) rejectTransitively(ctx context.Context, rejected []ids.ID } return nil } + +func (ts *Topological) GetParent(id ids.ID) ids.ID { + if block, ok := ts.blocks[id]; ok { + if block.blk == nil { + return ids.Empty + } + return block.blk.Parent() + } else { + return ids.Empty + } +} diff --git a/snow/context.go b/snow/context.go index 2b0f896363c9..adc262e4af3a 100644 --- a/snow/context.go +++ b/snow/context.go @@ -95,3 +95,7 @@ type ConsensusContext struct { // True iff this chain is currently state-syncing StateSyncing utils.Atomic[bool] } + +type BlockTraversal interface { + GetParent(id ids.ID) ids.ID +} diff --git a/snow/engine/snowman/config.go b/snow/engine/snowman/config.go index 3162471a2476..c515df28abdb 100644 --- a/snow/engine/snowman/config.go +++ b/snow/engine/snowman/config.go @@ -16,7 +16,7 @@ import ( // Config wraps all the parameters needed for a snowman engine type Config struct { common.AllGetsServer - + BlockTraversal snow.BlockTraversal Ctx *snow.ConsensusContext VM block.ChainVM Sender common.Sender diff --git a/snow/engine/snowman/config_test.go b/snow/engine/snowman/config_test.go index fe13d4f474c1..07f3db4beadf 100644 --- a/snow/engine/snowman/config_test.go +++ b/snow/engine/snowman/config_test.go @@ -18,6 +18,8 @@ import ( func DefaultConfig(t testing.TB) Config { ctx := snowtest.Context(t, snowtest.PChainID) + top := &snowman.Topological{} + return Config{ Ctx: snowtest.ConsensusContext(ctx), VM: &blocktest.VM{}, @@ -34,6 +36,7 @@ func DefaultConfig(t testing.TB) Config { MaxOutstandingItems: 1, MaxItemProcessingTime: 1, }, - Consensus: &snowman.Topological{}, + Consensus: top, + BlockTraversal: top, } } diff --git a/snow/engine/snowman/engine.go b/snow/engine/snowman/engine.go index 6f1ff9960f32..046de5f8f9dc 100644 --- a/snow/engine/snowman/engine.go +++ b/snow/engine/snowman/engine.go @@ -110,10 +110,11 @@ func New(config Config) (*Engine, error) { acceptedFrontiers := tracker.NewAccepted() config.Validators.RegisterSetCallbackListener(config.Ctx.SubnetID, acceptedFrontiers) - factory, err := poll.NewEarlyTermNoTraversalFactory( + factory, err := poll.NewEarlyTermTraversalFactory( config.Params.AlphaPreference, config.Params.AlphaConfidence, config.Ctx.Registerer, + config.BlockTraversal, ) if err != nil { return nil, err From 8276ebe204e7d1d040880c3a17219dd863847b45 Mon Sep 17 00:00:00 2001 From: Yacov Manevich Date: Fri, 27 Sep 2024 18:16:02 +0200 Subject: [PATCH 2/8] Address code review comments Signed-off-by: Yacov Manevich --- .../snowman/poll/early_term_no_traversal.go | 24 +++++++++---------- .../poll/early_term_no_traversal_test.go | 6 ++--- snow/consensus/snowman/poll/graph.go | 20 ++++++++-------- snow/consensus/snowman/poll/graph_test.go | 10 ++++---- snow/consensus/snowman/poll/prefix.go | 10 ++++---- snow/consensus/snowman/poll/prefix_test.go | 15 +++++++----- 6 files changed, 43 insertions(+), 42 deletions(-) diff --git a/snow/consensus/snowman/poll/early_term_no_traversal.go b/snow/consensus/snowman/poll/early_term_no_traversal.go index ca19b8ad02a5..e415d5eb5878 100644 --- a/snow/consensus/snowman/poll/early_term_no_traversal.go +++ b/snow/consensus/snowman/poll/early_term_no_traversal.go @@ -182,8 +182,6 @@ func (p *earlyTermTraversalPoll) Finished() bool { } remaining := p.polled.Len() - received := p.votes.Len() - maxPossibleVotes := received + remaining if remaining == 0 { p.finished = true @@ -191,6 +189,8 @@ func (p *earlyTermTraversalPoll) Finished() bool { return true // Case 1 } + received := p.votes.Len() + maxPossibleVotes := received + remaining if maxPossibleVotes < p.alphaPreference { p.finished = true p.metrics.observeEarlyFail(time.Since(p.start)) @@ -203,7 +203,7 @@ func (p *earlyTermTraversalPoll) Finished() bool { // We build a vote graph where each vertex represents a block ID. // A vertex 'v' is a parent of vertex 'u' if the ID of 'u' corresponds // to a block that is the successive block of the corresponding block for 'v'. - votesGraph := buildVotesGraph(p.bt.GetParent, p.votes) + votesGraph := buildVoteGraph(p.bt.GetParent, p.votes) // If vertex 'v' is a parent of vertex 'u', then a vote for the ID of vertex 'u' // should also be considered as a vote for the ID of the vertex 'v'. @@ -217,11 +217,11 @@ func (p *earlyTermTraversalPoll) Finished() bool { // The prefix inherits the votes for the IDs of 'u' and 'w'. // We therefore compute the transitive votes for all prefixes of IDs // for each bifurcation in the transitive vote graph. - transitiveVotesForPrefixes := transitiveVotesForPrefixes(&votesGraph, transitiveVotes) + transitiveVotesForPrefixes := computeTransitiveVotesForPrefixes(&votesGraph, transitiveVotes) // We wish to compute the votes for snowflake instances, no matter if they correspond to an actual block ID, // or a unary snowflake instance for a shared prefix between a bifurcation of two competing blocks. - // For that, only the number of votes and existence of such snowflake instances matters, and nothing else. + // For that, only the number of votes and existence of such snowflake instances matters. voteCountsForIDsOrPrefixes := aggregateVotesFromPrefixesAndIDs(transitiveVotesForPrefixes, transitiveVotes) // Given the aforementioned votes, we wish to see whether there exists a snowflake instance @@ -229,8 +229,8 @@ func (p *earlyTermTraversalPoll) Finished() bool { // We therefore check each amount of votes separately and see if voting for that snowflake instance // should terminate, as it cannot be improved by further voting. weCantImproveVoteForSomeIDOrPrefix := make(booleans, len(voteCountsForIDsOrPrefixes)) - for i, votesForID := range voteCountsForIDsOrPrefixes { - shouldTerminate := p.shouldTerminateDueToConfidence(votesForID, maxPossibleVotes, remaining) + for i, completedVotes := range voteCountsForIDsOrPrefixes { + shouldTerminate := p.shouldTerminateDueToConfidence(completedVotes, maxPossibleVotes, remaining) weCantImproveVoteForSomeIDOrPrefix[i] = shouldTerminate } @@ -292,14 +292,14 @@ func aggregateVotesFromPrefixesAndIDs(transitiveVotesForPrefixes map[string]int, return voteCountsForIDsOrPrefixes } -func transitiveVotesForPrefixes(votesGraph *voteGraph, transitiveVotes bag.Bag[ids.ID]) map[string]int { +func computeTransitiveVotesForPrefixes(votesGraph *voteGraph, transitiveVotes bag.Bag[ids.ID]) map[string]int { votesForPrefix := make(map[string]int) votesGraph.traverse(func(v *voteVertex) { - descendanstIDs := descendantIDsOfVertex(v) - pg := longestSharedPrefixes(descendanstIDs) - // Each shared prefix is associated to a bunch of IDs. + descendantIDs := descendantIDsOfVertex(v) + pg := longestSharedPrefixes(descendantIDs) + // Each shared prefix is associated with a bunch of IDs. // Sum up all the transitive votes for these blocks, - // and return all such shared prefixes indexed by the underlying transitive descendant IDs.. + // and return all such shared prefixes indexed by the underlying transitive descendant IDs. pg.bifurcationsWithCommonPrefix(func(ids []ids.ID, _ []uint8) { key := concatIDs(ids) count := sumVotesFromIDs(ids, transitiveVotes) diff --git a/snow/consensus/snowman/poll/early_term_no_traversal_test.go b/snow/consensus/snowman/poll/early_term_no_traversal_test.go index b67d9396e608..b747c1c478b7 100644 --- a/snow/consensus/snowman/poll/early_term_no_traversal_test.go +++ b/snow/consensus/snowman/poll/early_term_no_traversal_test.go @@ -237,10 +237,10 @@ func TestTransitiveVotesForPrefixes(t *testing.T) { }, } wireParents(g) - getParent := getParent(g) + getParent := getParentFunc(g) votes := bag.Of(ids.ID{1}, ids.ID{1}, ids.ID{2}, ids.ID{4}) - vg := buildVotesGraph(getParent, votes) - transitiveVotes := transitiveVotesForPrefixes(&vg, votes) + vg := buildVoteGraph(getParent, votes) + transitiveVotes := computeTransitiveVotesForPrefixes(&vg, votes) var voteCount int for _, count := range transitiveVotes { diff --git a/snow/consensus/snowman/poll/graph.go b/snow/consensus/snowman/poll/graph.go index 9a9edac49469..b1e3f227969b 100644 --- a/snow/consensus/snowman/poll/graph.go +++ b/snow/consensus/snowman/poll/graph.go @@ -24,7 +24,7 @@ func (v *voteVertex) traverse(f func(*voteVertex)) { } // voteGraph is a collection of vote vertices. -// it must be initialized via buildVotesGraph() and not explicitly. +// it must be initialized via buildVoteGraph() and not explicitly. type voteGraph struct { vertexCount int leaves []*voteVertex @@ -50,7 +50,7 @@ func (vg *voteGraph) topologicalSortTraversal(f func(*voteVertex)) { leaves := make([]*voteVertex, len(vg.leaves)) copy(leaves, vg.leaves) - // Iterate from leaves to roots and recursively apply the predicate over each vertex + // Iterate from leaves to roots and recursively apply f() over each vertex for len(leaves) > 0 { newLeaves := make([]*voteVertex, 0, len(leaves)) for _, leaf := range leaves { @@ -75,9 +75,9 @@ func clone(votes bag.Bag[ids.ID]) bag.Bag[ids.ID] { return votesClone } -func findLeaves(id2Vertex map[ids.ID]*voteVertex) []*voteVertex { - leaves := make([]*voteVertex, 0, len(id2Vertex)) - for _, v := range id2Vertex { +func findLeaves(idToVertex map[ids.ID]*voteVertex) []*voteVertex { + leaves := make([]*voteVertex, 0, len(idToVertex)) + for _, v := range idToVertex { if len(v.descendants) == 0 { leaves = append(leaves, v) } @@ -85,11 +85,11 @@ func findLeaves(id2Vertex map[ids.ID]*voteVertex) []*voteVertex { return leaves } -// buildVotesGraph receives as input a function that returns the ID of a block, or Empty if unknown, +// buildVoteGraph receives as input a function that returns the ID of a block, or Empty if unknown, // as well as a bag of IDs (The bag is for enforcing uniqueness among the IDs in contrast to a list). // It returns a voteGraph where each vertex corresponds to an ID and is linked to vertices -// according to what getParent() returns for each ID. -func buildVotesGraph(getParent func(ids.ID) ids.ID, votes bag.Bag[ids.ID]) voteGraph { +// according to what getParentFunc() returns for each ID. +func buildVoteGraph(getParent func(ids.ID) ids.ID, votes bag.Bag[ids.ID]) voteGraph { idList := votes.List() id2Vertex := make(map[ids.ID]*voteVertex, len(idList)) @@ -113,7 +113,7 @@ func buildVotesGraph(getParent func(ids.ID) ids.ID, votes bag.Bag[ids.ID]) voteG continue } _, ok := id2Vertex[parent] - // The parent is not finalized, so we can vote on it, therefore add it to the graph. + // If the parent is not finalized we can vote on it, so add it to the graph if !ok { v := &voteVertex{id: parent, descendants: make([]*voteVertex, 0, 2)} id2Vertex[parent] = v @@ -138,7 +138,7 @@ func buildVotesGraph(getParent func(ids.ID) ids.ID, votes bag.Bag[ids.ID]) voteG } // computeTransitiveVoteCountGraph receives a vote graph and corresponding votes for each vertex ID. -// Returns a new bag where element represents the number of votes transitive descendents in the graph. +// Returns a new bag where element represents the number of votes for transitive descendents in the graph. func computeTransitiveVoteCountGraph(graph *voteGraph, votes bag.Bag[ids.ID]) bag.Bag[ids.ID] { transitiveClosureVotes := clone(votes) diff --git a/snow/consensus/snowman/poll/graph_test.go b/snow/consensus/snowman/poll/graph_test.go index 620be14a265c..e8d52773c5ec 100644 --- a/snow/consensus/snowman/poll/graph_test.go +++ b/snow/consensus/snowman/poll/graph_test.go @@ -27,7 +27,7 @@ func TestBuildVotesGraph(t *testing.T) { } wireParents(g) - getParent := getParent(g) + getParent := getParentFunc(g) var votes bag.Bag[ids.ID] g.traverse(func(v *voteVertex) { @@ -37,7 +37,7 @@ func TestBuildVotesGraph(t *testing.T) { parents := make(map[ids.ID]ids.ID) children := make(map[ids.ID]*bag.Bag[ids.ID]) - votesGraph := buildVotesGraph(getParent, votes) + votesGraph := buildVoteGraph(getParent, votes) votesGraph.traverse(func(v *voteVertex) { if v.parent == nil { parents[v.id] = ids.Empty @@ -74,7 +74,7 @@ func TestBuildVotesGraph(t *testing.T) { require.True(t, children[ids.ID{11}].Equals(expected11)) } -func getParent(g *voteVertex) func(id ids.ID) ids.ID { +func getParentFunc(g *voteVertex) func(id ids.ID) ids.ID { return func(id ids.ID) ids.ID { var result ids.ID g.traverse(func(v *voteVertex) { @@ -109,8 +109,8 @@ func TestComputeTransitiveVoteCountGraph(t *testing.T) { votes.Add(v.id) }) - getParent := getParent(g) - votesGraph := buildVotesGraph(getParent, votes) + getParent := getParentFunc(g) + votesGraph := buildVoteGraph(getParent, votes) transitiveVotes := computeTransitiveVoteCountGraph(&votesGraph, votes) expected := len(transitiveVotes.List()) diff --git a/snow/consensus/snowman/poll/prefix.go b/snow/consensus/snowman/poll/prefix.go index 7b08440d82e8..849c693fb3ad 100644 --- a/snow/consensus/snowman/poll/prefix.go +++ b/snow/consensus/snowman/poll/prefix.go @@ -12,7 +12,7 @@ import ( // longestSharedPrefixes creates a prefixGroup that is the root of a graph // of prefixGroup vertices. // When iterating the graph, each prefixGroup vertex represents a shared bit prefix -// of IDs, and the members field contains all IDs from the given idList that their bit prefix +// of IDs, and the members field contains all IDs from the given idList for which their bit prefix // matches the prefix field. func longestSharedPrefixes(idList []ids.ID) *prefixGroup { originPG := &prefixGroup{members: idList} @@ -82,9 +82,7 @@ func determineDescendant(pg *prefixGroup) *prefixGroup { // according to the next bit index in the index field. // Successively splitting prefixGroups yields a graph, with the first prefixGroup as the root. type prefixGroup struct { - // only used for tests to be readable. - vertexName rune - // the bit index this prefixGroup would be split according to upon next invocation of split(). + // the bit index this prefixGroup would be split on the next invocation of split(). index int // the bits of the members of this prefixGroup from the first bit to the bit index. prefix []uint8 @@ -94,7 +92,7 @@ type prefixGroup struct { zg, og *prefixGroup // the prefixGroup that this prefixGroup was split from. parent *prefixGroup - // was this prefixGroup split before. Used to prevent a prefixGroup to be split more than once, + // was this prefixGroup split before. Used to prevent a prefixGroup from being split more than once, // otherwise longestSharedPrefixes() would run indefinitely. wasSplit bool } @@ -135,7 +133,7 @@ func (pg *prefixGroup) traverse(f func(*prefixGroup)) { // All members in the current prefixGroup with bit zero in the next bit index are returned // in the left result, and similarly for the bit one for the right result. // Invariant: As long as the current prefixGroup can be split (canSplit() returns true), -// The method will never return (nil, nil), as if canSplit() returns true, the prefixGroup +// The method will never return (nil, nil), since if canSplit() returns true, the prefixGroup // has at least two members, which means they either differ in the next bit index func (pg *prefixGroup) split() (*prefixGroup, *prefixGroup) { zg := &prefixGroup{ diff --git a/snow/consensus/snowman/poll/prefix_test.go b/snow/consensus/snowman/poll/prefix_test.go index 942fdd913190..9818afd72911 100644 --- a/snow/consensus/snowman/poll/prefix_test.go +++ b/snow/consensus/snowman/poll/prefix_test.go @@ -13,12 +13,14 @@ import ( "github.com/ava-labs/avalanchego/ids" ) -func nameVertices(pg *prefixGroup) { +func nameVertices(pg *prefixGroup) map[*prefixGroup]string { + vertexNames := make(map[*prefixGroup]string) nextVertexName := 'a' pg.traverse(func(pg *prefixGroup) { - pg.vertexName = nextVertexName + vertexNames[pg] = string(nextVertexName) nextVertexName++ }) + return vertexNames } func TestSharedPrefixes(t *testing.T) { @@ -105,18 +107,19 @@ func TestSharedPrefixes(t *testing.T) { } { t.Run(tst.name, func(t *testing.T) { pg := longestSharedPrefixes(tst.input) - nameVertices(pg) + vertexNames := nameVertices(pg) edges := bytes.Buffer{} members := make(map[string][]ids.ID) pg.traverse(func(pg *prefixGroup) { - members[string(pg.vertexName)] = pg.members + pgVertexName := vertexNames[pg] + members[pgVertexName] = pg.members if pg.zg != nil { - fmt.Fprintf(&edges, "(%s,%s)", string(pg.vertexName), string(pg.zg.vertexName)) + fmt.Fprintf(&edges, "(%s,%s)", pgVertexName, vertexNames[pg.zg]) } if pg.og != nil { - fmt.Fprintf(&edges, "(%s,%s)", string(pg.vertexName), string(pg.og.vertexName)) + fmt.Fprintf(&edges, "(%s,%s)", pgVertexName, vertexNames[pg.og]) } }) From 6596f1575742cb32de51d64e40c9fbbc331e5460 Mon Sep 17 00:00:00 2001 From: Yacov Manevich Date: Sat, 28 Sep 2024 02:58:11 +0200 Subject: [PATCH 3/8] Address code review comments II Signed-off-by: Yacov Manevich --- snow/consensus/snowman/poll/graph.go | 2 +- snow/consensus/snowman/poll/prefix.go | 8 +++++--- 2 files changed, 6 insertions(+), 4 deletions(-) diff --git a/snow/consensus/snowman/poll/graph.go b/snow/consensus/snowman/poll/graph.go index b1e3f227969b..245169238b7b 100644 --- a/snow/consensus/snowman/poll/graph.go +++ b/snow/consensus/snowman/poll/graph.go @@ -88,7 +88,7 @@ func findLeaves(idToVertex map[ids.ID]*voteVertex) []*voteVertex { // buildVoteGraph receives as input a function that returns the ID of a block, or Empty if unknown, // as well as a bag of IDs (The bag is for enforcing uniqueness among the IDs in contrast to a list). // It returns a voteGraph where each vertex corresponds to an ID and is linked to vertices -// according to what getParentFunc() returns for each ID. +// according to what getParent() returns for each ID. func buildVoteGraph(getParent func(ids.ID) ids.ID, votes bag.Bag[ids.ID]) voteGraph { idList := votes.List() diff --git a/snow/consensus/snowman/poll/prefix.go b/snow/consensus/snowman/poll/prefix.go index 849c693fb3ad..268bbd21ec57 100644 --- a/snow/consensus/snowman/poll/prefix.go +++ b/snow/consensus/snowman/poll/prefix.go @@ -82,7 +82,7 @@ func determineDescendant(pg *prefixGroup) *prefixGroup { // according to the next bit index in the index field. // Successively splitting prefixGroups yields a graph, with the first prefixGroup as the root. type prefixGroup struct { - // the bit index this prefixGroup would be split on the next invocation of split(). + // the bit index this prefixGroup would be split on by the next invocation of split(). index int // the bits of the members of this prefixGroup from the first bit to the bit index. prefix []uint8 @@ -133,8 +133,10 @@ func (pg *prefixGroup) traverse(f func(*prefixGroup)) { // All members in the current prefixGroup with bit zero in the next bit index are returned // in the left result, and similarly for the bit one for the right result. // Invariant: As long as the current prefixGroup can be split (canSplit() returns true), -// The method will never return (nil, nil), since if canSplit() returns true, the prefixGroup -// has at least two members, which means they either differ in the next bit index +// If canSplit() returned true on this prefixGroup, split() will never return (nil, nil), +// since it has at least two members, which means they either differ in the next bit index, +// in which case two prefixGroups would be returned, and otherwise they do not differ +// in the next bit, and then at least one prefixGroup would be returned. func (pg *prefixGroup) split() (*prefixGroup, *prefixGroup) { zg := &prefixGroup{ parent: pg, From b327508c83ace72dfe27ca2e54311df0b1064e51 Mon Sep 17 00:00:00 2001 From: Yacov Manevich Date: Wed, 23 Oct 2024 19:16:55 +0200 Subject: [PATCH 4/8] Address code review comments III Signed-off-by: Yacov Manevich --- .../poll/early_term_no_traversal_test.go | 20 ++-- snow/consensus/snowman/poll/graph.go | 112 ++++++++---------- snow/consensus/snowman/poll/graph_test.go | 6 +- snow/consensus/snowman/poll/prefix.go | 42 +++---- snow/consensus/snowman/topological.go | 8 +- snow/context.go | 2 +- utils/bag/bag.go | 8 ++ utils/bag/bag_test.go | 12 ++ 8 files changed, 111 insertions(+), 99 deletions(-) diff --git a/snow/consensus/snowman/poll/early_term_no_traversal_test.go b/snow/consensus/snowman/poll/early_term_no_traversal_test.go index b747c1c478b7..31ab5eaa1b4f 100644 --- a/snow/consensus/snowman/poll/early_term_no_traversal_test.go +++ b/snow/consensus/snowman/poll/early_term_no_traversal_test.go @@ -13,20 +13,20 @@ import ( "github.com/ava-labs/avalanchego/utils/bag" ) -type parentGetter func(id ids.ID) ids.ID +type parentGetter func(id ids.ID) (ids.ID, bool) -func (p parentGetter) GetParent(id ids.ID) ids.ID { +func (p parentGetter) GetParent(id ids.ID) (ids.ID, bool) { return p(id) } func newEarlyTermNoTraversalTestFactory(require *require.Assertions, alpha int) Factory { - factory, err := NewEarlyTermTraversalFactory(alpha, alpha, prometheus.NewRegistry(), parentGetter(returnSelfID)) + factory, err := NewEarlyTermTraversalFactory(alpha, alpha, prometheus.NewRegistry(), parentGetter(returnEmpty)) require.NoError(err) return factory } -func returnSelfID(id ids.ID) ids.ID { - return id +func returnEmpty(_ ids.ID) (ids.ID, bool) { + return ids.Empty, false } func TestEarlyTermNoTraversalResults(t *testing.T) { @@ -110,7 +110,7 @@ func TestEarlyTermNoTraversalTerminatesEarlyWithAlphaPreference(t *testing.T) { alphaPreference := 3 alphaConfidence := 5 - factory, err := NewEarlyTermTraversalFactory(alphaPreference, alphaConfidence, prometheus.NewRegistry(), parentGetter(returnSelfID)) + factory, err := NewEarlyTermTraversalFactory(alphaPreference, alphaConfidence, prometheus.NewRegistry(), parentGetter(returnEmpty)) require.NoError(err) poll := factory.New(vdrs) @@ -135,7 +135,7 @@ func TestEarlyTermNoTraversalTerminatesEarlyWithAlphaConfidence(t *testing.T) { alphaPreference := 3 alphaConfidence := 3 - factory, err := NewEarlyTermTraversalFactory(alphaPreference, alphaConfidence, prometheus.NewRegistry(), parentGetter(returnSelfID)) + factory, err := NewEarlyTermTraversalFactory(alphaPreference, alphaConfidence, prometheus.NewRegistry(), parentGetter(returnEmpty)) require.NoError(err) poll := factory.New(vdrs) @@ -218,12 +218,12 @@ func TestEarlyTermNoTraversalDropWithWeightedResponses(t *testing.T) { type ancestryGraph map[ids.ID]ids.ID -func (ag ancestryGraph) GetParent(id ids.ID) ids.ID { +func (ag ancestryGraph) GetParent(id ids.ID) (ids.ID, bool) { parent, ok := ag[id] if !ok { - return ids.Empty + return ids.Empty, false } - return parent + return parent, true } func TestTransitiveVotesForPrefixes(t *testing.T) { diff --git a/snow/consensus/snowman/poll/graph.go b/snow/consensus/snowman/poll/graph.go index 245169238b7b..fb4f93af83f4 100644 --- a/snow/consensus/snowman/poll/graph.go +++ b/snow/consensus/snowman/poll/graph.go @@ -31,65 +31,11 @@ type voteGraph struct { roots []*voteVertex } -// traverse traverses over all vertices in the voteGraph in pre-order traversal. -func (vg *voteGraph) traverse(f func(*voteVertex)) { - for _, root := range vg.roots { - root.traverse(f) - } -} - -// topologicalSortTraversal invokes f() on all vote vertices in the voteGraph according to -// the topological order of the vertices. -func (vg *voteGraph) topologicalSortTraversal(f func(*voteVertex)) { - // We hold a counter for each vertex - childCount := make(map[ids.ID]int, vg.vertexCount) - vg.traverse(func(v *voteVertex) { - childCount[v.id] = len(v.descendants) - }) - - leaves := make([]*voteVertex, len(vg.leaves)) - copy(leaves, vg.leaves) - - // Iterate from leaves to roots and recursively apply f() over each vertex - for len(leaves) > 0 { - newLeaves := make([]*voteVertex, 0, len(leaves)) - for _, leaf := range leaves { - f(leaf) - if leaf.parent == nil { - continue - } - childCount[leaf.parent.id]-- - if childCount[leaf.parent.id] == 0 { - newLeaves = append(newLeaves, leaf.parent) - } - } - leaves = newLeaves - } -} - -func clone(votes bag.Bag[ids.ID]) bag.Bag[ids.ID] { - var votesClone bag.Bag[ids.ID] - for _, id := range votes.List() { - votesClone.AddCount(id, votes.Count(id)) - } - return votesClone -} - -func findLeaves(idToVertex map[ids.ID]*voteVertex) []*voteVertex { - leaves := make([]*voteVertex, 0, len(idToVertex)) - for _, v := range idToVertex { - if len(v.descendants) == 0 { - leaves = append(leaves, v) - } - } - return leaves -} - // buildVoteGraph receives as input a function that returns the ID of a block, or Empty if unknown, // as well as a bag of IDs (The bag is for enforcing uniqueness among the IDs in contrast to a list). // It returns a voteGraph where each vertex corresponds to an ID and is linked to vertices // according to what getParent() returns for each ID. -func buildVoteGraph(getParent func(ids.ID) ids.ID, votes bag.Bag[ids.ID]) voteGraph { +func buildVoteGraph(getParent func(ids.ID) (ids.ID, bool), votes bag.Bag[ids.ID]) voteGraph { idList := votes.List() id2Vertex := make(map[ids.ID]*voteVertex, len(idList)) @@ -107,12 +53,12 @@ func buildVoteGraph(getParent func(ids.ID) ids.ID, votes bag.Bag[ids.ID]) voteGr // Add the parents of the IDs to the graph, for those that are not already there. for _, id := range idList { - parent := getParent(id) + parent, ok := getParent(id) // If this parent isn't found, it must be already finalized, so don't add it to the graph. - if parent == ids.Empty { + if !ok { continue } - _, ok := id2Vertex[parent] + _, ok = id2Vertex[parent] // If the parent is not finalized we can vote on it, so add it to the graph if !ok { v := &voteVertex{id: parent, descendants: make([]*voteVertex, 0, 2)} @@ -121,7 +67,7 @@ func buildVoteGraph(getParent func(ids.ID) ids.ID, votes bag.Bag[ids.ID]) voteGr } for id, v := range id2Vertex { - parent := getParent(id) + parent, _ := getParent(id) u, ok := id2Vertex[parent] if ok { @@ -137,10 +83,56 @@ func buildVoteGraph(getParent func(ids.ID) ids.ID, votes bag.Bag[ids.ID]) voteGr return voteGraph{leaves: leaves, roots: roots, vertexCount: len(id2Vertex)} } +// traverse traverses over all vertices in the voteGraph in pre-order traversal. +func (vg *voteGraph) traverse(f func(*voteVertex)) { + for _, root := range vg.roots { + root.traverse(f) + } +} + +// topologicalSortTraversal invokes f() on all vote vertices in the voteGraph according to +// the topological order of the vertices. +func (vg *voteGraph) topologicalSortTraversal(f func(*voteVertex)) { + // We hold a counter for each vertex + childCount := make(map[ids.ID]int, vg.vertexCount) + vg.traverse(func(v *voteVertex) { + childCount[v.id] = len(v.descendants) + }) + + leaves := make([]*voteVertex, len(vg.leaves)) + copy(leaves, vg.leaves) + + // Iterate from leaves to roots and recursively apply f() over each vertex + for len(leaves) > 0 { + newLeaves := make([]*voteVertex, 0, len(leaves)) + for _, leaf := range leaves { + f(leaf) + if leaf.parent == nil { + continue + } + childCount[leaf.parent.id]-- + if childCount[leaf.parent.id] == 0 { + newLeaves = append(newLeaves, leaf.parent) + } + } + leaves = newLeaves + } +} + +func findLeaves(idToVertex map[ids.ID]*voteVertex) []*voteVertex { + leaves := make([]*voteVertex, 0, len(idToVertex)) + for _, v := range idToVertex { + if len(v.descendants) == 0 { + leaves = append(leaves, v) + } + } + return leaves +} + // computeTransitiveVoteCountGraph receives a vote graph and corresponding votes for each vertex ID. // Returns a new bag where element represents the number of votes for transitive descendents in the graph. func computeTransitiveVoteCountGraph(graph *voteGraph, votes bag.Bag[ids.ID]) bag.Bag[ids.ID] { - transitiveClosureVotes := clone(votes) + transitiveClosureVotes := votes.Clone() // Traverse from the leaves to the roots and recursively add the number of votes of descendents to each parent. graph.topologicalSortTraversal(func(v *voteVertex) { diff --git a/snow/consensus/snowman/poll/graph_test.go b/snow/consensus/snowman/poll/graph_test.go index e8d52773c5ec..41f376c26ed5 100644 --- a/snow/consensus/snowman/poll/graph_test.go +++ b/snow/consensus/snowman/poll/graph_test.go @@ -74,8 +74,8 @@ func TestBuildVotesGraph(t *testing.T) { require.True(t, children[ids.ID{11}].Equals(expected11)) } -func getParentFunc(g *voteVertex) func(id ids.ID) ids.ID { - return func(id ids.ID) ids.ID { +func getParentFunc(g *voteVertex) func(id ids.ID) (ids.ID, bool) { + return func(id ids.ID) (ids.ID, bool) { var result ids.ID g.traverse(func(v *voteVertex) { if v.id.Compare(id) == 0 { @@ -86,7 +86,7 @@ func getParentFunc(g *voteVertex) func(id ids.ID) ids.ID { } } }) - return result + return result, result != ids.Empty } } diff --git a/snow/consensus/snowman/poll/prefix.go b/snow/consensus/snowman/poll/prefix.go index 268bbd21ec57..f068fe39392e 100644 --- a/snow/consensus/snowman/poll/prefix.go +++ b/snow/consensus/snowman/poll/prefix.go @@ -9,6 +9,27 @@ import ( "github.com/ava-labs/avalanchego/ids" ) +// prefixGroup represents a bunch of IDs (stored in the members field), +// with a bit prefix. +// Each time the prefixGroup is split, it is divided into one or more prefixGroups +// according to the next bit index in the index field. +// Successively splitting prefixGroups yields a graph, with the first prefixGroup as the root. +type prefixGroup struct { + // the bit index this prefixGroup would be split on by the next invocation of split(). + index int + // the bits of the members of this prefixGroup from the first bit to the bit index. + prefix []uint8 + // the IDs of the prefixGroup + members []ids.ID + // prefixGroups that correspond to zero and one being the first bit of their members, respectively. + zg, og *prefixGroup + // the prefixGroup that this prefixGroup was split from. + parent *prefixGroup + // was this prefixGroup split before. Used to prevent a prefixGroup from being split more than once, + // otherwise longestSharedPrefixes() would run indefinitely. + wasSplit bool +} + // longestSharedPrefixes creates a prefixGroup that is the root of a graph // of prefixGroup vertices. // When iterating the graph, each prefixGroup vertex represents a shared bit prefix @@ -76,27 +97,6 @@ func determineDescendant(pg *prefixGroup) *prefixGroup { return descendant } -// prefixGroup represents a bunch of IDs (stored in the members field), -// with a bit prefix. -// Each time the prefixGroup is split, it is divided into one or more prefixGroups -// according to the next bit index in the index field. -// Successively splitting prefixGroups yields a graph, with the first prefixGroup as the root. -type prefixGroup struct { - // the bit index this prefixGroup would be split on by the next invocation of split(). - index int - // the bits of the members of this prefixGroup from the first bit to the bit index. - prefix []uint8 - // the IDs of the prefixGroup - members []ids.ID - // prefixGroups that correspond to zero and one being the first bit of their members, respectively. - zg, og *prefixGroup - // the prefixGroup that this prefixGroup was split from. - parent *prefixGroup - // was this prefixGroup split before. Used to prevent a prefixGroup from being split more than once, - // otherwise longestSharedPrefixes() would run indefinitely. - wasSplit bool -} - // bifurcationsWithCommonPrefix invokes f() on this and descendant prefixGroups // which represent common prefixes and not an ID in its entirety. func (pg *prefixGroup) bifurcationsWithCommonPrefix(f func([]ids.ID, []uint8)) { diff --git a/snow/consensus/snowman/topological.go b/snow/consensus/snowman/topological.go index 69a2f5c922c4..061a0a4eb1a3 100644 --- a/snow/consensus/snowman/topological.go +++ b/snow/consensus/snowman/topological.go @@ -678,13 +678,13 @@ func (ts *Topological) rejectTransitively(ctx context.Context, rejected []ids.ID return nil } -func (ts *Topological) GetParent(id ids.ID) ids.ID { +func (ts *Topological) GetParent(id ids.ID) (ids.ID, bool) { if block, ok := ts.blocks[id]; ok { if block.blk == nil { - return ids.Empty + return ids.Empty, false } - return block.blk.Parent() + return block.blk.Parent(), true } else { - return ids.Empty + return ids.Empty, false } } diff --git a/snow/context.go b/snow/context.go index adc262e4af3a..70ad86b7b200 100644 --- a/snow/context.go +++ b/snow/context.go @@ -97,5 +97,5 @@ type ConsensusContext struct { } type BlockTraversal interface { - GetParent(id ids.ID) ids.ID + GetParent(id ids.ID) (ids.ID, bool) } diff --git a/utils/bag/bag.go b/utils/bag/bag.go index a9af1acbcf49..7e9e8d69d064 100644 --- a/utils/bag/bag.go +++ b/utils/bag/bag.go @@ -174,3 +174,11 @@ func (b *Bag[T]) PrefixedString(prefix string) string { func (b *Bag[_]) String() string { return b.PrefixedString("") } + +func (b *Bag[T]) Clone() Bag[T] { + var clone Bag[T] + for _, id := range b.List() { + clone.AddCount(id, b.Count(id)) + } + return clone +} diff --git a/utils/bag/bag_test.go b/utils/bag/bag_test.go index 3b6e0faa07f0..07bbaaa2fb67 100644 --- a/utils/bag/bag_test.go +++ b/utils/bag/bag_test.go @@ -287,3 +287,15 @@ func TestBagEquals(t *testing.T) { require.True(bag1.Equals(bag2)) require.True(bag2.Equals(bag1)) } + +func TestBagClone(t *testing.T) { + require := require.New(t) + bag := Bag[int]{} + bag.AddCount(100, 1) + bag.AddCount(200, 2) + bag.AddCount(300, 3) + + clonedBag := bag.Clone() + + require.Equal(bag, clonedBag) +} From 1435a737c74ef5ed98dcebde1593bdda7dc0e868 Mon Sep 17 00:00:00 2001 From: Yacov Manevich Date: Wed, 30 Oct 2024 23:14:02 +0100 Subject: [PATCH 5/8] Address code review comments IV Signed-off-by: Yacov Manevich --- snow/consensus/snowman/poll/prefix_test.go | 7 +++++++ 1 file changed, 7 insertions(+) diff --git a/snow/consensus/snowman/poll/prefix_test.go b/snow/consensus/snowman/poll/prefix_test.go index 9818afd72911..84c9523cb964 100644 --- a/snow/consensus/snowman/poll/prefix_test.go +++ b/snow/consensus/snowman/poll/prefix_test.go @@ -123,6 +123,13 @@ func TestSharedPrefixes(t *testing.T) { } }) + // bifurcationsWithCommonPrefix holds common prefixes of prefix groups as bifurcationsWithCommonPrefix() + // traverses starting from the top prefix group. + // It does not contain leaf nodes, as bifurcationsWithCommonPrefix() does not visit leaf nodes in its traversal. + + // bifurcationsWithCommonPrefixMembers contains the IDs of the prefix group that corresponds to the prefix. + // The first bifurcationsWithCommonPrefixMembers corresponds to the input, as + bifurcationsWithCommonPrefixMembers := make([][]ids.ID, 0, len(tst.expectedBifurcationsWithCommonPrefixMembers)) bifurcationsWithCommonPrefix := make([][]uint8, 0, len(tst.expectedBifurcationsWithCommonPrefix)) From 4dc167e7d710fd3f0624c669fc51835e000911c6 Mon Sep 17 00:00:00 2001 From: Yacov Manevich Date: Wed, 30 Oct 2024 23:31:17 +0100 Subject: [PATCH 6/8] document TestSharedPrefixes test fields Signed-off-by: Yacov Manevich --- snow/consensus/snowman/poll/prefix_test.go | 52 +++++++++++----------- 1 file changed, 27 insertions(+), 25 deletions(-) diff --git a/snow/consensus/snowman/poll/prefix_test.go b/snow/consensus/snowman/poll/prefix_test.go index 84c9523cb964..a8f82c1c7e96 100644 --- a/snow/consensus/snowman/poll/prefix_test.go +++ b/snow/consensus/snowman/poll/prefix_test.go @@ -25,12 +25,12 @@ func nameVertices(pg *prefixGroup) map[*prefixGroup]string { func TestSharedPrefixes(t *testing.T) { for _, tst := range []struct { - name string - input []ids.ID - expectedEdges string - expectedMembers map[string][]ids.ID - expectedBifurcationsWithCommonPrefix [][]uint8 - expectedBifurcationsWithCommonPrefixMembers [][]ids.ID + name string + input []ids.ID + expectedEdges string + expectedMembers map[string][]ids.ID + expectedCommonPrefixes [][]uint8 + expectedCommonPrefixMembers [][]ids.ID }{ { name: "no shared prefix", @@ -41,8 +41,8 @@ func TestSharedPrefixes(t *testing.T) { "b": {{0x00, 0x1f}}, "c": {{0xff, 0x0f}}, }, - expectedBifurcationsWithCommonPrefix: [][]uint8{}, - expectedBifurcationsWithCommonPrefixMembers: [][]ids.ID{}, + expectedCommonPrefixes: [][]uint8{}, + expectedCommonPrefixMembers: [][]ids.ID{}, }, { name: "shared prefix for simple pair", @@ -53,8 +53,8 @@ func TestSharedPrefixes(t *testing.T) { "b": {blkID4}, "c": {blkID2}, }, - expectedBifurcationsWithCommonPrefix: [][]uint8{{0x0}}, - expectedBifurcationsWithCommonPrefixMembers: [][]ids.ID{{blkID2, blkID4}}, + expectedCommonPrefixes: [][]uint8{{0x0}}, + expectedCommonPrefixMembers: [][]ids.ID{{blkID2, blkID4}}, }, { name: "shared prefix for pair", @@ -65,8 +65,8 @@ func TestSharedPrefixes(t *testing.T) { "b": {{0xf0, 0x0f}}, "c": {{0xf0, 0x1f}}, }, - expectedBifurcationsWithCommonPrefix: [][]uint8{{0, 0, 0, 0, 1, 1, 1, 1, 1, 1, 1, 1}}, - expectedBifurcationsWithCommonPrefixMembers: [][]ids.ID{{{0xf0, 0x0f}, {0xf0, 0x1f}}}, + expectedCommonPrefixes: [][]uint8{{0, 0, 0, 0, 1, 1, 1, 1, 1, 1, 1, 1}}, + expectedCommonPrefixMembers: [][]ids.ID{{{0xf0, 0x0f}, {0xf0, 0x1f}}}, }, { name: "shared prefix pair out of three descendants", @@ -79,14 +79,14 @@ func TestSharedPrefixes(t *testing.T) { "d": {{0x0f, 0xff}}, "e": {{0xff, 0xf0}}, }, - expectedBifurcationsWithCommonPrefix: [][]uint8{{1, 1, 1, 1}}, - expectedBifurcationsWithCommonPrefixMembers: [][]ids.ID{{{0xff, 0xf0}, {0x0f, 0xff}}}, + expectedCommonPrefixes: [][]uint8{{1, 1, 1, 1}}, + expectedCommonPrefixMembers: [][]ids.ID{{{0xff, 0xf0}, {0x0f, 0xff}}}, }, { name: "shared prefix for quad", input: []ids.ID{{0xff, 0xff}, {0xff, 0xf0}, {0x0f, 0xff}, {0x0f, 0xf0}}, expectedEdges: `(a,b)(a,e)(b,c)(b,d)(e,f)(e,g)`, - expectedBifurcationsWithCommonPrefixMembers: [][]ids.ID{ + expectedCommonPrefixMembers: [][]ids.ID{ {{0xff, 0xff}, {0xff, 0xf0}, {0x0f, 0xff}, {0x0f, 0xf0}}, {{0x0f, 0xff}, {0x0f, 0xf0}}, {{0xff, 0xff}, {0xff, 0xf0}}, @@ -100,7 +100,7 @@ func TestSharedPrefixes(t *testing.T) { "f": {{0xff, 0xf0}}, "g": {{0xff, 0xff}}, }, - expectedBifurcationsWithCommonPrefix: [][]uint8{ + expectedCommonPrefixes: [][]uint8{ {1, 1, 1, 1}, {1, 1, 1, 1, 0, 0, 0, 0}, {1, 1, 1, 1, 1, 1, 1, 1}, }, }, @@ -123,25 +123,27 @@ func TestSharedPrefixes(t *testing.T) { } }) - // bifurcationsWithCommonPrefix holds common prefixes of prefix groups as bifurcationsWithCommonPrefix() + // commonPrefixMembers holds common prefixes of prefix groups as bifurcationsWithCommonPrefix() // traverses starting from the top prefix group. // It does not contain leaf nodes, as bifurcationsWithCommonPrefix() does not visit leaf nodes in its traversal. - // bifurcationsWithCommonPrefixMembers contains the IDs of the prefix group that corresponds to the prefix. - // The first bifurcationsWithCommonPrefixMembers corresponds to the input, as + // commonPrefixMembers contains the IDs of the prefix group that corresponds to the commonPrefixes. + // The intention of having it is showing how each bifurcation splits the members according to their common prefix. + // Had bifurcationsWithCommonPrefix() visited leaf prefix groups, the commonPrefixMembers would shrink down to a single ID. + // Since bifurcationsWithCommonPrefix() doesn't visit leaf prefix groups, commonPrefixMembers can contain sets of sets as small as two elements. - bifurcationsWithCommonPrefixMembers := make([][]ids.ID, 0, len(tst.expectedBifurcationsWithCommonPrefixMembers)) - bifurcationsWithCommonPrefix := make([][]uint8, 0, len(tst.expectedBifurcationsWithCommonPrefix)) + commonPrefixMembers := make([][]ids.ID, 0, len(tst.expectedCommonPrefixMembers)) + commonPrefixes := make([][]uint8, 0, len(tst.expectedCommonPrefixes)) pg.bifurcationsWithCommonPrefix(func(members []ids.ID, prefix []uint8) { - bifurcationsWithCommonPrefixMembers = append(bifurcationsWithCommonPrefixMembers, members) - bifurcationsWithCommonPrefix = append(bifurcationsWithCommonPrefix, prefix) + commonPrefixMembers = append(commonPrefixMembers, members) + commonPrefixes = append(commonPrefixes, prefix) }) require.Equal(t, tst.expectedEdges, edges.String()) require.Equal(t, tst.expectedMembers, members) - require.Equal(t, tst.expectedBifurcationsWithCommonPrefix, bifurcationsWithCommonPrefix) - require.Equal(t, tst.expectedBifurcationsWithCommonPrefixMembers, bifurcationsWithCommonPrefixMembers) + require.Equal(t, tst.expectedCommonPrefixes, commonPrefixes) + require.Equal(t, tst.expectedCommonPrefixMembers, commonPrefixMembers) }) } } From 99deea8560c30520c2b29d573e63c3b2ce37bbc9 Mon Sep 17 00:00:00 2001 From: Yacov Manevich Date: Fri, 15 Nov 2024 20:08:45 +0100 Subject: [PATCH 7/8] Simplify test, and use local max votes instead of global Signed-off-by: Yacov Manevich --- .../snowman/poll/early_term_no_traversal.go | 9 +- .../poll/early_term_no_traversal_test.go | 47 ++++++++++ snow/consensus/snowman/poll/prefix.go | 11 ++- snow/consensus/snowman/poll/prefix_test.go | 86 ++++++++++--------- 4 files changed, 104 insertions(+), 49 deletions(-) diff --git a/snow/consensus/snowman/poll/early_term_no_traversal.go b/snow/consensus/snowman/poll/early_term_no_traversal.go index e415d5eb5878..4e931736ebfa 100644 --- a/snow/consensus/snowman/poll/early_term_no_traversal.go +++ b/snow/consensus/snowman/poll/early_term_no_traversal.go @@ -230,7 +230,7 @@ func (p *earlyTermTraversalPoll) Finished() bool { // should terminate, as it cannot be improved by further voting. weCantImproveVoteForSomeIDOrPrefix := make(booleans, len(voteCountsForIDsOrPrefixes)) for i, completedVotes := range voteCountsForIDsOrPrefixes { - shouldTerminate := p.shouldTerminateDueToConfidence(completedVotes, maxPossibleVotes, remaining) + shouldTerminate := p.shouldTerminateDueToConfidence(completedVotes, remaining) weCantImproveVoteForSomeIDOrPrefix[i] = shouldTerminate } @@ -242,8 +242,9 @@ func (p *earlyTermTraversalPoll) Finished() bool { return p.finished } -func (p *earlyTermTraversalPoll) shouldTerminateDueToConfidence(freq int, maxPossibleVotes, remaining int) bool { - if freq+remaining < p.alphaPreference { +func (p *earlyTermTraversalPoll) shouldTerminateDueToConfidence(freq int, remaining int) bool { + maxPossibleVotes := freq + remaining + if maxPossibleVotes < p.alphaPreference { return true // Case 2 } @@ -300,7 +301,7 @@ func computeTransitiveVotesForPrefixes(votesGraph *voteGraph, transitiveVotes ba // Each shared prefix is associated with a bunch of IDs. // Sum up all the transitive votes for these blocks, // and return all such shared prefixes indexed by the underlying transitive descendant IDs. - pg.bifurcationsWithCommonPrefix(func(ids []ids.ID, _ []uint8) { + pg.bifurcationsWithCommonPrefix(func(ids []ids.ID) { key := concatIDs(ids) count := sumVotesFromIDs(ids, transitiveVotes) votesForPrefix[key] = count diff --git a/snow/consensus/snowman/poll/early_term_no_traversal_test.go b/snow/consensus/snowman/poll/early_term_no_traversal_test.go index 31ab5eaa1b4f..e0d9c1c0c5be 100644 --- a/snow/consensus/snowman/poll/early_term_no_traversal_test.go +++ b/snow/consensus/snowman/poll/early_term_no_traversal_test.go @@ -281,3 +281,50 @@ func TestEarlyTermYesTraversal(t *testing.T) { poll.Vote(vdr4, blkID4) require.False(poll.Finished()) } + +func TestEarlyTermYesTraversalII(t *testing.T) { + require := require.New(t) + + vdrs := bag.Of(vdr1, vdr2, vdr3, vdr4, vdr5) // k = 5 + alphaPreference := 2 + alphaConfidence := 3 + + blkID0 := ids.ID{0x00, 0x00} + blkID1 := ids.ID{0xf0, 0xff} + blkID2 := ids.ID{0xff, 0xf0} + blkID3 := ids.ID{0x0f, 0xff} + + // blkID0 + // blkID1 blkID2 blkID3 + g := ancestryGraph{ + blkID3: blkID0, + blkID2: blkID0, + blkID1: blkID0, + } + + factory, err := NewEarlyTermTraversalFactory(alphaPreference, alphaConfidence, prometheus.NewRegistry(), g) + require.NoError(err) + poll := factory.New(vdrs) + + poll.Vote(vdr1, blkID2) + require.False(poll.Finished()) + + poll.Vote(vdr2, blkID3) + require.False(poll.Finished()) + + poll.Vote(vdr3, blkID3) + require.False(poll.Finished()) + + poll.Vote(vdr4, blkID3) + require.False(poll.Finished()) + + // + // blkID0 blk0: {0x00, 0x00} + // 0/ \4 blk1: {0xf0, 0xff} + // blkID1 {0x?f} blk2: {0xff, 0xf0} + // 1/ \3 blk3: {0x0f, 0xff} + // blkID2 blkID3 + + poll.Vote(vdr5, blkID2) + require.True(poll.Finished()) +} diff --git a/snow/consensus/snowman/poll/prefix.go b/snow/consensus/snowman/poll/prefix.go index f068fe39392e..8015dd189d3b 100644 --- a/snow/consensus/snowman/poll/prefix.go +++ b/snow/consensus/snowman/poll/prefix.go @@ -97,12 +97,15 @@ func determineDescendant(pg *prefixGroup) *prefixGroup { return descendant } -// bifurcationsWithCommonPrefix invokes f() on this and descendant prefixGroups -// which represent common prefixes and not an ID in its entirety. -func (pg *prefixGroup) bifurcationsWithCommonPrefix(f func([]ids.ID, []uint8)) { +// bifurcationsWithCommonPrefix traverses the transitive descendants of this prefix group, +// and applies f() on the block IDs of each prefix group. +// Prefix groups with no descendants are skipped, as they do not represent any prefix. +// Prefix group without a prefix (root prefix group) are also skipped as they do not correspond +// to any instance of snowflake. +func (pg *prefixGroup) bifurcationsWithCommonPrefix(f func([]ids.ID)) { pg.traverse(func(prefixGroup *prefixGroup) { if prefixGroup.isBifurcation() && len(prefixGroup.prefix) > 0 { - f(prefixGroup.members, prefixGroup.prefix) + f(prefixGroup.members) } }) } diff --git a/snow/consensus/snowman/poll/prefix_test.go b/snow/consensus/snowman/poll/prefix_test.go index a8f82c1c7e96..0773793135fc 100644 --- a/snow/consensus/snowman/poll/prefix_test.go +++ b/snow/consensus/snowman/poll/prefix_test.go @@ -25,12 +25,10 @@ func nameVertices(pg *prefixGroup) map[*prefixGroup]string { func TestSharedPrefixes(t *testing.T) { for _, tst := range []struct { - name string - input []ids.ID - expectedEdges string - expectedMembers map[string][]ids.ID - expectedCommonPrefixes [][]uint8 - expectedCommonPrefixMembers [][]ids.ID + name string + input []ids.ID + expectedEdges string + expectedMembers map[string][]ids.ID }{ { name: "no shared prefix", @@ -41,8 +39,6 @@ func TestSharedPrefixes(t *testing.T) { "b": {{0x00, 0x1f}}, "c": {{0xff, 0x0f}}, }, - expectedCommonPrefixes: [][]uint8{}, - expectedCommonPrefixMembers: [][]ids.ID{}, }, { name: "shared prefix for simple pair", @@ -53,8 +49,6 @@ func TestSharedPrefixes(t *testing.T) { "b": {blkID4}, "c": {blkID2}, }, - expectedCommonPrefixes: [][]uint8{{0x0}}, - expectedCommonPrefixMembers: [][]ids.ID{{blkID2, blkID4}}, }, { name: "shared prefix for pair", @@ -65,8 +59,6 @@ func TestSharedPrefixes(t *testing.T) { "b": {{0xf0, 0x0f}}, "c": {{0xf0, 0x1f}}, }, - expectedCommonPrefixes: [][]uint8{{0, 0, 0, 0, 1, 1, 1, 1, 1, 1, 1, 1}}, - expectedCommonPrefixMembers: [][]ids.ID{{{0xf0, 0x0f}, {0xf0, 0x1f}}}, }, { name: "shared prefix pair out of three descendants", @@ -79,18 +71,11 @@ func TestSharedPrefixes(t *testing.T) { "d": {{0x0f, 0xff}}, "e": {{0xff, 0xf0}}, }, - expectedCommonPrefixes: [][]uint8{{1, 1, 1, 1}}, - expectedCommonPrefixMembers: [][]ids.ID{{{0xff, 0xf0}, {0x0f, 0xff}}}, }, { name: "shared prefix for quad", input: []ids.ID{{0xff, 0xff}, {0xff, 0xf0}, {0x0f, 0xff}, {0x0f, 0xf0}}, expectedEdges: `(a,b)(a,e)(b,c)(b,d)(e,f)(e,g)`, - expectedCommonPrefixMembers: [][]ids.ID{ - {{0xff, 0xff}, {0xff, 0xf0}, {0x0f, 0xff}, {0x0f, 0xf0}}, - {{0x0f, 0xff}, {0x0f, 0xf0}}, - {{0xff, 0xff}, {0xff, 0xf0}}, - }, expectedMembers: map[string][]ids.ID{ "a": {{0xff, 0xff}, {0xff, 0xf0}, {0x0f, 0xff}, {0x0f, 0xf0}}, "e": {{0xff, 0xff}, {0xff, 0xf0}}, @@ -100,9 +85,6 @@ func TestSharedPrefixes(t *testing.T) { "f": {{0xff, 0xf0}}, "g": {{0xff, 0xff}}, }, - expectedCommonPrefixes: [][]uint8{ - {1, 1, 1, 1}, {1, 1, 1, 1, 0, 0, 0, 0}, {1, 1, 1, 1, 1, 1, 1, 1}, - }, }, } { t.Run(tst.name, func(t *testing.T) { @@ -123,27 +105,49 @@ func TestSharedPrefixes(t *testing.T) { } }) - // commonPrefixMembers holds common prefixes of prefix groups as bifurcationsWithCommonPrefix() - // traverses starting from the top prefix group. - // It does not contain leaf nodes, as bifurcationsWithCommonPrefix() does not visit leaf nodes in its traversal. - - // commonPrefixMembers contains the IDs of the prefix group that corresponds to the commonPrefixes. - // The intention of having it is showing how each bifurcation splits the members according to their common prefix. - // Had bifurcationsWithCommonPrefix() visited leaf prefix groups, the commonPrefixMembers would shrink down to a single ID. - // Since bifurcationsWithCommonPrefix() doesn't visit leaf prefix groups, commonPrefixMembers can contain sets of sets as small as two elements. - - commonPrefixMembers := make([][]ids.ID, 0, len(tst.expectedCommonPrefixMembers)) - commonPrefixes := make([][]uint8, 0, len(tst.expectedCommonPrefixes)) - - pg.bifurcationsWithCommonPrefix(func(members []ids.ID, prefix []uint8) { - commonPrefixMembers = append(commonPrefixMembers, members) - commonPrefixes = append(commonPrefixes, prefix) - }) - require.Equal(t, tst.expectedEdges, edges.String()) require.Equal(t, tst.expectedMembers, members) - require.Equal(t, tst.expectedCommonPrefixes, commonPrefixes) - require.Equal(t, tst.expectedCommonPrefixMembers, commonPrefixMembers) }) } } + +func TestBifurcationsWithCommonPrefix(t *testing.T) { + pg := &prefixGroup{ + members: []ids.ID{{0, 1, 1}, {0, 0, 1}, {0, 1, 0}, {0, 0, 0}}, + og: &prefixGroup{ + prefix: []uint8{1}, + members: []ids.ID{{0, 1, 1}, {0, 0, 1}}, + og: &prefixGroup{ + prefix: []uint8{0, 0, 1}, + members: []ids.ID{{0, 0, 1}}, + }, + zg: &prefixGroup{ + prefix: []uint8{0, 1, 1}, + members: []ids.ID{{0, 1, 1}}, + }, + }, + zg: &prefixGroup{ + prefix: []uint8{0}, + members: []ids.ID{{0, 1, 0}, {0, 0, 0}}, + og: &prefixGroup{ + prefix: []uint8{0, 1, 0}, + members: []ids.ID{{0, 1, 0}}, + }, + zg: &prefixGroup{ + prefix: []uint8{0, 0, 0}, + members: []ids.ID{{0, 0, 0}}, + }, + }, + } + + expectedTraversalOrder := [][]ids.ID{ + {{0, 1, 0}, {0, 0, 0}}, + {{0, 1, 1}, {0, 0, 1}}, + } + + pg.bifurcationsWithCommonPrefix(func(actual []ids.ID) { + next, rest := expectedTraversalOrder[0], expectedTraversalOrder[1:] + require.Equal(t, next, actual) + expectedTraversalOrder = rest + }) +} From be96b366a18b130dd4d1a712b0ebad86fdbb8155 Mon Sep 17 00:00:00 2001 From: Yacov Manevich Date: Fri, 15 Nov 2024 23:47:37 +0100 Subject: [PATCH 8/8] Simplify test Signed-off-by: Yacov Manevich --- snow/consensus/snowman/poll/prefix_test.go | 8 +++++--- 1 file changed, 5 insertions(+), 3 deletions(-) diff --git a/snow/consensus/snowman/poll/prefix_test.go b/snow/consensus/snowman/poll/prefix_test.go index 0773793135fc..3fd0fcd870bd 100644 --- a/snow/consensus/snowman/poll/prefix_test.go +++ b/snow/consensus/snowman/poll/prefix_test.go @@ -145,9 +145,11 @@ func TestBifurcationsWithCommonPrefix(t *testing.T) { {{0, 1, 1}, {0, 0, 1}}, } + actualOrder := make([][]ids.ID, 0, 2) + pg.bifurcationsWithCommonPrefix(func(actual []ids.ID) { - next, rest := expectedTraversalOrder[0], expectedTraversalOrder[1:] - require.Equal(t, next, actual) - expectedTraversalOrder = rest + actualOrder = append(actualOrder, actual) }) + + require.Equal(t, expectedTraversalOrder, actualOrder) }