Skip to content
This repository has been archived by the owner on Nov 15, 2023. It is now read-only.

Add block announce validator. #3346

Merged
merged 14 commits into from
Sep 24, 2019

Conversation

twittner
Copy link
Contributor

@twittner twittner commented Aug 8, 2019

Adds a BlockAnnounceValidator trait which is used to check BlockAnnouncements. It also adds an associated data field to BlockAnnounce which is used for things like candidate messages.

@arkpar
Copy link
Member

arkpar commented Aug 9, 2019

Not sure it is such a great idea to bundle extra data with block announcement. Does the protocol guarantee that announcement for the same block will always come with the same extra data?
To me it looks like the extra data should go into a separate message in the protocol extension.

@rphmeier
Copy link
Contributor

rphmeier commented Aug 9, 2019

@arkpar

To me it looks like the extra data should go into a separate message in the protocol extension.

To be honest, that is a decent approach as well. Perhaps we should rather have a method to use a custom block announcement message rather than bundle the extra data.

The desired use-case is for Cumulus: before a block is included on the relay chain but after voting on it has begun, the block should be synced by peers only when bundled with a submission attestation.

We would need two changes:

  • A way for substrate-network to ask some user code if it would like to use its own message for the announcement.
  • A way for user code to tell substrate-network there has been a block announcement.

@arkpar
Copy link
Member

arkpar commented Aug 9, 2019

How is it different from e.g block justifications, that come not as part of the announcement, but as block data?

@rphmeier
Copy link
Contributor

rphmeier commented Aug 9, 2019

Justifications are meant to prove finality and be permanently valid. It's exactly what you pinpointed - that this is for a use-case where the justification-like thing is temporary and doesn't need to be persisted, at least not for long.

We want this data to come along with the announcement, because it is crucial for our decision as to whether to download the head or not. Having to do an extra round-trip for all announced blocks would be pretty ridiculous.

@gavofyork gavofyork added the A3-in_progress Pull request is in progress. No review needed at this stage. label Aug 21, 2019
@bkchr
Copy link
Member

bkchr commented Sep 3, 2019

So, I personally would go with an extra field for the block announcement message and move the default validation completely out of the sync code. I could imagine that people want to bring in their own validation of the block announcement, for whatever reasons.

But yeah, for the sake of bringing Cumulus to a point that it works, we can also go the way of adding an extra message for our own block announcements.

@arkpar @rphmeier which way to we want to go?

@arkpar
Copy link
Member

arkpar commented Sep 3, 2019

So, I personally would go with an extra field for the block announcement message and move the default validation completely out of the sync code.

I'm fine with this

@rphmeier
Copy link
Contributor

rphmeier commented Sep 3, 2019

@twittner OK - looks like the current approach of the PR with slight modification to move default validation out of the sync code will work.

@twittner
Copy link
Contributor Author

twittner commented Sep 3, 2019

It would be helpful to know what you consider "default validation" from here:

