Skip to content

Refactor with hotstuff.Distributor#8156

Merged
zhangchiqing merged 30 commits intomasterfrom
leo/hotstuff-distributor
Nov 20, 2025
Merged

Refactor with hotstuff.Distributor#8156
zhangchiqing merged 30 commits intomasterfrom
leo/hotstuff-distributor

Conversation

@zhangchiqing
Copy link
Copy Markdown
Member

@zhangchiqing zhangchiqing commented Nov 14, 2025

This PR refactors the finalization-events consumer to accept a hotstuff.Distributor and automatically register itself for event consumption. This prevents mistakes where callers forget to manually subscribe the consumer, which previously led to silent failures in receiving block-finalization events.

See details in comments.

@github-actions
Copy link
Copy Markdown
Contributor

github-actions bot commented Nov 14, 2025

Dependency Review

✅ No vulnerabilities or license issues or OpenSSF Scorecard issues found.

Scanned Files

None

if err != nil {
return nil, fmt.Errorf("could not create follower engine: %w", err)
}
builder.FollowerDistributor.AddOnBlockFinalizedConsumer(builder.FollowerEng.OnFinalizedBlock)
Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This was error-prone because builder.FollowerEng depended on the caller (access_node_builder) to manually register it with the follower distributor in order to receive block-finalization events. If that line was removed—or simply forgotten—the compiler would not complain, but the follower engine would silently stop receiving finalization events.

The goal of the refactor is to ensure that any component needing block-finalization notifications receives them automatically. Each such component now accepts a hotstuff.Distributor as a constructor argument and registers itself with the distributor during initialization, guaranteeing that it is properly subscribed.

Many of the changes stem from this principle, including extracting the hotstuff.Distributor interface (see the new file at consensus/hotstuff/distributor.go). The builder.FollowerDistributor now implements this interface and is passed into constructors, so callers no longer need to remember to invoke AddOnBlockFinalizedConsumer manually.

d.blockIncorporatedConsumers = append(d.blockIncorporatedConsumers, consumer)
}

func (d *FinalizationDistributor) AddFinalizationConsumer(consumer hotstuff.FinalizationConsumer) {
Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This is redundant. If the caller needs to add a finalization consumer, it can just use AddOnBlockFinalizedConsumer and AddOnBlockIncorporatedConsumer to subscribe for both events.

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This function exists to follow the pattern set by other consumer interfaces in this package. Similar events are grouped together, so that most consumer components would want to subscribe to the entire group of events. Then, the component can implement the interface and call one function to subscribe, instead of calling one function per event type. (We also added options to specifically subscribe to each event individually, because these events are frequently used.)

For example, we have consumer interfaces for TimeoutAggregationViolationConsumer, TimeoutCollectorConsumer, ParticipantConsumer etc.

Some of these interfaces are small, but some are quite large (eg. ParticipantConsumer has over 10 methods). The difference between calling AddFinalizationConsumer() and AddOnBlockFinalizedConsumer(); AddOnBlockFinalizedConsumer() isn't a big deal, butI don't think we want to call 10 subscribe functions to add a ParticipantConsumer.

In general I think we should keep AddFinalizationConsumer around, to maintain a consistent pattern across the package.

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Makes sense. 74b364c

I added back the AddFinalizationConsumer method, and internally it's implemented using AddOnBlockFinalizedConsumer and AddOnBlockIncorporatedConsumer. What I wanted to get rid of is the consumers field, which is actually redundant.

@zhangchiqing zhangchiqing marked this pull request as ready for review November 15, 2025 01:17
@zhangchiqing zhangchiqing requested a review from a team as a code owner November 15, 2025 01:17
Copy link
Copy Markdown
Member

@jordanschalm jordanschalm left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Nice work. I agree that it is better for correctness-critical notification subscription to happen in the relevant component's constructor. Added some documentation suggestions, and a request to keep AddFinalizationConsumer to maintain consistency across the pubsub interfaces.

Comment thread engine/consensus/sealing/engine.go Outdated
Comment on lines +162 to +163
distributor.AddOnBlockFinalizedConsumer(e.onFinalizedBlock)

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggested change
distributor.AddOnBlockFinalizedConsumer(e.onFinalizedBlock)
distributor.AddOnBlockFinalizedConsumer(e.onFinalizedBlock)

Comment on lines +80 to +81
// register callback with distributor
followerDistributor.AddOnBlockFinalizedConsumer(blockConsumer.onFinalizedBlock)
Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggested change
// register callback with distributor
followerDistributor.AddOnBlockFinalizedConsumer(blockConsumer.onFinalizedBlock)
followerDistributor.AddOnBlockFinalizedConsumer(blockConsumer.onFinalizedBlock)

mostly redundant comment

Comment on lines +252 to +253
// register callback with distributor
followerDistributor.AddOnBlockFinalizedConsumer(e.onBlockFinalized)
Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggested change
// register callback with distributor
followerDistributor.AddOnBlockFinalizedConsumer(e.onBlockFinalized)
followerDistributor.AddOnBlockFinalizedConsumer(e.onBlockFinalized)

Comment thread consensus/hotstuff/distributor.go Outdated
d.blockIncorporatedConsumers = append(d.blockIncorporatedConsumers, consumer)
}

func (d *FinalizationDistributor) AddFinalizationConsumer(consumer hotstuff.FinalizationConsumer) {
Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This function exists to follow the pattern set by other consumer interfaces in this package. Similar events are grouped together, so that most consumer components would want to subscribe to the entire group of events. Then, the component can implement the interface and call one function to subscribe, instead of calling one function per event type. (We also added options to specifically subscribe to each event individually, because these events are frequently used.)

For example, we have consumer interfaces for TimeoutAggregationViolationConsumer, TimeoutCollectorConsumer, ParticipantConsumer etc.

Some of these interfaces are small, but some are quite large (eg. ParticipantConsumer has over 10 methods). The difference between calling AddFinalizationConsumer() and AddOnBlockFinalizedConsumer(); AddOnBlockFinalizedConsumer() isn't a big deal, butI don't think we want to call 10 subscribe functions to add a ParticipantConsumer.

In general I think we should keep AddFinalizationConsumer around, to maintain a consistent pattern across the package.

Copy link
Copy Markdown
Contributor

@tim-barry tim-barry left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Looks good to me.

@zhangchiqing zhangchiqing added this pull request to the merge queue Nov 20, 2025
Merged via the queue into master with commit e054908 Nov 20, 2025
61 checks passed
@zhangchiqing zhangchiqing deleted the leo/hotstuff-distributor branch November 20, 2025 22:37
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

4 participants