1use std::{cmp::max, collections::HashMap, ops::Range, sync::Arc, time::Duration};
12
13use ::light_client::{
14 LightClient,
15 client::{FallbackClient, QueryServiceClient},
16 state::{Genesis as LightClientGenesis, LightClientOptions},
17 storage::LightClientDbOptions,
18};
19use alloy::primitives::U256;
20use anyhow::{Context, anyhow, bail, ensure};
21use async_lock::RwLock;
22use async_trait::async_trait;
23use committable::{Commitment, Committable};
24use derivative::Derivative;
25use espresso_api::error::SubmitError;
26use espresso_types::{
27 Certificate2, ChainConfig, Header, Leaf2, NodeState, PubKey, SeqTypes, Transaction,
28 ValidatedState,
29 traits::MembershipPersistence,
30 v0::traits::{SequencerPersistence, StateCatchup, StateCertPersistence},
31 v0_1::ChainId,
32};
33use futures::{FutureExt, future::BoxFuture};
34use hotshot_query_service::{
35 availability::{BlockInfo, BlockQueryData, LeafQueryData, VidCommonQueryData},
36 types::HeightIndexed,
37};
38use hotshot_types::{
39 ValidatorConfig,
40 data::{EpochNumber, VidShare, ViewNumber},
41 epoch_membership::EpochMembershipCoordinator,
42 message::UpgradeLock,
43 network::NetworkConfig,
44 traits::{
45 ValidatedState as _,
46 election::{Membership, MembershipSnapshot},
47 metrics::Metrics,
48 storage::Storage,
49 },
50 utils::{
51 StateAndDelta, epoch_from_block_number, is_epoch_root, is_transition_block,
52 root_block_in_epoch,
53 },
54};
55use http_client::{Client, StatusCode, error::ClientErr};
56use parking_lot::Mutex;
57use tokio::{sync::watch, time::sleep};
58use url::Url;
59use versions::NEW_PROTOCOL_VERSION;
60
61use crate::{
62 CatchupParams, Genesis, L1Params, LoadedNetworkConfig, NodeStateParts, SequencerApiVersion,
63 api::{
64 DecideSink,
65 context::{ApiContext, ConsensusSource, Delta, NodeLightClient},
66 light_client_genesis,
67 },
68 apply_genesis_overrides,
69 context::TaskList,
70 init_node_state, load_or_fetch_network_config,
71 startup_catchup::bootstrap_epoch_window,
72 state_signature::StateSignatureMemStorage,
73};
74
75#[derive(Clone, Debug)]
76pub struct FollowerOptions {
77 pub poll_interval: Duration,
78 pub max_blocks_per_poll: u64,
80 pub light_client: LightClientOptions,
81 pub light_client_db: LightClientDbOptions,
82 pub light_client_genesis: Option<LightClientGenesis>,
84}
85
86impl FollowerOptions {
87 fn validate(&self) -> anyhow::Result<()> {
88 ensure!(
89 self.max_blocks_per_poll > 0,
90 "max_blocks_per_poll must be at least 1: a follower ingesting no blocks per poll \
91 never follows"
92 );
93 ensure!(
94 !self.poll_interval.is_zero(),
95 "poll_interval must be positive: a follower polling without pause spins on its \
96 upstreams"
97 );
98 Ok(())
99 }
100}
101
102#[derive(Clone, Debug)]
103pub struct FollowerParams {
104 pub upstreams: Vec<Url>,
105 pub config_peers: Option<Vec<Url>>,
107 pub catchup: CatchupParams,
108 pub bootstrap_epoch_catchup_timeout: Duration,
109 pub options: FollowerOptions,
110}
111
112#[derive(Derivative, Clone)]
115#[derivative(Debug(bound = ""))]
116pub struct FollowerHandle<P: SequencerPersistence> {
117 #[derivative(Debug = "ignore")]
118 consensus: Arc<FollowerConsensus>,
119 #[derivative(Debug = "ignore")]
120 light_client: Option<Arc<NodeLightClient>>,
121 node_state: NodeState,
122 #[derivative(Debug = "ignore")]
123 persistence: Arc<P>,
124 network_config: NetworkConfig<SeqTypes>,
125 #[derivative(Debug = "ignore")]
129 tasks: Arc<Mutex<TaskList>>,
130}
131
132#[derive(Derivative)]
136#[derivative(Debug(bound = ""))]
137pub struct FollowerContext<P: SequencerPersistence> {
138 handle: FollowerHandle<P>,
139 tasks: TaskList,
140 detached: bool,
141}
142
143pub async fn init_follower_node<P>(
145 genesis: Genesis,
146 params: FollowerParams,
147 metrics: Box<dyn Metrics>,
148 persistence: P,
149 l1_params: L1Params,
150 sink: impl DecideSink + 'static,
151) -> anyhow::Result<FollowerHandle<P>>
152where
153 P: SequencerPersistence + MembershipPersistence,
154 Arc<P>: Storage<SeqTypes>,
155{
156 init_with_source(genesis, params, metrics, persistence, l1_params, sink, None).await
157}
158
159#[cfg(any(test, feature = "testing"))]
162pub(crate) async fn init_replay_node<P>(
163 genesis: Genesis,
164 params: FollowerParams,
165 metrics: Box<dyn Metrics>,
166 persistence: P,
167 l1_params: L1Params,
168 sink: impl DecideSink + 'static,
169 source: Arc<dyn ChainSource>,
170) -> anyhow::Result<FollowerHandle<P>>
171where
172 P: SequencerPersistence + MembershipPersistence,
173 Arc<P>: Storage<SeqTypes>,
174{
175 init_with_source(
176 genesis,
177 params,
178 metrics,
179 persistence,
180 l1_params,
181 sink,
182 Some(source),
183 )
184 .await
185}
186
187async fn init_with_source<P>(
188 genesis: Genesis,
189 params: FollowerParams,
190 metrics: Box<dyn Metrics>,
191 persistence: P,
192 l1_params: L1Params,
193 sink: impl DecideSink + 'static,
194 replay: Option<Arc<dyn ChainSource>>,
195) -> anyhow::Result<FollowerHandle<P>>
196where
197 P: SequencerPersistence + MembershipPersistence,
198 Arc<P>: Storage<SeqTypes>,
199{
200 params.options.validate()?;
201
202 let validator_config = ValidatorConfig::<SeqTypes>::generated_from_seed_indexed(
204 rand::random(),
205 0,
206 U256::ONE,
207 false,
208 );
209 let config_peers = params
210 .config_peers
211 .clone()
212 .unwrap_or_else(|| params.upstreams.clone());
213 let (mut network_config, fetched) = match load_or_fetch_network_config(
214 &persistence,
215 Some(config_peers),
216 &validator_config,
217 params.catchup.backoff,
218 params.catchup.base_timeout,
219 )
220 .await?
221 {
222 Some(LoadedNetworkConfig::Persisted(config)) => (config, false),
223 Some(LoadedNetworkConfig::Fetched(config)) => (config, true),
224 None => bail!("a follower needs peers to fetch the network config from"),
225 };
226 apply_genesis_overrides(&mut network_config, &genesis);
227 if fetched {
228 persistence.save_config(&network_config).await?;
229 }
230 let epoch_height = genesis.epoch_height.unwrap_or_default();
233 ensure!(
234 epoch_height > 0,
235 "a follower needs a positive epoch_height in genesis"
236 );
237 let upgrade = versions::Upgrade::new(genesis.base_version, genesis.upgrade_version);
238
239 let NodeStateParts {
240 node_state,
241 state_catchup: _,
242 persistence,
243 } = init_node_state(
244 &genesis,
245 &network_config,
246 l1_params,
247 params.catchup.clone(),
248 persistence,
249 &*metrics,
250 )
251 .await?;
252 let coordinator = node_state.coordinator.clone();
253
254 let current_epoch = bootstrap_epoch_window(
255 &coordinator,
256 epoch_height,
257 params.bootstrap_epoch_catchup_timeout,
258 )
259 .await
260 .context("startup stake-table catchup failed")?;
261 tracing::info!(%current_epoch, "startup catchup complete");
262
263 let (source, light_client): (Arc<dyn ChainSource>, _) = match replay {
264 Some(source) => (source, None),
265 None => {
266 let light_client = Arc::new(
267 connect_light_client(
268 ¶ms.options,
269 ¶ms.upstreams,
270 &network_config,
271 genesis.chain_config.chain_id,
272 fetched,
273 )
274 .await?,
275 );
276 (light_client.clone(), Some(light_client))
277 },
278 };
279
280 let upgrade_lock =
283 UpgradeLock::from_certificate(upgrade, &persistence.load_upgrade_certificate().await?);
284
285 let (decided, decided_rx) = watch::channel(None);
286 let chain_config = Arc::new(RwLock::new(genesis.chain_config));
287 let consensus = FollowerConsensus {
288 decided: decided_rx,
289 chain_config: chain_config.clone(),
290 coordinator: coordinator.clone(),
291 upgrade_lock: upgrade_lock.clone(),
292 upstreams: params.upstreams,
293 epoch_height,
294 };
295
296 let next = sink
297 .block_height()
298 .await
299 .context("reading the height the query database has reached")?;
300 let follower = Follower {
301 source,
302 sink: Arc::new(sink),
303 persistence: persistence.clone(),
304 coordinator,
305 state_catchup: node_state.state_catchup.clone(),
306 decided,
307 chain_config,
308 upgrade_lock,
309 epoch_height,
310 options: params.options,
311 bootstrap_epoch_catchup_timeout: params.bootstrap_epoch_catchup_timeout,
312 next,
313 };
314 let mut tasks = TaskList::default();
315 tasks.spawn("light client follower", follower.run());
316
317 Ok(FollowerHandle {
318 consensus: Arc::new(consensus),
319 light_client,
320 node_state,
321 persistence,
322 network_config,
323 tasks: Arc::new(Mutex::new(tasks)),
324 })
325}
326
327impl<P: SequencerPersistence> FollowerContext<P> {
328 pub fn new(handle: FollowerHandle<P>) -> Self {
331 let tasks = std::mem::take(&mut *handle.tasks.lock());
332 Self {
333 handle,
334 tasks,
335 detached: false,
336 }
337 }
338
339 pub fn handle(&self) -> FollowerHandle<P> {
340 self.handle.clone()
341 }
342
343 pub async fn decided_leaf(&self) -> Leaf2 {
345 self.handle.consensus.decided_leaf().await
346 }
347
348 pub fn node_state(&self) -> NodeState {
349 self.handle.node_state.clone()
350 }
351
352 pub fn light_client(&self) -> Option<Arc<NodeLightClient>> {
354 self.handle.light_client.clone()
355 }
356
357 pub async fn shut_down(&mut self) {
358 tracing::info!("shutting down FollowerContext");
359 self.tasks.shut_down();
360 self.handle.node_state.l1_client.shut_down_tasks().await;
361 self.detached = true;
362 }
363
364 pub async fn join(&mut self) {
365 self.tasks.join().await;
366 }
367
368 pub fn detach(&mut self) {
370 self.detached = true;
371 }
372}
373
374impl<P: SequencerPersistence> Drop for FollowerContext<P> {
375 fn drop(&mut self) {
376 if !self.detached {
377 let tasks = self.tasks.clone();
378 let l1_client = self.handle.node_state.l1_client.clone();
379 tokio::spawn(async move {
380 tracing::info!("shutting down FollowerContext");
381 tasks.shut_down();
382 l1_client.shut_down_tasks().await;
383 });
384 self.detached = true;
385 }
386 }
387}
388
389impl<P: SequencerPersistence> ApiContext for FollowerHandle<P> {
390 fn consensus(&self) -> Arc<dyn ConsensusSource> {
391 self.consensus.clone()
392 }
393
394 fn membership(&self) -> Arc<dyn MembershipPersistence> {
395 self.persistence.clone()
396 }
397
398 fn state_certs(&self) -> Arc<dyn StateCertPersistence> {
399 self.persistence.clone()
400 }
401
402 fn node_state(&self) -> NodeState {
403 self.node_state.clone()
404 }
405
406 fn network_config(&self) -> NetworkConfig<SeqTypes> {
407 self.network_config.clone()
408 }
409
410 fn validator_config(&self) -> Option<&ValidatorConfig<SeqTypes>> {
411 None
412 }
413
414 fn state_signatures(&self) -> Option<Arc<RwLock<StateSignatureMemStorage>>> {
415 None
416 }
417
418 fn light_client(&self) -> Option<Arc<NodeLightClient>> {
419 self.light_client.clone()
420 }
421
422 fn request_vid_shares(
423 &self,
424 _block_number: u64,
425 _vid_common: VidCommonQueryData<SeqTypes>,
426 _timeout: Duration,
427 ) -> BoxFuture<'static, anyhow::Result<Vec<VidShare>>> {
428 futures::future::ready(Err(anyhow!(
429 "a follower has no request-response protocol to fetch VID shares with"
430 )))
431 .boxed()
432 }
433
434 fn with_task_list(self, tasks: TaskList) -> Self {
435 self.tasks.lock().extend(tasks);
436 self
437 }
438}
439
440struct FollowerConsensus {
442 decided: watch::Receiver<Option<LeafQueryData<SeqTypes>>>,
443 chain_config: Arc<RwLock<ChainConfig>>,
444 coordinator: EpochMembershipCoordinator<SeqTypes>,
445 upgrade_lock: UpgradeLock<SeqTypes>,
446 upstreams: Vec<Url>,
447 epoch_height: u64,
448}
449
450#[async_trait]
451impl ConsensusSource for FollowerConsensus {
452 async fn decided_leaf(&self) -> Leaf2 {
453 let mut decided = self.decided.clone();
454 let leaf = decided
455 .wait_for(Option::is_some)
456 .await
457 .expect("the follower context holds the sender");
458 leaf.as_ref()
459 .expect("waited for a decided leaf")
460 .leaf()
461 .clone()
462 }
463
464 async fn decided_state(&self) -> Option<Arc<ValidatedState>> {
465 let leaf = self.decided.borrow().clone()?;
466 let mut state = ValidatedState::from_header(leaf.header());
467 state.chain_config = (*self.chain_config.read().await).into();
468 Some(Arc::new(state))
469 }
470
471 async fn state(&self, _view: ViewNumber) -> Option<Arc<ValidatedState>> {
472 None
473 }
474
475 async fn state_and_delta(&self, _view: ViewNumber) -> StateAndDelta<SeqTypes> {
476 (None, None)
477 }
478
479 async fn undecided_leaves(&self) -> Vec<Leaf2> {
480 Vec::new()
481 }
482
483 async fn current_epoch(&self) -> Option<EpochNumber> {
484 let decided = self.decided.borrow();
485 decided
486 .as_ref()
487 .and_then(|leaf| leaf.leaf().epoch(self.epoch_height))
488 }
489
490 async fn membership_coordinator(&self) -> EpochMembershipCoordinator<SeqTypes> {
491 self.coordinator.clone()
492 }
493
494 async fn upgrade_lock(&self) -> UpgradeLock<SeqTypes> {
495 self.upgrade_lock.clone()
496 }
497
498 async fn submit_transaction(&self, tx: Transaction) -> anyhow::Result<Commitment<Transaction>> {
499 let mut last_err = anyhow!("no upstream query nodes to forward the transaction to");
500 for upstream in &self.upstreams {
501 let client = Client::<ClientErr, SequencerApiVersion>::new(upstream.clone());
502 let result = client
503 .post::<Commitment<Transaction>>("submit/submit")
504 .body_binary(&tx)?
505 .send()
506 .await;
507 match result {
508 Ok(commitment) => return Ok(commitment),
509 Err(err) => {
510 tracing::warn!(%upstream, %err, "forwarding transaction failed");
511 match err.status {
512 StatusCode::BAD_REQUEST => {
514 return Err(SubmitError::Invalid(err.message).into());
515 },
516 StatusCode::SERVICE_UNAVAILABLE => {
517 last_err = SubmitError::Overloaded(err.message).into();
518 },
519 _ => {
520 last_err = anyhow::Error::from(err)
521 .context(format!("forwarding to {upstream}"));
522 },
523 }
524 },
525 }
526 }
527 Err(last_err)
528 }
529
530 async fn update_leaf(
531 &self,
532 _leaf: Leaf2,
533 _state: Arc<ValidatedState>,
534 _delta: Option<Arc<Delta>>,
535 ) -> anyhow::Result<()> {
536 Ok(())
538 }
539
540 async fn current_proposal_participation(&self) -> HashMap<PubKey, f64> {
541 HashMap::new()
542 }
543
544 async fn proposal_participation(&self, _epoch: EpochNumber) -> HashMap<PubKey, f64> {
545 HashMap::new()
546 }
547
548 async fn current_vote_participation(&self) -> HashMap<PubKey, f64> {
549 HashMap::new()
550 }
551
552 async fn vote_participation(&self, _epoch: EpochNumber) -> HashMap<PubKey, f64> {
553 HashMap::new()
554 }
555}
556
557struct Follower<P> {
558 source: Arc<dyn ChainSource>,
559 sink: Arc<dyn DecideSink>,
560 persistence: Arc<P>,
561 coordinator: EpochMembershipCoordinator<SeqTypes>,
562 state_catchup: Arc<dyn StateCatchup>,
563 decided: watch::Sender<Option<LeafQueryData<SeqTypes>>>,
564 chain_config: Arc<RwLock<ChainConfig>>,
565 upgrade_lock: UpgradeLock<SeqTypes>,
566 epoch_height: u64,
567 options: FollowerOptions,
568 bootstrap_epoch_catchup_timeout: Duration,
569 next: u64,
570}
571
572#[async_trait]
574pub(crate) trait ChainSource: Send + Sync {
575 async fn block_height(&self) -> anyhow::Result<u64>;
576
577 async fn fetch_range(&self, from: u64, to: u64) -> anyhow::Result<Vec<VerifiedBlock>>;
579
580 async fn fetch_cert2(&self, header: &Header) -> anyhow::Result<Option<Certificate2<SeqTypes>>>;
582}
583
584pub(crate) struct VerifiedBlock {
585 pub(crate) leaf: LeafQueryData<SeqTypes>,
586 pub(crate) block: BlockQueryData<SeqTypes>,
587 pub(crate) vid_common: VidCommonQueryData<SeqTypes>,
588}
589
590impl<P: SequencerPersistence> Follower<P> {
591 async fn run(mut self) {
592 tracing::info!(from = self.next, "following the chain");
593 loop {
594 if let Err(err) = self.poll().await {
595 tracing::warn!(
596 next = self.next,
597 err = %format!("{err:#}"),
598 "following the chain failed"
599 );
600 }
601 sleep(self.options.poll_interval).await;
602 }
603 }
604
605 async fn poll(&mut self) -> anyhow::Result<()> {
606 let tip = self
607 .source
608 .block_height()
609 .await
610 .context("fetching the chain height")?;
611 let from = max(
612 self.next,
613 tip.saturating_sub(self.options.max_blocks_per_poll),
614 );
615 if from >= tip {
616 return Ok(());
617 }
618 if from > self.next {
619 tracing::info!(
620 next = self.next,
621 from,
622 tip,
623 "skipping ahead; the query service backfills the gap above its pruned height"
624 );
625 self.catch_up_skipped_epochs(self.next..from).await?;
626 self.next = from;
627 }
628 for block in self.source.fetch_range(from, tip).await? {
629 let height = block.leaf.height();
630 ensure!(
631 height == self.next,
632 "the light client returned block {height} where {} was expected",
633 self.next
634 );
635 self.follow(block)
636 .await
637 .with_context(|| format!("following block {height}"))?;
638 self.next = height + 1;
639 }
640 Ok(())
641 }
642
643 async fn follow(&self, block: VerifiedBlock) -> anyhow::Result<()> {
644 let VerifiedBlock {
645 leaf,
646 block,
647 vid_common,
648 } = block;
649 let mut info = BlockInfo::new(leaf.clone(), Some(block), Some(vid_common), None);
650 if leaf.header().version() >= NEW_PROTOCOL_VERSION
653 && let Some(cert2) = self
654 .source
655 .fetch_cert2(leaf.header())
656 .await
657 .context("fetching cert2")?
658 {
659 info = info.with_cert2(cert2);
660 }
661 self.sink.append(info).await.context("storing block")?;
662
663 self.track_upgrade(leaf.leaf()).await;
664 self.track_epoch(leaf.leaf()).await?;
665 self.track_chain_config(leaf.header()).await;
666 self.decided.send_replace(Some(leaf));
667 Ok(())
668 }
669
670 async fn track_upgrade(&self, leaf: &Leaf2) {
673 let Some(cert) = leaf.upgrade_certificate() else {
674 return;
675 };
676 if self.upgrade_lock.decided_upgrade_cert().as_ref() == Some(cert) {
677 return;
678 }
679 tracing::warn!(
680 height = leaf.height(),
681 version = ?cert.data.new_version,
682 "the network decided an upgrade"
683 );
684 self.upgrade_lock.set_decided_upgrade_cert(cert.clone());
685 if let Err(err) = self
686 .persistence
687 .store_upgrade_certificate(Some(cert.clone()))
688 .await
689 {
690 tracing::warn!(
691 height = leaf.height(),
692 "cannot persist the upgrade certificate: {err:#}"
693 );
694 }
695 }
696
697 async fn track_epoch(&self, leaf: &Leaf2) -> anyhow::Result<()> {
701 let height = leaf.height();
702 let Some(epoch) = leaf.epoch(self.epoch_height) else {
703 return Ok(());
704 };
705 if is_epoch_root(height, self.epoch_height) && !self.has_stake_table(epoch + 2) {
706 self.coordinator
707 .add_epoch_root(leaf.block_header().clone())
708 .await
709 .map_err(|err| anyhow!("deriving the stake table of epoch {}: {err}", epoch + 2))?;
710 }
711 if is_transition_block(height, self.epoch_height)
712 && let Some(drb) = leaf.next_drb_result
713 && !self.has_drb(epoch + 1)
714 {
715 self.coordinator.supply_drb(epoch + 1, drb);
716 }
717 Ok(())
718 }
719
720 async fn catch_up_skipped_epochs(&self, skipped: Range<u64>) -> anyhow::Result<()> {
724 if skipped.is_empty() {
725 return Ok(());
726 }
727 let first_epoch = self.coordinator.membership().first_epoch();
728 let first = max(
729 epoch_from_block_number(skipped.start, self.epoch_height),
730 first_epoch.u64(),
731 );
732 let last = epoch_from_block_number(skipped.end - 1, self.epoch_height);
733 let missing = (first..=last).any(|epoch| {
734 skipped.contains(&root_block_in_epoch(epoch, self.epoch_height))
735 && !self.has_stake_table(EpochNumber::new(epoch + 2))
736 });
737 if !missing {
738 return Ok(());
739 }
740 tracing::warn!(
741 ?skipped,
742 "skipped epoch roots whose stake tables are missing; catching up from peers"
743 );
744 let current_epoch = bootstrap_epoch_window(
745 &self.coordinator,
746 self.epoch_height,
747 self.bootstrap_epoch_catchup_timeout,
748 )
749 .await
750 .context("stake-table catchup after skipping ahead")?;
751 tracing::info!(%current_epoch, "stake-table catchup complete");
752 Ok(())
753 }
754
755 fn has_stake_table(&self, epoch: EpochNumber) -> bool {
756 self.coordinator.membership().snapshot(epoch).is_some()
757 }
758
759 fn has_drb(&self, epoch: EpochNumber) -> bool {
760 self.coordinator
761 .membership()
762 .snapshot(epoch)
763 .is_some_and(|snapshot| snapshot.has_drb())
764 }
765
766 async fn track_chain_config(&self, header: &Header) {
768 let resolvable = header.chain_config();
769 if resolvable.commit() == self.chain_config.read().await.commit() {
770 return;
771 }
772 let resolved = match resolvable.resolve() {
773 Some(chain_config) => Ok(chain_config),
774 None => {
775 self.state_catchup
776 .fetch_chain_config(resolvable.commit())
777 .await
778 },
779 };
780 match resolved {
781 Ok(chain_config) => *self.chain_config.write().await = chain_config,
782 Err(err) => tracing::warn!(
783 height = header.height(),
784 "cannot resolve the chain config: {err:#}"
785 ),
786 }
787 }
788}
789
790#[async_trait]
791impl ChainSource for NodeLightClient {
792 async fn block_height(&self) -> anyhow::Result<u64> {
793 Ok(NodeLightClient::block_height(self).await?)
794 }
795
796 async fn fetch_range(&self, from: u64, to: u64) -> anyhow::Result<Vec<VerifiedBlock>> {
798 let (from, to) = (from as usize, to as usize);
799 let leaves = self
800 .fetch_leaves_in_range(from, to)
801 .await
802 .context("fetching leaves")?;
803 let blocks = self
804 .fetch_blocks_and_vid_common_in_range(from, to)
805 .await
806 .context("fetching payloads")?;
807 ensure!(
808 leaves.len() == to - from && blocks.len() == to - from,
809 "the light client returned {} leaves and {} payloads for {} blocks",
810 leaves.len(),
811 blocks.len(),
812 to - from
813 );
814 Ok(leaves
815 .into_iter()
816 .zip(blocks)
817 .map(|(leaf, (block, vid_common))| VerifiedBlock {
818 leaf,
819 block,
820 vid_common,
821 })
822 .collect())
823 }
824
825 async fn fetch_cert2(&self, header: &Header) -> anyhow::Result<Option<Certificate2<SeqTypes>>> {
826 Ok(self.fetch_certificate2_for_header(header).await?)
827 }
828}
829
830async fn connect_light_client(
831 options: &FollowerOptions,
832 upstreams: &[Url],
833 network_config: &NetworkConfig<SeqTypes>,
834 chain_id: ChainId,
835 config_fetched: bool,
836) -> anyhow::Result<NodeLightClient> {
837 let db = options
838 .light_client_db
839 .clone()
840 .connect()
841 .await
842 .context("opening the light client database")?;
843 let client = FallbackClient::new(
844 upstreams
845 .iter()
846 .cloned()
847 .map(QueryServiceClient::new)
848 .collect(),
849 )?;
850 let genesis = match options.light_client_genesis.clone() {
851 Some(genesis) => genesis,
852 None => {
853 if config_fetched {
854 tracing::warn!(
855 "deriving the light client's root of trust from a network config fetched from \
856 peers; pin a light client genesis unless those peers are trusted"
857 );
858 }
859 light_client_genesis(network_config, chain_id)
860 },
861 };
862 Ok(LightClient::from_genesis_with_options(
863 db,
864 client,
865 genesis,
866 options.light_client.clone(),
867 ))
868}