pub fn on_block_announce(&mut self, who: PeerId, hash: B::Hash, header: &B::Header) -> OnBlockAnnounce<B> {
let number = *header.number();
debug!(target: "sync", "Received block announcement with number {:?}", number);
if number.is_zero() {
warn!(target: "sync", "Ignored genesis block (#0) announcement from {}: {}", who, hash);
return OnBlockAnnounce::Nothing
}
let parent_status = self.block_status(header.parent_hash()).ok().unwrap_or(BlockStatus::Unknown);
let known_parent = parent_status != BlockStatus::Unknown;
let ancient_parent = parent_status == BlockStatus::InChainPruned;
let known = self.is_known(&hash);
let peer = if let Some(peer) = self.peers.get_mut(&who) {
peer
} else {
error!(target: "sync", "Called on_block_announce with a bad peer ID");
return OnBlockAnnounce::Nothing
};
while peer.recently_announced.len() >= ANNOUNCE_HISTORY_SIZE {
peer.recently_announced.pop_front();
}
peer.recently_announced.push_back(hash.clone());
if number > peer.best_number {
// update their best block
peer.best_number = number;
peer.best_hash = hash;
}
if let PeerSyncState::AncestorSearch(_, _) = peer.state {
return OnBlockAnnounce::Nothing
}
// We assume that the announced block is the latest they have seen, and so our common number
// is either one further ahead or it's the one they just announced, if we know about it.
if known {
peer.common_number = number
} else if header.parent_hash() == &self.best_queued_hash || known_parent {
peer.common_number = number - One::one();
}
self.is_idle = false;
// known block case
if known || self.is_already_downloading(&hash) {
trace!(target: "sync", "Known block announce from {}: {}", who, hash);
return OnBlockAnnounce::Nothing
}
// stale block case
let requires_additional_data = !self.role.is_light();
if number <= self.best_queued_number {
if !(known_parent || self.is_already_downloading(header.parent_hash())) {
let block_status = self.client.block_status(&BlockId::Number(*header.number()))
.unwrap_or(BlockStatus::Unknown);
if block_status == BlockStatus::InChainPruned {
trace!(
target: "sync",
"Ignored unknown ancient block announced from {}: {} {:?}", who, hash, header
);
return OnBlockAnnounce::Nothing
}
trace!(
target: "sync",
"Considering new unknown stale block announced from {}: {} {:?}", who, hash, header
);
if let Some(request) = self.download_unknown_stale(&who, &hash) {
if requires_additional_data {
return OnBlockAnnounce::Request(who, request)
} else {
return OnBlockAnnounce::ImportHeader
}
} else {
return OnBlockAnnounce::Nothing
}
} else {
if ancient_parent {
trace!(target: "sync", "Ignored ancient stale block announced from {}: {} {:?}", who, hash, header);
return OnBlockAnnounce::Nothing
}
if let Some(request) = self.download_stale(&who, &hash) {
if requires_additional_data {
return OnBlockAnnounce::Request(who, request)
} else {
return OnBlockAnnounce::ImportHeader
}
} else {
return OnBlockAnnounce::Nothing
}
}
}
if ancient_parent {
trace!(target: "sync", "Ignored ancient block announced from {}: {} {:?}", who, hash, header);
return OnBlockAnnounce::Nothing
}
trace!(target: "sync", "Considering new block announced from {}: {} {:?}", who, hash, header);
let (range, request) = match self.select_new_blocks(who.clone()) {
Some((range, request)) => (range, request),
None => return OnBlockAnnounce::Nothing
};
let is_required_data_available = !requires_additional_data
&& range.end - range.start == One::one()
&& range.start == *header.number();
if !is_required_data_available {
return OnBlockAnnounce::Request(who, request)
}
OnBlockAnnounce::ImportHeader
}
? Most of it relates to the state of peers which would mean to replicate a lot of the peer state handling and also to keep it up to date w.r.t. what is currently being downloaded from where etc. I am not sure it is a good idea to replicate this which begs the question what is left to validate?

@bkchr
Copy link
Member

bkchr commented Sep 3, 2019

The peer management does not seems to be used after line 933? As the updating of the peer information is not validating, it should stay in the sync layer.

@twittner
Copy link
Contributor Author

twittner commented Sep 3, 2019

The peer management does not seems to be used after line 933?

It is, e.g.

  • is_already_downloading (line 941),
  • download_unknown_stale (lines 955, 969),
  • select_new_blocks (line 988).

@bkchr
Copy link
Member

bkchr commented Sep 4, 2019

Can we not make this available to the announce validator using a trait?

The validator would probably return the following:

enum ValidationResult {
    Request(Request),
    Nothing,
}

Then in the on_block_announce we convert this into the OnBlockAnnounce by doing:

match res {
    ValidationResult::Request(req) => if requires_additional_data {
						return OnBlockAnnounce::Request(who, request)
					} else {
						return OnBlockAnnounce::ImportHeader
					}
     ValidationResult::Nothing => OnBlockAnnounce::Nothing
}

@twittner
Copy link
Contributor Author

twittner commented Sep 5, 2019

