1use crate::{
2 config::CompressionConfig,
3 errors::CompressionError,
4 metrics::CompressionMetricsManager,
5 ports::{
6 block_source::{
7 BlockAt,
8 BlockSource,
9 BlockWithMetadataExt,
10 },
11 canonical_height::CanonicalHeight,
12 compression_storage::{
13 CompressionStorage,
14 LatestHeight,
15 WriteCompressedBlock,
16 },
17 configuration::CompressionConfigProvider,
18 },
19 sync_state::{
20 SyncStateNotifier,
21 SyncStateObserver,
22 new_sync_state_channel,
23 },
24 temporal_registry::CompressionContext,
25};
26use fuel_core_compression::compress::compress;
27use fuel_core_services::{
28 RunnableService,
29 RunnableTask,
30 ServiceRunner,
31 StateWatcher,
32 TaskNextAction,
33 try_or_continue,
34};
35use fuel_core_storage::transactional::WriteTransaction;
36use fuel_core_types::{
37 blockchain::block::Block,
38 fuel_types::ChainId,
39};
40use futures::{
41 FutureExt,
42 StreamExt,
43};
44use std::time::Instant;
45
46pub struct UninitializedCompressionService<B, S, CH> {
48 block_source: B,
50 canonical_height: CH,
52 storage: S,
54 config: CompressionConfig,
56 sync_notifier: SyncStateNotifier,
58 metrics_manager: Option<CompressionMetricsManager>,
60}
61
62impl<B, S, CH> UninitializedCompressionService<B, S, CH> {
63 fn new(
64 block_source: B,
65 storage: S,
66 config: CompressionConfig,
67 canonical_height: CH,
68 ) -> Self {
69 let (sync_notifier, _) = new_sync_state_channel();
70
71 let metrics_manager = if config.metrics() {
72 Some(CompressionMetricsManager::new())
73 } else {
74 None
75 };
76
77 Self {
78 block_source,
79 canonical_height,
80 storage,
81 config,
82 sync_notifier,
83 metrics_manager,
84 }
85 }
86}
87
88pub struct CompressionService<S> {
94 block_stream: crate::ports::block_source::BlockStream,
96 storage: S,
98 config: CompressionConfig,
100 sync_notifier: SyncStateNotifier,
102 metrics_manager: Option<CompressionMetricsManager>,
104}
105
106impl<S> CompressionService<S>
107where
108 S: CompressionStorage,
109{
110 fn new(
111 block_stream: crate::ports::block_source::BlockStream,
112 storage: S,
113 config: CompressionConfig,
114 sync_notifier: SyncStateNotifier,
115 metrics_manager: Option<CompressionMetricsManager>,
116 ) -> Self {
117 Self {
118 block_stream,
119 storage,
120 config,
121 sync_notifier,
122 metrics_manager,
123 }
124 }
125}
126
127fn compress_block<S>(
129 storage: &mut S,
130 block: &Block,
131 config: &CompressionConfig,
132) -> crate::Result<usize>
133where
134 S: CompressionStorage,
135{
136 let mut storage_tx = storage.write_transaction();
137
138 let chain_id = config.chain_id();
140 let compression_context =
141 CompressionContext::create_from_block(&mut storage_tx, block, chain_id)
142 .map_err(CompressionError::FailedToCompressBlock)?;
143 let compressed_block = compress(&config.into(), compression_context, block)
144 .now_or_never()
145 .expect("The current implementation should resolve all futures instantly")
146 .map_err(crate::errors::CompressionError::FailedToCompressBlock)?;
147
148 let size_of_compressed_block =
149 storage_tx.write_compressed_block(block.header().height(), &compressed_block)?;
150
151 storage_tx
152 .commit()
153 .map_err(crate::errors::CompressionError::FailedToCommitTransaction)?;
154
155 Ok(size_of_compressed_block)
156}
157
158fn handle_new_block<S>(
159 storage: &mut S,
160 block: &Block,
161 config: &CompressionConfig,
162 sync_notifier: &SyncStateNotifier,
163 metrics_manager: &Option<CompressionMetricsManager>,
164) -> crate::Result<()>
165where
166 S: CompressionStorage,
167{
168 sync_notifier
170 .send(crate::sync_state::SyncState::NotSynced)
171 .ok();
172 let height = (*block.header().height()).into();
173 if let Some(metrics_manager) = metrics_manager {
175 let (compressed_block_size, compression_duration) = {
176 let start = Instant::now();
177 let compressed_block_size = compress_block(storage, block, config)?;
178 (compressed_block_size, start.elapsed().as_secs_f64())
179 };
180
181 metrics_manager.record_compression_duration_ms(compression_duration);
182 metrics_manager.record_compressed_block_size(compressed_block_size);
183 metrics_manager.record_compression_block_height(height);
184 } else {
185 compress_block(storage, block, config)?;
186 }
187 sync_notifier
189 .send(crate::sync_state::SyncState::Synced(height))
190 .ok();
191
192 Ok(())
193}
194
195impl<S> CompressionService<S>
196where
197 S: CompressionStorage,
198{
199 fn handle_new_block(&mut self, block: &Block) -> crate::Result<()> {
200 handle_new_block(
201 &mut self.storage,
202 block,
203 &self.config,
204 &self.sync_notifier,
205 &self.metrics_manager,
206 )
207 }
208}
209
210#[derive(Debug)]
211enum SyncHeight {
212 StorageHeight(u32),
213 ConfiguredHeight(u32),
214 Genesis,
215}
216
217impl SyncHeight {
218 #[inline]
219 const fn value(&self) -> u32 {
220 match self {
221 SyncHeight::StorageHeight(height) => *height,
222 SyncHeight::ConfiguredHeight(height) => *height,
223 SyncHeight::Genesis => 0,
224 }
225 }
226
227 #[inline]
228 const fn is_from_storage(&self) -> bool {
229 matches!(self, SyncHeight::StorageHeight(_))
230 }
231}
232
233impl<B, S, CH> UninitializedCompressionService<B, S, CH>
234where
235 B: BlockSource,
236 S: CompressionStorage + LatestHeight,
237 CH: CanonicalHeight,
238{
239 async fn sync_previously_produced_blocks(
240 &mut self,
241 state_watcher: &StateWatcher,
242 ) -> crate::Result<()> {
243 loop {
244 let state = state_watcher.borrow();
246 if !state.starting() {
247 break;
248 }
249
250 let canonical_height = match self.canonical_height.get() {
251 Some(height) => height,
252 None => {
253 break;
256 }
257 };
258
259 let maybe_height =
260 match (self.storage.latest_height(), self.config.starting_height()) {
261 (Some(height), _) => SyncHeight::StorageHeight(height),
262 (None, Some(height)) => SyncHeight::ConfiguredHeight(height),
263 (None, None) => SyncHeight::Genesis,
264 };
265
266 if canonical_height < maybe_height.value() {
267 tracing::error!(
268 "Canonical height is less than fetched height: Canonical height: {:?}, Fetched height: {:?}",
269 &canonical_height,
270 &maybe_height
271 );
272 return Err(crate::errors::CompressionError::FailedToGetSyncStatus);
273 }
274
275 if canonical_height == maybe_height.value() && maybe_height.is_from_storage()
276 {
277 tracing::info!("Compression database is up to date");
278 break;
279 }
280
281 let height_to_sync = match maybe_height {
282 SyncHeight::Genesis => BlockAt::Genesis,
283 SyncHeight::StorageHeight(height) => {
284 BlockAt::Specific(height.saturating_add(1))
285 }
286 SyncHeight::ConfiguredHeight(height) => BlockAt::Specific(height),
287 };
288
289 let block = self.block_source.get_block(height_to_sync).map_err(|err| {
290 CompressionError::FailedToGetBlock(format!(
291 "during synchronization of canonical chain at height: {:?}: {}",
292 height_to_sync, err
293 ))
294 })?;
295
296 handle_new_block(
297 &mut self.storage,
298 &block,
299 &self.config,
300 &self.sync_notifier,
301 &self.metrics_manager,
302 )?;
303 }
304
305 Ok(())
306 }
307}
308
309#[derive(Debug, Clone)]
311pub struct SharedData {
312 sync_observer: SyncStateObserver,
314}
315
316impl SharedData {
317 pub async fn await_synced_until(&self, block_height: &u32) -> crate::Result<()> {
320 let mut observer = self.sync_observer.clone();
321 loop {
322 if observer.borrow_and_update().is_synced_until(block_height) {
323 break;
324 }
325
326 observer
327 .changed()
328 .await
329 .map_err(|_| crate::errors::CompressionError::FailedToGetSyncStatus)?;
330 }
331
332 Ok(())
333 }
334}
335
336#[async_trait::async_trait]
337impl<B, S, CH> RunnableService for UninitializedCompressionService<B, S, CH>
338where
339 B: BlockSource,
340 S: CompressionStorage + LatestHeight,
341 CH: CanonicalHeight,
342{
343 const NAME: &'static str = "CompressionService";
344 type Task = CompressionService<S>;
345 type SharedData = SharedData;
346 type TaskParams = ();
347
348 fn shared_data(&self) -> Self::SharedData {
349 SharedData {
350 sync_observer: self.sync_notifier.subscribe(),
351 }
352 }
353
354 async fn into_task(
355 mut self,
356 state_watcher: &StateWatcher,
357 _params: Self::TaskParams,
358 ) -> anyhow::Result<Self::Task> {
359 self.sync_previously_produced_blocks(state_watcher).await?;
360
361 let compression_service = CompressionService::new(
362 self.block_source.subscribe(),
363 self.storage,
364 self.config,
365 self.sync_notifier,
366 self.metrics_manager,
367 );
368
369 Ok(compression_service)
370 }
371}
372
373impl<S> RunnableTask for CompressionService<S>
374where
375 S: CompressionStorage,
376{
377 async fn run(&mut self, watcher: &mut StateWatcher) -> TaskNextAction {
378 tokio::select! {
379 biased;
380
381 _ = watcher.while_started() => {
382 TaskNextAction::Stop
383 }
384
385 block_res = self.block_stream.next() => {
386 match block_res {
387 None => {
388 tracing::warn!("No blocks available");
389 TaskNextAction::Stop
390 }
391 Some(block_with_metadata) => {
392 tracing::debug!("Got new block: {:?}", &block_with_metadata.height());
393 try_or_continue!(
394 self.handle_new_block(block_with_metadata.block()),
395 |e| tracing::error!("Error handling new block: {:?}", e)
396 );
397 TaskNextAction::Continue
398 }
399 }
400 }
401 }
402 }
403
404 async fn shutdown(mut self) -> anyhow::Result<()> {
405 if let Some(Some(block_with_metadata)) = self.block_stream.next().now_or_never()
407 && let Err(e) = self.handle_new_block(block_with_metadata.block())
408 {
409 return Err(anyhow::anyhow!(e).context(format!(
410 "Couldn't compress block: {}. Shutting down. \
411 Node will be in indeterminate state upon restart. \
412 Suggested to delete compression database.",
413 block_with_metadata.height()
414 )));
415 }
416 Ok(())
417 }
418}
419
420pub fn new_service<B, S, C, CH>(
422 block_source: B,
423 storage: S,
424 config_provider: C,
425 canonical_height: CH,
426 chain_id: ChainId,
427) -> crate::Result<ServiceRunner<UninitializedCompressionService<B, S, CH>>>
428where
429 B: BlockSource,
430 S: CompressionStorage + LatestHeight,
431 C: CompressionConfigProvider,
432 CH: CanonicalHeight,
433{
434 let config = config_provider.config(chain_id);
435 Ok(ServiceRunner::new(UninitializedCompressionService::new(
436 block_source,
437 storage,
438 config,
439 canonical_height,
440 )))
441}
442
443#[allow(non_snake_case)]
444#[cfg(test)]
445mod tests {
446 use std::num::NonZeroU32;
447
448 use super::*;
449 use crate::{
450 ports::block_source::{
451 BlockWithMetadata,
452 BlockWithMetadataExt,
453 },
454 storage,
455 storage::CompressedBlocks,
456 };
457 use fuel_core_services::{
458 Service,
459 stream::{
460 BoxStream,
461 IntoBoxStream,
462 },
463 };
464 use fuel_core_storage::{
465 StorageAsRef,
466 iter::{
467 IterDirection,
468 IteratorOverTable,
469 changes_iterator::ChangesIterator,
470 },
471 merkle::column::MerkleizedColumn,
472 structured_storage::test::InMemoryStorage,
473 transactional::{
474 IntoTransaction,
475 StorageChanges,
476 StorageTransaction,
477 },
478 };
479
480 struct EmptyBlockSource;
481
482 impl BlockSource for EmptyBlockSource {
483 fn subscribe(&self) -> BoxStream<BlockWithMetadata> {
484 tokio_stream::pending().into_boxed()
485 }
486
487 fn get_block(
488 &self,
489 _: crate::ports::block_source::BlockAt,
490 ) -> anyhow::Result<Block> {
491 anyhow::bail!("Block not found")
492 }
493 }
494
495 type MockStorage = StorageTransaction<
496 InMemoryStorage<MerkleizedColumn<crate::storage::column::CompressionColumn>>,
497 >;
498
499 fn test_storage() -> MockStorage {
500 InMemoryStorage::default().into_transaction()
501 }
502
503 impl LatestHeight for MockStorage {
504 fn latest_height(&self) -> Option<u32> {
505 let changes = StorageChanges::Changes(self.changes().clone());
506 let view = ChangesIterator::new(&changes);
507 let compressed_block_changes = view
508 .iter_all_keys::<CompressedBlocks>(Some(IterDirection::Reverse))
509 .next()
510 .transpose()
511 .unwrap();
512 compressed_block_changes.map(Into::into)
513 }
514 }
515
516 struct MockConfigProvider(crate::config::CompressionConfig);
517
518 impl Default for MockConfigProvider {
519 fn default() -> Self {
520 let chain_id = ChainId::default();
521 Self(CompressionConfig::new(
522 std::time::Duration::from_secs(10),
523 None,
524 false,
525 chain_id,
526 ))
527 }
528 }
529
530 impl MockConfigProvider {
531 fn new(config: crate::config::CompressionConfig) -> Self {
532 Self(config)
533 }
534 }
535
536 impl CompressionConfigProvider for MockConfigProvider {
537 fn config(&self, _: ChainId) -> crate::config::CompressionConfig {
538 self.0
539 }
540 }
541
542 #[derive(Default, Clone)]
543 struct MockCanonicalHeightProvider(u32);
544
545 impl MockCanonicalHeightProvider {
546 fn new(height: u32) -> Self {
547 Self(height)
548 }
549 }
550
551 impl CanonicalHeight for MockCanonicalHeightProvider {
552 fn get(&self) -> Option<u32> {
553 match self.0 {
554 0 => None,
555 _ => Some(self.0),
556 }
557 }
558 }
559
560 #[tokio::test]
561 async fn compression_service__can_be_started_and_stopped() {
562 let chain_id = ChainId::default();
564 let block_source = EmptyBlockSource;
565 let storage = test_storage();
566 let config_provider = MockConfigProvider::default();
567 let canonical_height_provider = MockCanonicalHeightProvider::default();
568
569 let service = new_service(
570 block_source,
571 storage,
572 config_provider,
573 canonical_height_provider,
574 chain_id,
575 )
576 .unwrap();
577
578 service.start_and_await().await.unwrap();
580
581 service.stop_and_await().await.unwrap();
584 }
585
586 struct MockBlockSource(Vec<BlockWithMetadata>);
587
588 impl MockBlockSource {
589 fn new(blocks: Vec<BlockWithMetadata>) -> Self {
590 Self(blocks)
591 }
592 }
593
594 impl BlockSource for MockBlockSource {
595 fn subscribe(&self) -> BoxStream<BlockWithMetadata> {
596 tokio_stream::iter(self.0.clone()).into_boxed()
597 }
598
599 fn get_block(&self, height: BlockAt) -> anyhow::Result<Block> {
600 let block_height = match height {
601 BlockAt::Specific(h) => h,
602 BlockAt::Genesis => 0,
604 };
605 self.0
606 .iter()
607 .find(|block| block.height() == &block_height)
608 .map(|block| block.block().clone())
609 .ok_or_else(|| anyhow::anyhow!("Block not found"))
610 }
611 }
612
613 #[tokio::test]
614 async fn compression_service__run__does_not_compress_blocks_if_no_blocks_provided() {
615 let chain_id = ChainId::default();
618 let block_source = MockBlockSource::new(vec![]);
619 let storage = test_storage();
620 let config_provider = MockConfigProvider::default();
621 let (sync_notifier, _) = new_sync_state_channel();
622
623 let mut service = CompressionService::new(
624 block_source.subscribe(),
625 storage,
626 config_provider.config(chain_id),
627 sync_notifier,
628 None,
629 );
630
631 let _ = service.run(&mut StateWatcher::started()).await;
633
634 let maybe_block = service
636 .storage
637 .storage_as_ref::<storage::CompressedBlocks>()
638 .get(&0.into())
639 .unwrap();
640 assert!(maybe_block.is_none());
641 service.shutdown().await.unwrap();
642 }
643
644 #[tokio::test]
645 async fn compression_service__run__compresses_blocks() {
646 let chain_id = ChainId::default();
649 let block_with_metadata = BlockWithMetadata::default();
650 let block_source = MockBlockSource::new(vec![block_with_metadata]);
651 let storage = test_storage();
652 let config_provider = MockConfigProvider::default();
653 let canonical_height_provider = MockCanonicalHeightProvider::default();
654
655 let uninit_service = UninitializedCompressionService::new(
656 block_source,
657 storage,
658 config_provider.config(chain_id),
659 canonical_height_provider,
660 );
661 let sync_observer = uninit_service.shared_data();
662
663 let mut service = uninit_service
665 .into_task(&Default::default(), ())
666 .await
667 .unwrap();
668 let _ = service.run(&mut StateWatcher::started()).await;
669
670 let target_block_height = 0;
672 sync_observer
673 .await_synced_until(&target_block_height)
674 .await
675 .unwrap();
676 let maybe_block = service
677 .storage
678 .storage_as_ref::<storage::CompressedBlocks>()
679 .get(&target_block_height.into())
680 .unwrap();
681 assert!(maybe_block.is_some());
682 service.shutdown().await.unwrap();
683 }
684
685 #[tokio::test]
686 async fn compression_service__syncs_from_scratch_when_database_is_empty() {
687 let block_count = 10;
689 let chain_id = ChainId::default();
690 let mut blocks = Vec::with_capacity(block_count);
691 for i in 0..u32::try_from(block_count).unwrap() {
692 blocks.push(BlockWithMetadata::test_block_with_height(i));
693 }
694 let block_source = MockBlockSource::new(blocks);
695 let storage = test_storage();
696 let config_provider = MockConfigProvider::default();
697 let canonical_height_provider = MockCanonicalHeightProvider::new(5);
698
699 let uninit_service = UninitializedCompressionService::new(
700 block_source,
701 storage,
702 config_provider.config(chain_id),
703 canonical_height_provider.clone(),
704 );
705
706 let service = uninit_service
708 .into_task(&StateWatcher::starting(), ())
709 .await
710 .unwrap();
711
712 let maybe_block = service
714 .storage
715 .storage_as_ref::<storage::CompressedBlocks>()
716 .get(&canonical_height_provider.get().unwrap().into())
717 .unwrap();
718 assert!(maybe_block.is_some());
719 service.shutdown().await.unwrap();
720 }
721
722 #[tokio::test]
723 async fn compression_service__syncs_from_overridden_starting_height_when_provided() {
724 let block_count = 10;
727 let chain_id = ChainId::default();
728 let override_starting_height = 1;
729 let mut blocks = Vec::with_capacity(block_count);
730 for i in 0..u32::try_from(block_count).unwrap() {
731 blocks.push(BlockWithMetadata::test_block_with_height(i));
732 }
733 let block_source = MockBlockSource::new(blocks);
734 let storage = test_storage();
735 let config_provider =
736 MockConfigProvider::new(crate::config::CompressionConfig::new(
737 std::time::Duration::from_secs(10),
738 Some(NonZeroU32::new(override_starting_height).unwrap()),
739 false,
740 chain_id,
741 ));
742 let canonical_height_provider = MockCanonicalHeightProvider::new(5);
743
744 let uninit_service = UninitializedCompressionService::new(
745 block_source,
746 storage,
747 config_provider.config(chain_id),
748 canonical_height_provider.clone(),
749 );
750
751 let service = uninit_service
753 .into_task(&StateWatcher::starting(), ())
754 .await
755 .unwrap();
756
757 let maybe_block = service
759 .storage
760 .storage_as_ref::<storage::CompressedBlocks>()
761 .get(&override_starting_height.into())
762 .unwrap();
763 assert!(maybe_block.is_some());
764 service.shutdown().await.unwrap();
765 }
766
767 #[cfg(feature = "fault-proving")]
768 #[tokio::test]
769 async fn compression_service__writes_to_registrations_table() {
770 let block_with_metadata = BlockWithMetadata::default();
772 let block_source = MockBlockSource::new(vec![block_with_metadata]);
773 let storage = test_storage();
774 let config_provider = MockConfigProvider::default();
775 let canonical_height_provider = MockCanonicalHeightProvider::default();
776 let chain_id = ChainId::default();
777
778 let uninit_service = UninitializedCompressionService::new(
779 block_source,
780 storage,
781 config_provider.config(chain_id),
782 canonical_height_provider,
783 );
784 let sync_observer = uninit_service.shared_data();
785
786 let mut service = uninit_service
788 .into_task(&Default::default(), ())
789 .await
790 .unwrap();
791 let _ = service.run(&mut StateWatcher::started()).await;
792
793 let target_block_height = 0;
795 sync_observer
796 .await_synced_until(&target_block_height)
797 .await
798 .unwrap();
799 let maybe_registrations = service
800 .storage
801 .storage_as_ref::<storage::registrations::Registrations>()
802 .get(&target_block_height.into())
803 .unwrap();
804 assert!(maybe_registrations.is_some());
805 }
806}