Skip to main content

polkadot_collator_protocol/validator_side_experimental/
mod.rs

1// Copyright (C) Parity Technologies (UK) Ltd.
2// This file is part of Polkadot.
3
4// Polkadot is free software: you can redistribute it and/or modify
5// it under the terms of the GNU General Public License as published by
6// the Free Software Foundation, either version 3 of the License, or
7// (at your option) any later version.
8
9// Polkadot is distributed in the hope that it will be useful,
10// but WITHOUT ANY WARRANTY; without even the implied warranty of
11// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
12// GNU General Public License for more details.
13
14// You should have received a copy of the GNU General Public License
15// along with Polkadot.  If not, see <http://www.gnu.org/licenses/>.
16
17mod collation_manager;
18mod common;
19mod error;
20mod peer_manager;
21mod state;
22#[cfg(test)]
23mod tests;
24
25use crate::{
26	validator_side_experimental::{common::MIN_FETCH_TIMER_DELAY, peer_manager::PersistentDb},
27	LOG_TARGET,
28};
29use collation_manager::CollationManager;
30use common::{ProspectiveCandidate, MAX_STORED_SCORES_PER_PARA};
31use error::{log_error, FatalError, FatalResult, Result};
32use futures::{future::Fuse, select, FutureExt, StreamExt};
33use polkadot_node_clock::Clock;
34use polkadot_node_network_protocol::{
35	self as net_protocol, peer_set::PeerSet, v1 as protocol_v1, v2 as protocol_v2,
36	v3_collation as protocol_v3, CollationProtocols, PeerId,
37};
38use polkadot_node_subsystem::{
39	messages::{CollatorProtocolMessage, NetworkBridgeEvent, NetworkBridgeTxMessage},
40	overseer, ActivatedLeaf, CollatorProtocolSenderTrait, FromOrchestra, OverseerSignal,
41};
42use polkadot_node_subsystem_util::database::Database;
43use sp_keystore::KeystorePtr;
44use std::{future, future::Future, pin::Pin, sync::Arc, time::Duration};
45
46#[cfg(test)]
47use peer_manager::Db;
48use peer_manager::PeerManager;
49
50use state::State;
51
52pub use crate::validator_side_metrics::Metrics;
53
54/// Default interval for persisting the reputation database to disk (in seconds).
55const DEFAULT_PERSIST_INTERVAL_SECS: u64 = 600;
56
57/// Configuration for the reputation db.
58#[derive(Debug, Clone, Copy)]
59pub struct ReputationConfig {
60	/// The data column in the store to use for reputation data.
61	pub col_reputation_data: u32,
62	/// How often to persist the reputation database to disk.
63	/// If None, defaults to DEFAULT_PERSIST_INTERVAL_SECS seconds.
64	pub persist_interval: Option<Duration>,
65}
66
67/// The main run loop.
68#[overseer::contextbounds(CollatorProtocol, prefix = self::overseer)]
69pub(crate) async fn run<Context>(
70	mut ctx: Context,
71	keystore: KeystorePtr,
72	metrics: Metrics,
73	db: Arc<dyn Database>,
74	reputation_config: ReputationConfig,
75	clock: Arc<dyn Clock>,
76) -> FatalResult<()> {
77	let persist_interval = reputation_config
78		.persist_interval
79		.unwrap_or(Duration::from_secs(DEFAULT_PERSIST_INTERVAL_SECS));
80	gum::info!(
81		LOG_TARGET,
82		persist_interval_secs = persist_interval.as_secs(),
83		"Running experimental collator protocol"
84	);
85	if let Some(state) =
86		initialize(&mut ctx, keystore, metrics, db, reputation_config, clock.clone()).await?
87	{
88		run_inner(ctx, state, persist_interval, clock).await?;
89	}
90
91	Ok(())
92}
93
94#[overseer::contextbounds(CollatorProtocol, prefix = self::overseer)]
95async fn initialize<Context>(
96	ctx: &mut Context,
97	keystore: KeystorePtr,
98	metrics: Metrics,
99	db: Arc<dyn Database>,
100	reputation_config: ReputationConfig,
101	clock: Arc<dyn Clock>,
102) -> FatalResult<Option<State<PersistentDb>>> {
103	loop {
104		let first_leaf = match wait_for_first_leaf(ctx).await? {
105			Some(activated_leaf) => {
106				gum::debug!(
107					target: LOG_TARGET,
108					number = activated_leaf.number,
109					hash = ?activated_leaf.hash,
110					"Got the first active leaf notification, trying to initialize subsystem."
111				);
112				activated_leaf
113			},
114			None => return Ok(None),
115		};
116
117		let collation_manager =
118			CollationManager::new(ctx.sender(), keystore.clone(), first_leaf, clock.clone())
119				.await?;
120
121		let scheduled_paras = collation_manager.assignments();
122
123		gum::debug!(
124			target: LOG_TARGET,
125			?scheduled_paras,
126			"Collator protocol initial assignments",
127		);
128
129		// Create PersistentDb with disk persistence
130		let (backend, task) = match PersistentDb::new(
131			db.clone(),
132			reputation_config,
133			MAX_STORED_SCORES_PER_PARA,
134		)
135		.await
136		{
137			Ok(result) => result,
138			Err(e) => {
139				gum::error!(
140					target: LOG_TARGET,
141					error = ?e,
142					"Failed to initialize persistent reputation DB"
143				);
144				return Err(FatalError::ReputationDbInit(e));
145			},
146		};
147
148		// Background task for async writes
149		ctx.spawn_blocking("collator-reputation-persistence-task", task)
150			.map_err(|e| FatalError::SpawnTask(e.to_string()))?;
151
152		gum::trace!(target: LOG_TARGET, "Spawned background reputation persistence task");
153
154		match PeerManager::startup(
155			backend,
156			ctx.sender(),
157			scheduled_paras.into_iter().collect(),
158			clock.clone(),
159		)
160		.await
161		{
162			Ok(peer_manager) => {
163				return Ok(Some(State::new(peer_manager, collation_manager, metrics)))
164			},
165			Err(err) => {
166				log_error(Err(err))?;
167				continue;
168			},
169		}
170	}
171}
172
173/// Wait for `ActiveLeavesUpdate`, returns `None` if `Conclude` signal came first.
174#[overseer::contextbounds(CollatorProtocol, prefix = self::overseer)]
175async fn wait_for_first_leaf<Context>(ctx: &mut Context) -> FatalResult<Option<ActivatedLeaf>> {
176	loop {
177		match ctx.recv().await.map_err(FatalError::SubsystemReceive)? {
178			FromOrchestra::Signal(OverseerSignal::Conclude) => return Ok(None),
179			FromOrchestra::Signal(OverseerSignal::ActiveLeaves(update)) => {
180				if let Some(activated) = update.activated {
181					return Ok(Some(activated));
182				}
183			},
184			FromOrchestra::Signal(OverseerSignal::BlockFinalized(_, _)) => {},
185			FromOrchestra::Communication { msg } => {
186				// Disconnect peers that connect before the subsystem is initialized.
187				// They will reconnect later when we're ready.
188				match msg {
189					CollatorProtocolMessage::NetworkBridgeUpdate(
190						NetworkBridgeEvent::PeerConnected(peer_id, ..),
191					) => {
192						gum::debug!(
193							target: LOG_TARGET,
194							?peer_id,
195							"Disconnecting peer that connected before subsystem initialization",
196						);
197						ctx.send_message(NetworkBridgeTxMessage::DisconnectPeers(
198							vec![peer_id],
199							PeerSet::Collation,
200						))
201						.await;
202					},
203					CollatorProtocolMessage::NetworkBridgeUpdate(
204						NetworkBridgeEvent::PeerMessage(peer_id, ..),
205					) => {
206						gum::debug!(
207							target: LOG_TARGET,
208							?peer_id,
209							"Disconnecting peer that sent message before subsystem initialization",
210						);
211						ctx.send_message(NetworkBridgeTxMessage::DisconnectPeers(
212							vec![peer_id],
213							PeerSet::Collation,
214						))
215						.await;
216					},
217					msg => {
218						gum::trace!(
219							target: LOG_TARGET,
220							?msg,
221							"Received msg before first active leaves update, dropping.",
222						);
223					},
224				}
225			},
226		}
227	}
228}
229
230fn create_timer(
231	clock: &dyn Clock,
232	maybe_delay: Option<Duration>,
233) -> Fuse<Pin<Box<dyn Future<Output = ()> + Send>>> {
234	let timer: Pin<Box<dyn Future<Output = ()> + Send>> = match maybe_delay {
235		Some(delay) => clock.delay(delay),
236		None => Box::pin(future::pending::<()>()),
237	};
238
239	timer.fuse()
240}
241
242/// Create the persistence timer that fires after the given interval.
243fn create_persistence_timer(
244	clock: &dyn Clock,
245	interval: Duration,
246) -> Fuse<Pin<Box<dyn Future<Output = ()> + Send>>> {
247	clock.delay(interval).fuse()
248}
249
250#[overseer::contextbounds(CollatorProtocol, prefix = self::overseer)]
251async fn run_inner<Context>(
252	mut ctx: Context,
253	mut state: State<PersistentDb>,
254	persist_interval: Duration,
255	clock: Arc<dyn Clock>,
256) -> FatalResult<()> {
257	let mut timer = create_timer(&*clock, None);
258	let mut persistence_timer = create_persistence_timer(&*clock, persist_interval);
259
260	loop {
261		select! {
262			// Calling `fuse()` here is useless, because the termination state of the resulting
263			// fused future is discarded after each iteration. But we need to do it in order to
264			// make the compiler happy.
265			// However, we actually need `ctx.recv()` to poll for received messages/signals during
266			// each loop iteration, so the resulting behavior is the desired one.
267			res = ctx.recv().fuse() => {
268				match res {
269					Ok(FromOrchestra::Communication { msg }) => {
270						gum::trace!(target: LOG_TARGET, msg = ?msg, "received a message");
271						process_msg(
272							ctx.sender(),
273							&mut state,
274							msg,
275						).await;
276					}
277					Ok(FromOrchestra::Signal(OverseerSignal::Conclude)) | Err(_) => {
278						// Persist to disk before shutdown
279						state.persist_reputations().await;
280						break
281					},
282					Ok(FromOrchestra::Signal(OverseerSignal::BlockFinalized(hash, number))) => {
283						state.handle_finalized_block(ctx.sender(), hash, number).await?;
284					},
285					Ok(FromOrchestra::Signal(_)) => continue,
286				}
287			},
288			resp = state.collation_response_stream().select_next_some() => {
289				state.handle_fetched_collation(ctx.sender(), resp).await;
290			},
291			_ = &mut timer => {
292				// We don't need to do anything specific here.
293				// If the timer expires, we only need to trigger the advertisement fetching logic.
294			},
295			_ = &mut persistence_timer => {
296				// Periodic persistence - write reputation DB to disk
297				state.background_persist_reputations();
298				// Reset the timer for the next interval
299				persistence_timer = create_persistence_timer(&*clock, persist_interval);
300			},
301		}
302
303		// Now try triggering advertisement fetching, if we have room in any of the active leaves
304		// (any of them are in Waiting state).
305		// We could optimise to not always re-run this code (have the other functions return
306		// whether we should attempt launching fetch requests) However, most messages could
307		// indeed trigger a new legitimate request.
308		// Also, it takes constant time to run because we only try launching new requests for
309		// unfulfilled claims. It's probably not worth optimising.
310		let maybe_delay = state.try_launch_new_fetch_requests(ctx.sender()).await;
311		timer = create_timer(
312			&*clock,
313			maybe_delay.map(|delay| std::cmp::max(delay, MIN_FETCH_TIMER_DELAY)),
314		);
315	}
316
317	Ok(())
318}
319
320/// The main message receiver switch.
321async fn process_msg<Sender: CollatorProtocolSenderTrait>(
322	sender: &mut Sender,
323	state: &mut State<PersistentDb>,
324	msg: CollatorProtocolMessage,
325) {
326	use CollatorProtocolMessage::*;
327
328	let _timer = state.metrics().time_process_msg();
329
330	match msg {
331		CollateOn(id) => {
332			gum::warn!(
333				target: LOG_TARGET,
334				para_id = %id,
335				"CollateOn message is not expected on the validator side of the protocol",
336			);
337		},
338		DistributeCollation { .. } => {
339			gum::warn!(
340				target: LOG_TARGET,
341				"DistributeCollation message is not expected on the validator side of the protocol",
342			);
343		},
344		NetworkBridgeUpdate(event) => {
345			if let Err(e) = handle_network_msg(sender, state, event).await {
346				gum::warn!(
347					target: LOG_TARGET,
348					err = ?e,
349					"Failed to handle incoming network message",
350				);
351			}
352		},
353		Seconded(parent, stmt) => {
354			state.handle_seconded_collation(sender, stmt, parent).await;
355		},
356		Invalid(parent, candidate_receipt) => {
357			state.handle_invalid_collation(candidate_receipt, parent).await;
358		},
359		ConnectToBackingGroups => {
360			gum::warn!(
361				target: LOG_TARGET,
362				"ConnectToBackingGroups message is not expected on the validator side of the protocol",
363			);
364		},
365		DisconnectFromBackingGroups => {
366			gum::warn!(
367				target: LOG_TARGET,
368				"DisconnectFromBackingGroups message is not expected on the validator side of the protocol",
369			);
370		},
371	}
372}
373
374/// Bridge event switch.
375async fn handle_network_msg<Sender: CollatorProtocolSenderTrait>(
376	sender: &mut Sender,
377	state: &mut State<PersistentDb>,
378	bridge_message: NetworkBridgeEvent<net_protocol::CollatorProtocolMessage>,
379) -> Result<()> {
380	use NetworkBridgeEvent::*;
381
382	match bridge_message {
383		PeerConnected(peer_id, observed_role, protocol_version, _) => {
384			let version = match protocol_version.try_into() {
385				Ok(version) => version,
386				Err(err) => {
387					// Network bridge is expected to handle this.
388					gum::error!(
389						target: LOG_TARGET,
390						?peer_id,
391						?observed_role,
392						?err,
393						"Unsupported protocol version"
394					);
395					return Ok(());
396				},
397			};
398			state.handle_peer_connected(sender, peer_id, version).await;
399		},
400		PeerDisconnected(peer_id) => {
401			state.handle_peer_disconnected(peer_id).await;
402		},
403		NewGossipTopology { .. } => {
404			// impossible!
405		},
406		PeerViewChange(_, _) => {
407			// We don't really care about a peer's view.
408		},
409		OurViewChange(view) => {
410			state.handle_our_view_change(sender, view).await?;
411		},
412		PeerMessage(remote, msg) => {
413			process_incoming_peer_message(sender, state, remote, msg).await;
414		},
415		UpdatedAuthorityIds { .. } => {
416			// The validator side doesn't deal with `AuthorityDiscoveryId`s.
417		},
418	}
419
420	Ok(())
421}
422
423async fn process_incoming_peer_message<Sender: CollatorProtocolSenderTrait>(
424	sender: &mut Sender,
425	state: &mut State<PersistentDb>,
426	origin: PeerId,
427	msg: CollationProtocols<
428		protocol_v1::CollatorProtocolMessage,
429		protocol_v2::CollatorProtocolMessage,
430		protocol_v3::CollatorProtocolMessage,
431	>,
432) {
433	use protocol_v1::CollatorProtocolMessage as V1;
434	use protocol_v2::CollatorProtocolMessage as V2;
435	use protocol_v3::CollatorProtocolMessage as V3;
436
437	match msg {
438		CollationProtocols::V1(V1::Declare(_collator_id, para_id, _signature)) |
439		CollationProtocols::V2(V2::Declare(_collator_id, para_id, _signature)) |
440		CollationProtocols::V3(V3::Declare(_collator_id, para_id, _signature)) => {
441			state.handle_declare(sender, origin, para_id).await;
442		},
443		CollationProtocols::V1(V1::CollationSeconded(..)) |
444		CollationProtocols::V2(V2::CollationSeconded(..)) |
445		CollationProtocols::V3(V3::CollationSeconded(..)) => {
446			gum::warn!(
447				target: LOG_TARGET,
448				peer_id = ?origin,
449				"Unexpected `CollationSeconded` message",
450			);
451		},
452		CollationProtocols::V1(V1::AdvertiseCollation(relay_parent)) => {
453			state.handle_advertisement(sender, origin, relay_parent, None, None).await;
454		},
455		CollationProtocols::V2(V2::AdvertiseCollation {
456			scheduling_parent,
457			candidate_hash,
458			parent_head_data_hash,
459			..
460		}) => {
461			state
462				.handle_advertisement(
463					sender,
464					origin,
465					scheduling_parent,
466					Some(ProspectiveCandidate { candidate_hash, parent_head_data_hash }),
467					None,
468				)
469				.await;
470		},
471		CollationProtocols::V3(V3::AdvertiseCollation {
472			scheduling_parent,
473			candidate_hash,
474			parent_head_data_hash,
475			candidate_descriptor_version,
476			..
477		}) => {
478			state
479				.handle_advertisement(
480					sender,
481					origin,
482					scheduling_parent,
483					Some(ProspectiveCandidate { candidate_hash, parent_head_data_hash }),
484					Some(candidate_descriptor_version),
485				)
486				.await;
487		},
488	}
489}