Could you be more specific? Do you mean we should run on_block_announce in sync.rs as it is today and within this method collect some data (what exactly?) which we then pass to the validator which in turn constructs a substrate_network::protocol::message::BlockRequest that on_block_announce then sends over the network? If so I still do not understand what validation logic should be moved to the validator. Also, is this coupling not a bit tight?

@twittner
Copy link
Contributor Author

twittner commented Sep 9, 2019

@bkchr?

@rphmeier
Copy link
Contributor

rphmeier commented Sep 9, 2019

The goal of the PR is to give us the option to do additional, customized validation on whether a block announcement is valid based on possibly time-sensitive circumstances. We'd do so by bundling a bit of optional extra data with the announcement to be checked by some user code, along with the announcement.

Most of the checks that the on_block_announce function is doing are technically announcement validation, but are the kind of checks that you would always want to do. I don't think it's worthwhile to abstract to the point where these are extractable:

  • Checking if the peer has recently announced the block
  • Checking if the block's parent is ancient
  • Checking what protocol state the peer is in
  • Checking whether we know the block already or are downloading it

With that in mind I'd suggest that "default" verification could be an empty or nearly-empty trait implementation - this trait implementation could live outside of the sync code, although I'd say it's reasonable for it to live in the network crate.

I assume that Basti is referring to replacing these lines

		if !is_required_data_available {
			return OnBlockAnnounce::Request(who, request)
		}

with

match res {
    ValidationResult::Request(req) => if requires_additional_data {
        return OnBlockAnnounce::Request(who, request)
    } else {
	return OnBlockAnnounce::ImportHeader
    },
    ValidationResult::Nothing => OnBlockAnnounce::Nothing
}

I'm not 100% sure what the implications of having the request come from the announce-validator are. @bkchr, it doesn't seem to be a problem to me if we have the request generated by self.select_new_blocks as is the case now, and only give the announce-validator the option to say whether to request in general.

@rphmeier
Copy link
Contributor

rphmeier commented Sep 9, 2019

Then there's also the other side of the coin to discuss: how the block announcement should actually be bundled with this extra-data.

I'm not particularly opinionated on this, but ideally the two methods can fall within the same trait to prevent user-protocol logic from becoming spread out.

@bkchr
Copy link
Member

bkchr commented Sep 10, 2019

Yeah, sorry for the delay!

I think what @rphmeier writes, sounds reasonable. Regarding the extra-data, as this is probably just some opaque field, we could probably check this in the same function we are giving the general "yes/no" to the block request.

@twittner twittner marked this pull request as ready for review September 10, 2019 15:29
@twittner
Copy link
Contributor Author

Ok, turning it into a non-WIP PR now. Please review when time permits it. Regarding

Then there's also the other side of the coin to discuss: how the block announcement should actually be bundled with this extra-data.

This is not addressed in this PR. The various places where interfaces require associated data use empty vectors for now.

@twittner twittner requested a review from bkchr September 10, 2019 15:30
Copy link
Contributor

@Demi-Marie Demi-Marie left a comment

Choose a reason for hiding this comment

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

Nits

