Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Unexpected behaviour of block import_notification_stream #5596

Closed
2 tasks done
ffarall opened this issue Sep 5, 2024 · 5 comments · Fixed by #5811
Closed
2 tasks done

Unexpected behaviour of block import_notification_stream #5596

ffarall opened this issue Sep 5, 2024 · 5 comments · Fixed by #5811
Labels
I2-bug The node fails to follow expected behavior. I10-unconfirmed Issue might be valid, but it's not yet known.

Comments

@ffarall
Copy link
Contributor

ffarall commented Sep 5, 2024

Is there an existing issue?

  • I have searched the existing issues

Experiencing problems? Have you tried our Stack Exchange first?

  • This is not a support question.

Description of bug

From my understanding, there seems to be a misalignment between the commented (expected) behaviour of the import_notification_stream from BlockchainEvents (code here), and the actual code. I'm going to explain my understanding of what the code should do according to the comment, and what it actually does. Please correct me if I'm wrong.

The Code

This code snippet is where the notifications are sent through the streams:

match import_notification_action {
			ImportNotificationAction::Both => {
				trigger_storage_changes_notification();
				self.import_notification_sinks
					.lock()
					.retain(|sink| sink.unbounded_send(notification.clone()).is_ok());

				self.every_import_notification_sinks
					.lock()
					.retain(|sink| sink.unbounded_send(notification.clone()).is_ok());
			},
			ImportNotificationAction::RecentBlock => {
				trigger_storage_changes_notification();
				self.import_notification_sinks
					.lock()
					.retain(|sink| sink.unbounded_send(notification.clone()).is_ok());

				self.every_import_notification_sinks.lock().retain(|sink| !sink.is_closed());
			},
			ImportNotificationAction::EveryBlock => {
				self.every_import_notification_sinks
					.lock()
					.retain(|sink| sink.unbounded_send(notification.clone()).is_ok());

				self.import_notification_sinks.lock().retain(|sink| !sink.is_closed());
			},
			ImportNotificationAction::None => {
				// This branch is unreachable in fact because the block import notification must be
				// Some(_) instead of None (it's already handled at the beginning of this function)
				// at this point.
				self.import_notification_sinks.lock().retain(|sink| !sink.is_closed());

				self.every_import_notification_sinks.lock().retain(|sink| !sink.is_closed());
			},
		}

So depending on the value of import_notification_action, both notification streams (import_notification_stream and every_import_notification_stream) are used, or just one of them.

The value of import_notification_action is defined here:

			let import_notification_action = if should_notify_every_block {
				if should_notify_recent_block {
					ImportNotificationAction::Both
				} else {
					ImportNotificationAction::EveryBlock
				}
			} else {
				ImportNotificationAction::RecentBlock
			};

should_notify_recent_block is responsible for determining if import_notification_stream should be triggered, and its value is determined in this line:

		// Notify when we are already synced to the tip of the chain
		// or if this import triggers a re-org
		let should_notify_recent_block = make_notifications || tree_route.is_some();

The second operand of the logical OR (tree_route) is what one would expect:

		let tree_route = if is_new_best && info.best_hash != parent_hash && parent_exists {
			let route_from_best =
				sp_blockchain::tree_route(self.backend.blockchain(), info.best_hash, parent_hash)?;
			Some(route_from_best)
		} else {
			None
		};

In other words, if there is a new best block which belongs to a fork branch that is not the current one, this is a re-org, so tree_route will be Some.

The first operand though (make_notifications), is defined here:

		// this is a fairly arbitrary choice of where to draw the line on making notifications,
		// but the general goal is to only make notifications when we are already fully synced
		// and get a new chain head.
		let make_notifications = match origin {
			BlockOrigin::NetworkBroadcast | BlockOrigin::Own | BlockOrigin::ConsensusBroadcast =>
				true,
			BlockOrigin::Genesis | BlockOrigin::NetworkInitialSync | BlockOrigin::File => false,
		};

