zebra_rpc/sync.rs
1//! Syncer task for maintaining a non-finalized state in Zebra's ReadStateService and updating `ChainTipSender` via RPCs
2
3use std::{
4 net::SocketAddr,
5 sync::Arc,
6 time::{Duration, Instant},
7};
8
9use tokio::task::JoinHandle;
10use tonic::{Status, Streaming};
11use tower::BoxError;
12use zebra_chain::{
13 block::{self, Block, Height},
14 parameters::Network,
15 serialization::BytesInDisplayOrder,
16};
17use zebra_state::{
18 spawn_init_read_only, ChainTipBlock, ChainTipChange, ChainTipSender, CheckpointVerifiedBlock,
19 HashOrHeight, LatestChainTip, NonFinalizedState, ReadStateService, SemanticallyVerifiedBlock,
20 ValidateContextError, ZebraDb,
21};
22
23use zebra_chain::diagnostic::task::WaitForPanics;
24
25use crate::indexer::{
26 indexer_client::IndexerClient, BlockAndHash, BlockRequest, Empty,
27 NonFinalizedStateChangeRequest,
28};
29
30/// How long to wait between calls to `subscribe_to_non_finalized_state_change` when it returns an error.
31const POLL_DELAY: Duration = Duration::from_secs(5);
32
33/// How long to wait for a message on a gRPC subscription stream before assuming the stream is dead
34/// and re-subscribing.
35///
36/// Generous, because legitimate gaps between blocks/tip changes can be several minutes; this is a
37/// backstop against a wedged connection that the keep-alive ping below doesn't catch. Re-subscribing
38/// is cheap and resumes from the syncer's current chain tips, so an occasional false trigger during
39/// a quiet period is harmless.
40const STREAM_MESSAGE_TIMEOUT: Duration = Duration::from_secs(10 * 60);
41
42/// HTTP/2 keep-alive ping interval for the indexer gRPC connection, so a half-open connection is
43/// detected promptly instead of hanging a stream read indefinitely.
44const KEEPALIVE_INTERVAL: Duration = Duration::from_secs(60);
45
46/// How long to wait for a keep-alive ping response before treating the connection as dead.
47const KEEPALIVE_TIMEOUT: Duration = Duration::from_secs(20);
48
49/// How long to wait before re-subscribing after a block fails to commit to the non-finalized state.
50///
51/// A block can persistently fail to commit (e.g. [`ValidateContextError::NotReadyToBeCommitted`])
52/// when the secondary finalized state hasn't yet caught up with the primary. Without this delay,
53/// re-subscribing immediately turns that into a full-speed busy loop that saturates the logs.
54const COMMIT_RETRY_DELAY: Duration = Duration::from_secs(1);
55
56/// How long to wait before re-subscribing after a subscription stream ends or returns an error.
57///
58/// A stream ending isn't necessarily a connection failure: the server can drop a subscription while
59/// it's still reachable, and re-subscribing then succeeds and is dropped again. Without this delay
60/// that becomes a full-speed subscribe/drop loop that saturates the logs on both sides.
61const STREAM_RESTART_DELAY: Duration = Duration::from_secs(1);
62
63/// How long to wait for a single `get_block` fetch while bridging the finalized gap before giving
64/// up. A single block fetch from a co-located node should return promptly, so this bounds a wedged
65/// connection without false-triggering; the bridge retries on the next subscription.
66const GET_BLOCK_TIMEOUT: Duration = Duration::from_secs(30);
67
68/// How long to wait to establish a gRPC subscription stream before assuming the request is wedged
69/// and retrying. The subscription handshake should complete promptly.
70const SUBSCRIBE_TIMEOUT: Duration = Duration::from_secs(30);
71
72/// Syncs non-finalized blocks in the best chain from a trusted Zebra node's RPC methods.
73#[derive(Debug)]
74pub struct TrustedChainSync {
75 /// gRPC client for calling Zebra's indexer methods.
76 pub indexer_rpc_client: IndexerClient<tonic::transport::Channel>,
77 /// The read state service.
78 db: ZebraDb,
79 /// The non-finalized state - currently only contains the best chain.
80 non_finalized_state: NonFinalizedState,
81 /// The chain tip sender for updating [`LatestChainTip`] and [`ChainTipChange`].
82 chain_tip_sender: ChainTipSender,
83 /// The non-finalized state sender, for updating the [`ReadStateService`] when the non-finalized best chain changes.
84 non_finalized_state_sender: tokio::sync::watch::Sender<NonFinalizedState>,
85 /// Flipped to `true` once `sync()` receives its first parseable block from the
86 /// `non_finalized_state_change` stream, which stops [`update_finalized_chain_tip`] so `sync()`
87 /// becomes the sole caller of `try_catch_up_with_primary` on the shared secondary db.
88 started_sync_sender: tokio::sync::watch::Sender<bool>,
89}
90
91async fn update_finalized_chain_tip(
92 db: ZebraDb,
93 mut indexer_rpc_client: IndexerClient<tonic::transport::Channel>,
94 mut finalized_chain_tip_sender: ChainTipSender,
95 mut started_sync_receiver: tokio::sync::watch::Receiver<bool>,
96) {
97 let mut chain_tip_change_stream = None;
98
99 loop {
100 // Stop as soon as `sync()` has received its first parseable non-finalized block. From then
101 // on `sync()` is the sole caller of `try_catch_up_with_primary` on the shared secondary, so
102 // this task must not advance the secondary's view concurrently (which would let a block be
103 // finalized between `sync()`'s check and its commit, failing as a duplicate-effects error).
104 // `sync()` also owns the published chain tip from then on, via its (higher) non-finalized
105 // tip, so publishing our lagging finalized tip would drag the reported tip backwards.
106 if *started_sync_receiver.borrow() {
107 return;
108 }
109
110 let Some(ref mut chain_tip_change) = chain_tip_change_stream else {
111 chain_tip_change_stream = match tokio::time::timeout(
112 SUBSCRIBE_TIMEOUT,
113 indexer_rpc_client.chain_tip_change(Empty {}),
114 )
115 .await
116 {
117 Ok(Ok(response)) => Some(response.into_inner()),
118 Ok(Err(err)) => {
119 tracing::warn!(?err, "failed to subscribe to chain tip changes");
120 tokio::time::sleep(POLL_DELAY).await;
121 None
122 }
123 Err(_) => {
124 tracing::warn!("timed out subscribing to chain tip changes");
125 tokio::time::sleep(POLL_DELAY).await;
126 None
127 }
128 };
129
130 continue;
131 };
132
133 // The message is only a signal that the primary's best chain advanced. We publish our own
134 // finalized tip below, not the primary's (non-finalized) best tip, so the hash is unused.
135 //
136 // Stop immediately if `sync()` takes over while we're parked here, rather than waiting for
137 // the next chain-tip change, so we never catch up concurrently with `sync()`'s commits.
138 let message = tokio::select! {
139 biased;
140 _ = started_sync_receiver.changed() => return,
141 message = tokio::time::timeout(STREAM_MESSAGE_TIMEOUT, chain_tip_change.message()) => message,
142 };
143
144 match message {
145 Ok(Ok(Some(_block_hash_and_height))) => {}
146 Ok(Ok(None)) => {
147 tracing::warn!("chain_tip_change stream ended unexpectedly");
148 chain_tip_change_stream = None;
149 tokio::time::sleep(STREAM_RESTART_DELAY).await;
150 continue;
151 }
152 Ok(Err(err)) => {
153 tracing::warn!(?err, "error receiving chain tip change");
154 chain_tip_change_stream = None;
155 tokio::time::sleep(STREAM_RESTART_DELAY).await;
156 continue;
157 }
158 Err(_) => {
159 tracing::debug!("chain tip change stream timed out, re-subscribing");
160 chain_tip_change_stream = None;
161 continue;
162 }
163 }
164
165 // Don't advance the secondary or publish once `sync()` has taken over (it may have done so
166 // while we waited above), so we never catch up concurrently with `sync()`'s commits.
167 if *started_sync_receiver.borrow() {
168 return;
169 }
170
171 // Catch the secondary's finalized state up to the primary, then publish its finalized tip.
172 //
173 // This keeps the finalized tip current while the secondary is still catching up, before
174 // `TrustedChainSync::sync()` has any non-finalized blocks of its own to publish.
175 //
176 // # Correctness
177 //
178 // Catching up with the primary db instance concurrent with commits to the non-finalized state
179 // when the underlying Zebra node is syncing blocks rapidly could cause repeated commit failures.
180 if let Err(error) = db.spawn_try_catch_up_with_primary().await {
181 tracing::debug!(
182 ?error,
183 "failed to catch up to the primary database while updating the finalized tip"
184 );
185 continue;
186 }
187
188 if let Some(tip_block) = finalized_chain_tip_block(&db).await {
189 // Re-check immediately before publishing, after the awaits above: `sync()` can take over
190 // while we wait on `spawn_try_catch_up_with_primary` or `finalized_chain_tip_block`, so
191 // checking any earlier would leave a window where we still publish the stale tip.
192 //
193 // `sync()`'s `ChainTipSender` latches onto the non-finalized tip, but this task holds a
194 // separate `finalized_sender()` handle that doesn't, so `set_finalized_tip` would keep
195 // overwriting the non-finalized tip rather than becoming a no-op. The task therefore
196 // stops itself. The non-finalized state only grows once populated, so this handover is
197 // permanent.
198 if *started_sync_receiver.borrow() {
199 return;
200 }
201
202 finalized_chain_tip_sender.set_finalized_tip(tip_block);
203 }
204 }
205}
206
207/// Reads the finalized tip block from the secondary db instance and converts it to a
208/// [`ChainTipBlock`].
209async fn finalized_chain_tip_block(db: &ZebraDb) -> Option<ChainTipBlock> {
210 let db = db.clone();
211 tokio::task::spawn_blocking(move || {
212 let (height, hash) = db.tip()?;
213 db.block(height.into())
214 .map(|block| CheckpointVerifiedBlock::with_hash(block, hash))
215 .map(ChainTipBlock::from)
216 })
217 .wait_for_panics()
218 .await
219}
220
221impl TrustedChainSync {
222 /// Creates a new [`TrustedChainSync`] with a [`ChainTipSender`], then spawns a task to sync blocks
223 /// from the node's non-finalized best chain.
224 ///
225 /// Returns the [`LatestChainTip`], [`ChainTipChange`], and a [`JoinHandle`] for the sync task.
226 pub async fn spawn(
227 indexer_rpc_address: SocketAddr,
228 db: ZebraDb,
229 non_finalized_state_sender: tokio::sync::watch::Sender<NonFinalizedState>,
230 ) -> Result<(LatestChainTip, ChainTipChange, JoinHandle<()>), BoxError> {
231 let non_finalized_state = NonFinalizedState::new(&db.network());
232 let (chain_tip_sender, latest_chain_tip, chain_tip_change) =
233 ChainTipSender::new(None, &db.network());
234 let channel =
235 tonic::transport::Endpoint::from_shared(format!("http://{indexer_rpc_address}"))?
236 .keep_alive_while_idle(true)
237 .http2_keep_alive_interval(KEEPALIVE_INTERVAL)
238 .keep_alive_timeout(KEEPALIVE_TIMEOUT)
239 .connect()
240 .await?;
241 let indexer_rpc_client = IndexerClient::new(channel);
242 let finalized_chain_tip_sender = chain_tip_sender.finalized_sender();
243
244 // `sync()` flips this to `true` as soon as it receives its first parseable non-finalized
245 // block, which stops `update_finalized_chain_tip` so `sync()` becomes the sole caller of
246 // `try_catch_up_with_primary` on the shared secondary db.
247 let (started_sync_sender, started_sync_receiver) = tokio::sync::watch::channel(false);
248
249 let mut syncer = Self {
250 indexer_rpc_client: indexer_rpc_client.clone(),
251 db: db.clone(),
252 non_finalized_state,
253 chain_tip_sender,
254 non_finalized_state_sender,
255 started_sync_sender,
256 };
257
258 // Spawn a task to send finalized chain tip changes to the chain tip change and latest chain tip channels.
259 tokio::spawn(async move {
260 update_finalized_chain_tip(
261 db,
262 indexer_rpc_client,
263 finalized_chain_tip_sender,
264 started_sync_receiver,
265 )
266 .await
267 });
268
269 let sync_task = tokio::spawn(async move {
270 syncer.sync().await;
271 });
272
273 Ok((latest_chain_tip, chain_tip_change, sync_task))
274 }
275
276 /// Starts syncing blocks from the node's non-finalized best chain and checking for chain tip changes in the finalized state.
277 ///
278 /// When the best chain tip in Zebra is not available in the finalized state or the local non-finalized state,
279 /// gets any unavailable blocks in Zebra's best chain from the RPC server, adds them to the local non-finalized state, then
280 /// sends the updated chain tip block and non-finalized state to the [`ChainTipSender`] and non-finalized state sender.
281 #[tracing::instrument(skip_all)]
282 async fn sync(&mut self) {
283 let mut non_finalized_blocks_listener = None;
284 // The hash of the block that most recently failed to commit, used to avoid re-logging the
285 // same warning at full rate while a block persistently fails to commit.
286 let mut last_failed_commit_hash = None;
287 // Whether we've signalled `update_finalized_chain_tip` to stop, so we only send once.
288 let mut signalled_started = false;
289 self.try_catch_up_with_primary().await;
290 if let Some(finalized_tip_block) = finalized_chain_tip_block(&self.db).await {
291 self.chain_tip_sender.set_finalized_tip(finalized_tip_block);
292 }
293
294 loop {
295 let Some(ref mut non_finalized_state_change) = non_finalized_blocks_listener else {
296 non_finalized_blocks_listener = match self
297 .subscribe_to_non_finalized_state_change()
298 .await
299 {
300 Ok(listener) => Some(listener),
301 Err(err) => {
302 tracing::warn!(?err, "failed to subscribe to non-finalized state changes");
303 tokio::time::sleep(POLL_DELAY).await;
304 None
305 }
306 };
307
308 continue;
309 };
310
311 let message = match tokio::time::timeout(
312 STREAM_MESSAGE_TIMEOUT,
313 non_finalized_state_change.message(),
314 )
315 .await
316 {
317 Ok(Ok(Some(block_and_hash))) => block_and_hash,
318 Ok(Ok(None)) => {
319 tracing::warn!("non-finalized state change stream ended unexpectedly");
320 non_finalized_blocks_listener = None;
321 tokio::time::sleep(STREAM_RESTART_DELAY).await;
322 continue;
323 }
324 Ok(Err(err)) => {
325 tracing::warn!(?err, "error receiving non-finalized state change");
326 non_finalized_blocks_listener = None;
327 tokio::time::sleep(STREAM_RESTART_DELAY).await;
328 continue;
329 }
330 Err(_) => {
331 tracing::debug!("non-finalized state change stream timed out, re-subscribing");
332 non_finalized_blocks_listener = None;
333 continue;
334 }
335 };
336
337 let Some((block, hash)) = message.decode() else {
338 tracing::warn!("received malformed non-finalized state change message");
339 non_finalized_blocks_listener = None;
340 tokio::time::sleep(STREAM_RESTART_DELAY).await;
341 continue;
342 };
343
344 // We have a parseable block from the stream, so take over the finalized tip: stop
345 // `update_finalized_chain_tip` so it no longer catches up the shared secondary db
346 // concurrently with our commits below.
347 if !signalled_started {
348 self.started_sync_sender.send_replace(true);
349 signalled_started = true;
350 }
351
352 if self.non_finalized_state.any_chain_contains(&hash) {
353 // Expected and harmless: on a resumed or multi-chain stream the server can re-send a
354 // block the syncer already has (e.g. a fork's shared ancestors), so this is logged
355 // at debug rather than warn to avoid noise.
356 tracing::debug!(
357 ?hash,
358 "non-finalized state already contains block, skipping"
359 );
360 continue;
361 }
362
363 // Skip blocks that are already finalized on the secondary. The server streams its
364 // entire non-finalized chain on initial subscription, which can start below our
365 // secondary's finalized tip when the secondary has caught up past the server's
366 // non-finalized root. Trying to commit those blocks fails with NotReadyToBeCommitted
367 // and triggers an endless re-subscribe loop.
368 if let Some(height) = block.coinbase_height() {
369 if self
370 .db
371 .finalized_tip_height()
372 .is_some_and(|tip| height <= tip)
373 {
374 tracing::debug!(?height, "skipping block already finalized on secondary");
375 continue;
376 }
377 }
378
379 // Stamp the receipt time so equal-work ties resolve like the source node: the
380 // listener streams the best chain first, so arrival order mirrors its preference.
381 let mut block = SemanticallyVerifiedBlock::with_hash(Arc::new(block), hash);
382 block.received_time = Some(Instant::now());
383 match self.try_commit(block.clone()).await {
384 Ok(()) => {
385 last_failed_commit_hash = None;
386 }
387 Err(error) => {
388 // Only log on transitions to avoid saturating the logs when the same block
389 // persistently fails to commit (e.g. when the secondary finalized state hasn't
390 // caught up with the primary yet).
391 if last_failed_commit_hash != Some(hash) {
392 tracing::warn!(
393 ?error,
394 ?hash,
395 "failed to commit block to non-finalized state"
396 );
397 last_failed_commit_hash = Some(hash);
398 }
399
400 non_finalized_blocks_listener = None;
401
402 // Back off before re-subscribing so a persistently failing block doesn't turn
403 // re-subscription into a full-speed busy loop.
404 tokio::time::sleep(COMMIT_RETRY_DELAY).await;
405 }
406 };
407 }
408 }
409
410 async fn try_commit(
411 &mut self,
412 block: SemanticallyVerifiedBlock,
413 ) -> Result<(), ValidateContextError> {
414 self.try_catch_up_with_primary().await;
415
416 // When the non-finalized state is empty and the incoming block doesn't build directly on
417 // the secondary's finalized tip, the secondary's finalized state is lagging the primary's.
418 // The streamed non-finalized blocks start at the primary's finalized tip, which can be
419 // several blocks above ours, so the incoming block has no parent to attach to. Bridge that
420 // gap by fetching the missing (already-finalized) blocks from the primary, so the incoming
421 // block has a contiguous chain to commit onto.
422 if self.non_finalized_state.best_chain().is_none()
423 && self.db.finalized_tip_hash() != block.block.header.previous_block_hash
424 {
425 self.fill_finalized_gap(block.height).await;
426 }
427
428 self.commit(block)
429 }
430
431 /// Commits `block` to the non-finalized state, starting a new chain if it builds on the
432 /// finalized tip or extending an existing chain otherwise, prunes newly-finalized blocks, and
433 /// publishes the updated chain tip and non-finalized state.
434 ///
435 /// Updating the channels here (rather than only after `try_commit` returns) means the bridge
436 /// blocks committed by [`Self::fill_finalized_gap`] also advance the published chain tip.
437 fn commit(&mut self, block: SemanticallyVerifiedBlock) -> Result<(), ValidateContextError> {
438 if self.db.finalized_tip_hash() == block.block.header.previous_block_hash {
439 self.prune_finalized();
440 self.non_finalized_state.commit_new_chain(block, &self.db)?;
441 } else {
442 self.non_finalized_state.commit_block(block, &self.db)?;
443 self.prune_finalized();
444 }
445
446 self.update_channels();
447
448 Ok(())
449 }
450
451 /// Fetches the blocks between the secondary's finalized tip and `target_height` (exclusive)
452 /// from the primary and commits them to the non-finalized state.
453 ///
454 /// These blocks are finalized on the primary but not yet on the lagging secondary. Committing
455 /// them as non-finalized bridge blocks gives the block at `target_height` a contiguous chain to
456 /// attach to; they are dropped again by [`Self::prune_finalized`] as the secondary catches up.
457 ///
458 /// This is best-effort: if a block can't be fetched or committed, it returns early and lets the
459 /// caller's commit fail so the block is retried on the next subscription.
460 async fn fill_finalized_gap(&mut self, target_height: Height) {
461 loop {
462 // Try to advance the secondary's finalized state first: if it catches up to the gap on
463 // its own, we can stop fetching blocks. This also drops any bridge blocks the secondary
464 // has since finalized, so the next height is recomputed from where we actually are.
465 self.try_catch_up_with_primary().await;
466 self.prune_finalized();
467
468 // The next height to bridge is just above the highest block we already have: the
469 // non-finalized tip if we've committed bridge blocks, otherwise the finalized tip.
470 let Some(highest) = self
471 .non_finalized_state
472 .best_tip()
473 .map(|(height, _hash)| height)
474 .max(self.db.finalized_tip_height())
475 else {
476 return;
477 };
478
479 let Ok(next_height) = highest.next() else {
480 return;
481 };
482
483 // Stop once the chain reaches the streamed block's parent, whether by the finalized
484 // state catching up or by the bridge blocks we fetched.
485 if next_height >= target_height {
486 return;
487 }
488
489 let (block, hash) = match self.get_block(next_height.into()).await {
490 Ok(block_and_hash) => block_and_hash,
491 Err(error) => {
492 tracing::warn!(
493 ?error,
494 ?next_height,
495 "failed to fetch a block while bridging the finalized gap; \
496 will retry on the next subscription"
497 );
498 return;
499 }
500 };
501
502 let mut block = SemanticallyVerifiedBlock::with_hash(Arc::new(block), hash);
503 block.received_time = Some(Instant::now());
504 if let Err(error) = self.commit(block) {
505 tracing::warn!(
506 ?error,
507 ?next_height,
508 "failed to commit a block while bridging the finalized gap; \
509 will retry on the next subscription"
510 );
511 return;
512 }
513 }
514 }
515
516 /// Fetches a single block from the primary by hash or height via the indexer gRPC.
517 async fn get_block(
518 &self,
519 hash_or_height: HashOrHeight,
520 ) -> Result<(Block, block::Hash), Status> {
521 // Encode the request as a single `hash_or_height` byte string: a 32-byte hash in display
522 // order, or a 4-byte big-endian height. The server tells them apart by length.
523 let hash_or_height = match hash_or_height {
524 HashOrHeight::Hash(hash) => hash.bytes_in_display_order().to_vec(),
525 HashOrHeight::Height(height) => height.0.to_be_bytes().to_vec(),
526 };
527 let request = BlockRequest { hash_or_height };
528
529 let response = tokio::time::timeout(
530 GET_BLOCK_TIMEOUT,
531 self.indexer_rpc_client.clone().get_block(request),
532 )
533 .await
534 .map_err(|_| Status::deadline_exceeded("get_block request timed out"))??;
535
536 response
537 .into_inner()
538 .decode()
539 .ok_or_else(|| Status::internal("failed to decode block from get_block response"))
540 }
541
542 /// Calls the `non_finalized_state_change()` method on the indexer gRPC client to subscribe
543 /// to non-finalized state changes, and returns the response stream.
544 ///
545 /// Passes the tip hashes of every chain currently in this syncer's non-finalized state so the
546 /// server only streams blocks after the tips we already have, instead of re-sending the whole
547 /// non-finalized state on every (re)subscription. When the non-finalized state is empty, the
548 /// request carries no tips and the server streams every non-finalized block.
549 async fn subscribe_to_non_finalized_state_change(
550 &mut self,
551 ) -> Result<Streaming<BlockAndHash>, Status> {
552 let request = NonFinalizedStateChangeRequest {
553 chain_tip_hashes: self
554 .non_finalized_state
555 .chain_iter()
556 .map(|c| c.non_finalized_tip_hash().bytes_in_display_order().to_vec())
557 .collect(),
558 };
559
560 tokio::time::timeout(
561 SUBSCRIBE_TIMEOUT,
562 self.indexer_rpc_client
563 .clone()
564 .non_finalized_state_change(request),
565 )
566 .await
567 .map_err(|_| {
568 Status::deadline_exceeded("non_finalized_state_change subscription timed out")
569 })?
570 .map(|a| a.into_inner())
571 }
572
573 /// Tries to catch up to the primary db instance for an up-to-date view of finalized blocks.
574 async fn try_catch_up_with_primary(&self) {
575 let _ = self.db.spawn_try_catch_up_with_primary().await;
576 }
577
578 /// Finalizes any non-finalized blocks that are at or below the finalized tip height.
579 ///
580 /// The secondary's finalized state follows the primary, so after catching up it may have
581 /// advanced past the root of the non-finalized state. This drops those now-finalized blocks so
582 /// the non-finalized state only tracks blocks above the finalized tip. Does nothing when the
583 /// non-finalized state is empty.
584 fn prune_finalized(&mut self) {
585 let finalized_tip_height = self.db.finalized_tip_height().unwrap_or(Height::MIN);
586 while self
587 .non_finalized_state
588 .root_height()
589 .is_some_and(|root_height| root_height <= finalized_tip_height)
590 {
591 tracing::trace!("finalizing block past the reorg limit");
592 self.non_finalized_state.finalize();
593 }
594 }
595
596 /// Sends the new chain tip and non-finalized state to the latest chain channels.
597 // TODO: Replace this with the `update_latest_chain_channels()` fn in `write.rs`.
598 fn update_channels(&mut self) {
599 // If the final receiver was just dropped, ignore the error.
600 let _ = self
601 .non_finalized_state_sender
602 .send(self.non_finalized_state.clone());
603
604 let best_chain = self.non_finalized_state.best_chain().expect("unexpected empty non-finalized state: must commit at least one block before updating channels");
605
606 let tip_block = best_chain
607 .tip_block()
608 .expect(
609 "unexpected empty chain: must commit at least one block before updating channels",
610 )
611 .clone();
612
613 self.chain_tip_sender
614 .set_best_non_finalized_tip(Some(tip_block.into()));
615 }
616}
617
618/// Accepts a [zebra-state configuration](zebra_state::Config), a [`Network`], and
619/// the [`SocketAddr`] of a Zebra node's RPC server.
620///
621/// Initializes a [`ReadStateService`] and a [`TrustedChainSync`] to update the
622/// non-finalized best chain and the latest chain tip.
623///
624/// Returns a [`ReadStateService`], [`LatestChainTip`], [`ChainTipChange`], and
625/// a [`JoinHandle`] for the sync task.
626pub fn init_read_state_with_syncer(
627 config: zebra_state::Config,
628 network: &Network,
629 indexer_rpc_address: SocketAddr,
630) -> tokio::task::JoinHandle<
631 Result<
632 (
633 ReadStateService,
634 LatestChainTip,
635 ChainTipChange,
636 tokio::task::JoinHandle<()>,
637 ),
638 BoxError,
639 >,
640> {
641 let network = network.clone();
642 tokio::spawn(async move {
643 if config.ephemeral {
644 return Err("standalone read state service cannot be used with ephemeral state".into());
645 }
646
647 // The outer `?` propagates a `JoinError` if the blocking task panicked or was
648 // cancelled, and the inner `?` propagates a `StateInitError` (e.g. a missing
649 // read-only database).
650 let (read_state, db, non_finalized_state_sender) =
651 spawn_init_read_only(config, &network).await??;
652 let (latest_chain_tip, chain_tip_change, sync_task) =
653 TrustedChainSync::spawn(indexer_rpc_address, db, non_finalized_state_sender).await?;
654 Ok((read_state, latest_chain_tip, chain_tip_change, sync_task))
655 })
656}