@@ -499,7 +499,7 @@ fn light_peer_imports_header_from_announce() {
let mut runtime = current_thread::Runtime::new().unwrap();

fn import_with_announce(net: &mut TestNet, runtime: &mut current_thread::Runtime, hash: H256) {
net.peer(0).announce_block(hash);
net.peer(0).announce_block(hash, Vec::new()); // TODO
Copy link
Contributor

Choose a reason for hiding this comment

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

Needs an issue filed.

@@ -1015,6 +1002,7 @@ impl<B, E, Block, RA> Client<B, E, Block, RA> where
is_new_best,
storage_changes,
retracted,
associated_data: Vec::new(), // TODO
Copy link
Contributor

Choose a reason for hiding this comment

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

Any TODOs or FIXMEs need to contain links to open issues.

&self,
notify_import: ImportSummary<Block>,
) -> error::Result<()> {
fn notify_imported(&self, notify_import: ImportSummary<Block>) -> error::Result<()> {
if let Some(storage_changes) = notify_import.storage_changes {
// TODO [ToDr] How to handle re-orgs? Should we re-emit all storage changes?
Copy link
Contributor

Choose a reason for hiding this comment

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

@tomusdrw solved or needs an issue?

Copy link
Contributor

Choose a reason for hiding this comment

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

It seems that we are notifying about all blocks (not only the new best ones), so I think we don't really need any special handling here. CC @jacogr what would you expect in case of re-orgs and storage changes subscription?

Copy link
Contributor

Choose a reason for hiding this comment

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

If there is a re-org, would expect the subscriptions to pass through the most-recent-actual-state values.

Effectively the lasted values from the subscription and state_getStorage should match - both should reflect the current chain state.

use crate::{
config::{Roles, BoxFinalityProofRequestBuilder},
message::{self, generic::FinalityProofRequest, BlockAttributes, BlockRequest, BlockResponse, FinalityProofResponse},
message::{
BlockAnnounce,
Copy link
Contributor

Choose a reason for hiding this comment

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

better to wrap when the line limit exceeds.

Copy link
Member

Choose a reason for hiding this comment

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

Yeah.

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Why?

Copy link
Contributor

@kianenigma kianenigma Sep 16, 2019

Choose a reason for hiding this comment

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

consistency with the rest of substrate -- though this (afaik) is not strictly in our style guide. Usually what I do (and I think it is sort of the implicit styleguide):

use crate::{
     top_level_stuff_1, top_level_stuff_2, top_level_stuff_3, top_level_stuff_wrap_if_needed,
     inner_module_1::{stuff, bar, Bazinga},
     inner_module_2::{foo, kaboom, blah},
}

basically break once per sub-module but other than that only when line-width is reached.

Copy link
Member

@bkchr bkchr left a comment

Choose a reason for hiding this comment

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

Some nitpicks, in general looking good.

core/network/src/protocol.rs Outdated Show resolved Hide resolved
core/network/src/test/sync.rs Outdated Show resolved Hide resolved
use crate::{
config::{Roles, BoxFinalityProofRequestBuilder},
message::{self, generic::FinalityProofRequest, BlockAttributes, BlockRequest, BlockResponse, FinalityProofResponse},
message::{
BlockAnnounce,
Copy link
Member

Choose a reason for hiding this comment

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

Yeah.

#[derive(Debug, PartialEq, Eq)]
pub enum Validation {
/// Valid block announcement.
Success,
Copy link
Member

Choose a reason for hiding this comment

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

Why not call them Valid and Invalid directly? And the enum ValidationResult or something similar?

Copy link
Contributor

@rphmeier rphmeier Sep 12, 2019

Choose a reason for hiding this comment

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

Result has a special meaning in Rust - as a nitpick, we shouldn't call anything except for variants of the Result<T, E> enum Result.

Typical Rust style would have this be enum Valid { Valid, NotValid } or enum Valid { Yes, No }.

Copy link
Contributor Author

@twittner twittner Sep 16, 2019

Choose a reason for hiding this comment

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

I think naming the type "Valid" can be misleading because "valid" is already biased towards "yes". Saying something valid is not valid feels a little inconsistent to me, so at a minimum I would like to see a neutral name. I agree that the "Result" suffix should better be avoided, especially since the type name will often be embedded in another Result. I would rather read Result<Validation, ...> instead of Result<ValidationResult, ...>.

@@ -201,6 +186,8 @@ pub struct BlockImportNotification<Block: BlockT> {
pub is_new_best: bool,
/// List of retracted blocks ordered by block number.
pub retracted: Vec<Block::Hash>,
/// Any data associated with this block import.
pub associated_data: Vec<u8>,
Copy link
Member

Choose a reason for hiding this comment

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

Why do we need the data as part of the import notification?

Copy link
Member

Choose a reason for hiding this comment

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

Hmm, now I understand, but I don't think that this is required. In Cumulus we will have to delay sending the notification anyway and will fetch the associated data from a different source. So, yeah, just remove it.

Copy link
Contributor Author

Choose a reason for hiding this comment

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

If we remove this, how do we send block announcements with associated data to network peers?

Copy link
Member

Choose a reason for hiding this comment

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

https://github.com/paritytech/substrate/pull/3346/files#diff-16b4c34d50bdfcbf732c09c7be1fdcf9R684 here do we send the block announcement. The import notification is just used to know that we need to send a block announcement. But as I already said, we will send the block announcements differently in Cumulus.

twittner and others added 6 commits September 16, 2019 10:24
Co-Authored-By: Kian Paimani <5588131+kianenigma@users.noreply.github.com>
Co-Authored-By: Bastian Köcher <bkchr@users.noreply.github.com>
Co-Authored-By: Bastian Köcher <bkchr@users.noreply.github.com>
Copy link
Member

@bkchr bkchr left a comment

Choose a reason for hiding this comment

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

Just one last master merge.

@arkpar do we need to update the protocol version? I assume yes?

@twittner
Copy link
Contributor Author

Just one last master merge.

Merging was actually quite involved. I think #3602 has introduced some changes in the vicinity. Maybe @andresilva could check that things still make sense?

@arkpar
Copy link
Member

arkpar commented Sep 19, 2019

Indeed the protocol should be backwards compatible. It should not send any additional data to nodes running older version. Furthermore message encoding for the current version should match standard SCALE encoding.

Copy link
Contributor

@rphmeier rphmeier 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! Thanks Toralf.

edit: logic/API is fine, Arkady's point about backwards compatibility would be the last thing to address

@@ -284,16 +286,21 @@ pub mod generic {
if let Some(state) = &self.state {
state.encode_to(dest);
}
if !self.data.is_empty() {
Copy link
Member

Choose a reason for hiding this comment

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

This will interfere with encoding any additional fields. I.e. this new protocol version should always encode data when communicating with the same protocol version and do not encode when communicating with an older version. I'd suggest using Option<Vec<u8>> in BlockAnnounce for that. Later we can remove backwards compatibility and Option without changing the actual encoding for the current version.

Copy link
Contributor Author

Choose a reason for hiding this comment

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

I would normally have called encode_to unconditionally. Wrapping data in an Option is not bulletproof either, as one can easily just put it in a Some. Also, changing the type ripples through the whole system which is not great. As I mentioned elsewhere, we should reconsider our encoding, otherwise minor changes such as this will continue to hurt us.

Copy link
Member

Choose a reason for hiding this comment

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

Sure, we should get proper versioning support at some point, but this is out of scope of this PR I believe.

Copy link
Member

@bkchr bkchr left a comment

Choose a reason for hiding this comment

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

2 last nitpicks, then it is good to be merged.

core/network/src/protocol/sync.rs Outdated Show resolved Hide resolved
@@ -273,6 +273,8 @@ pub mod generic {
pub header: H,
/// Block state. TODO: Remove `Option` and custom encoding when v4 becomes common.
pub state: Option<BlockState>,
/// Data associated with this block announcement, e.g. a candidate message.
Copy link
Member

Choose a reason for hiding this comment

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

The Option should be removed here as well, when 4 becomes common.

}
}

impl<H: Decode> Decode for BlockAnnounce<H> {
fn decode<I: Input>(input: &mut I) -> Result<Self, codec::Error> {
let header = H::decode(input)?;
let state = BlockState::decode(input).ok();
let data = Vec::decode(input).ok();
Copy link
Member

Choose a reason for hiding this comment

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

I'm not happy with this. Conceptually it should be Option<(BlockState, Vec<u8>)>. As both should be present with version 4. But yeah, for the sake of merging this and don't over-complicating it more, we probably can go with this.

twittner and others added 2 commits September 23, 2019 13:12
Co-Authored-By: Bastian Köcher <bkchr@users.noreply.github.com>
@bkchr bkchr merged commit 7a7d641 into paritytech:master Sep 24, 2019
@twittner twittner deleted the block-announce-validator branch September 24, 2019 09:16
Sign up for free to subscribe to this conversation on GitHub. Already have an account? Sign in.
Labels
A3-in_progress Pull request is in progress. No review needed at this stage.
Projects
None yet
Development

Successfully merging this pull request may close these issues.

9 participants