This means that make_notifications will only be false iff the origin of the block is one of BlockOrigin::Genesis | BlockOrigin::NetworkInitialSync | BlockOrigin::File. Which means that, for example, make_notifications will still be true if a block is imported from the network, and that block belongs to a fork branch that is not the current best branch, and is not the new best (doesn't cause a re-org). Given that should_notify_recent_block is determined by an OR, regardless of tree_route, should_notify_recent_block will still be true for this non-reorging block.

Behaviour from the Code

My interpretation from this analysis is that:

  1. During the initial syncing process of the node, make_notifications will be false, so should_notify_recent_block will be true if somehow there is a re-org during the syncing process.
  2. After the initial sync, make_notifications will essentially always be true, so should_notify_recent_block will always be true regardless of whether the block is the new best of the current branch, a non-new best from another fork branch, or a new best from another branch that causes a re-org.

This means that the tree_route condition is relevant only to notify about re-orgs during a syncing process.

Commented Behaviour of import_notification_stream

This is the comment on import_notification_stream in the code:

	/// Get block import event stream.
	///
	/// Not guaranteed to be fired for every imported block, only fired when the node
	/// has synced to the tip or there is a re-org. Use `every_import_notification_stream()`
	/// if you want a notification of every imported block regardless.
	fn import_notification_stream(&self) -> ImportNotifications<Block>;

Based on this comment, my intuition for the behaviour of import_notification_stream is:

  1. It should start notifying block imports after the initial sync.
  2. After the initial sync, it should notify of new best blocks in the current best branch.
  3. It should not notify of block imports from a fork branch that don't cause a re-org.
  4. It should notify of block imports from another fork branch that do cause a re-org.

Conclusion

Either the behaviour interpreted from the code is the expected one, and the comment is not clear about it, or the comment reflects the intended behaviour, but the code doesn't.

Personally, I feel it is counterintuitive if the expected behaviour is to notify every block after the initial sync (re-org or not), but during the initial sync, notify only of re-orgs. So I'm inclined to believe that the comment reflects the intended behaviour, and the code is wrong. But of course, there are use cases that I might not be considering.

Whatever the case, I'm open to open a PR and help make this behaviour clearer. Either by updating the comment on import_notification_stream, or fixing the code.

Thank you for your time!

Steps to reproduce

Not needed

@ffarall ffarall added I10-unconfirmed Issue might be valid, but it's not yet known. I2-bug The node fails to follow expected behavior. labels Sep 5, 2024
@bkchr
Copy link
Member

bkchr commented Sep 5, 2024

Maybe you want to make the comment more clear to you. However,

Only fired when the node has synced to the tip or there is a re-org

means to me that the notification is either send when you are at the tip of the chain (for every block) or there is a re-org (which is only relevant when you are not at the tip of the chain).

  • It should not notify of block imports from a fork branch that don't cause a re-org.

  • It should notify of block imports from another fork branch that do cause a re-org.

The first one not being true and the second one being true because we notify for every imported block.

@ffarall
Copy link
Contributor Author

ffarall commented Sep 5, 2024

Thank you for the clarification @bkchr ! Of course this is based on my interpretation of the comment, so yes, the comment wasn't clear at least to me.

I'm curious to understand the reasoning behind this. Why would a re-org only be relevant when you're not synced to the tip? My thinking was that, if you don't care about being notified of blocks before syncing to the tip, why would you be only interested in blocks during that sync that cause a re-org?

@ffarall
Copy link
Contributor Author

ffarall commented Sep 5, 2024

A colleague helped me find the reasoning in this PR.

@bkchr
Copy link
Member

bkchr commented Sep 6, 2024

If you like you can open a pr to make the comment more clear to you and others.

@ffarall
Copy link
Contributor Author

ffarall commented Sep 24, 2024

@bkchr I just did that. No rush at all in reviewing it or anything, but hope this helps. Thanks again for your clarification.

bkchr added a commit that referenced this issue Sep 28, 2024
# Description

Updates the doc comment on the `import_notification_stream` to make its
behaviour clearer.

Closes [Unexpected behaviour of block
`import_notification_stream`](#5596).

## Integration

Doesn't apply.

## Review Notes

The old comment docs caused some confusion to myself and some members of
my team, on when this notification stream is triggered. This is
reflected in the linked
[issue](#5596), and as
discussed there, this PR aims to prevent this confusion in future devs
looking to make use of this functionality.

# Checklist

* [x] My PR includes a detailed description as outlined in the
"Description" and its two subsections above.
* [ ] My PR follows the [labeling requirements](

https://github.com/paritytech/polkadot-sdk/blob/master/docs/contributor/CONTRIBUTING.md#Process
) of this project (at minimum one label for `T` required)
* External contributors: ask maintainers to put the right label on your
PR.
* [x] I have made corresponding changes to the documentation (if
applicable)
* [x] I have added tests that prove my fix is effective or that my
feature works (if applicable)

You can remove the "Checklist" section once all have been checked. Thank
you for your contribution!

---------

Co-authored-by: Michal Kucharczyk <[email protected]>
Co-authored-by: Bastian Köcher <[email protected]>
paritytech-ci pushed a commit that referenced this issue Oct 1, 2024
# Description

Updates the doc comment on the `import_notification_stream` to make its
behaviour clearer.

Closes [Unexpected behaviour of block
`import_notification_stream`](#5596).

## Integration

Doesn't apply.

## Review Notes

The old comment docs caused some confusion to myself and some members of
my team, on when this notification stream is triggered. This is
reflected in the linked
[issue](#5596), and as
discussed there, this PR aims to prevent this confusion in future devs
looking to make use of this functionality.

# Checklist

* [x] My PR includes a detailed description as outlined in the
"Description" and its two subsections above.
* [ ] My PR follows the [labeling requirements](

https://github.com/paritytech/polkadot-sdk/blob/master/docs/contributor/CONTRIBUTING.md#Process
) of this project (at minimum one label for `T` required)
* External contributors: ask maintainers to put the right label on your
PR.
* [x] I have made corresponding changes to the documentation (if
applicable)
* [x] I have added tests that prove my fix is effective or that my
feature works (if applicable)

You can remove the "Checklist" section once all have been checked. Thank
you for your contribution!

---------

Co-authored-by: Michal Kucharczyk <[email protected]>
Co-authored-by: Bastian Köcher <[email protected]>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
I2-bug The node fails to follow expected behavior. I10-unconfirmed Issue might be valid, but it's not yet known.
Projects
None yet
Development

Successfully merging a pull request may close this issue.

2 participants