1#![allow(dead_code)]
6
7#[cfg(test)]
8mod tests;
9
10pub mod progress;
11pub mod reader;
12pub mod restore;
13pub mod uploader;
14mod writer;
15
16use std::{
17 collections::HashSet,
18 num::NonZeroUsize,
19 path::PathBuf,
20 sync::{
21 Arc,
22 atomic::{AtomicU64, Ordering},
23 },
24};
25
26use anyhow::Result;
27use fastcrypto::hash::MultisetHash;
28use indicatif::{MultiProgress, ProgressBar, ProgressStyle};
29use iota_core::{
30 authority::{
31 authority_store_tables::{AuthorityPerpetualTables, LiveObject},
32 epoch_start_configuration::{EpochFlag, EpochStartConfiguration},
33 },
34 checkpoints::CheckpointStore,
35 epoch::committee_store::CommitteeStore,
36 global_state_hasher::GlobalStateHasher,
37};
38use iota_sdk_types::{ObjectId, ObjectReference};
39use iota_storage::{
40 FileCompression, SHA3_BYTES, compute_sha3_checksum, object_store::util::path_to_filesystem,
41};
42use iota_types::{
43 IOTA_SYSTEM_STATE_OBJECT_ID,
44 base_types::ExecutionDigests,
45 committee::{Committee, CommitteeChainVerifier, EpochId},
46 digests::ChainIdentifier,
47 effects::{TransactionEffectsAPI, TransactionEffectsExt},
48 global_state_hash::GlobalStateHash,
49 iota_system_state::{
50 IotaSystemState, IotaSystemStateTrait,
51 epoch_start_iota_system_state::EpochStartSystemStateTrait, get_iota_system_state,
52 },
53 messages_checkpoint::{CheckpointSequenceNumber, ECMHLiveObjectSetDigest},
54 object::Object,
55 storage::{EpochInfoV1Entry, EpochInfoV2},
56};
57use num_enum::{IntoPrimitive, TryFromPrimitive};
58use object_store::path::Path;
59use serde::{Deserialize, Serialize};
60
61use crate::{
62 progress::{ProgressTicker, ProgressUnit},
63 restore::RestoreEpochInfo,
64};
65
66const OBJECT_FILE_MAGIC: u32 = 0x00B7EC76;
171const REFERENCE_FILE_MAGIC: u32 = 0xDEADBEEF;
172const EPOCH_INFO_FILE_MAGIC: u32 = 0x9000C001;
173const MANIFEST_FILE_MAGIC: u32 = 0x00C0FFEE;
174const MAGIC_BYTES: usize = 4;
175const SNAPSHOT_VERSION_BYTES: usize = 1;
176const ADDRESS_LENGTH_BYTES: usize = 8;
177const PADDING_BYTES: usize = 3;
178const MANIFEST_FILE_HEADER_BYTES: usize =
179 MAGIC_BYTES + SNAPSHOT_VERSION_BYTES + ADDRESS_LENGTH_BYTES + PADDING_BYTES;
180const FILE_MAX_BYTES: usize = 128 * 1024 * 1024;
181const OBJECT_ID_BYTES: usize = ObjectId::LENGTH;
182const SEQUENCE_NUM_BYTES: usize = 8;
183const OBJECT_DIGEST_BYTES: usize = 32;
184const OBJECT_REF_BYTES: usize = OBJECT_ID_BYTES + SEQUENCE_NUM_BYTES + OBJECT_DIGEST_BYTES;
185const FILE_TYPE_BYTES: usize = 1;
186const BUCKET_BYTES: usize = 4;
187const BUCKET_PARTITION_BYTES: usize = 4;
188const COMPRESSION_TYPE_BYTES: usize = 1;
189const FILE_METADATA_BYTES: usize =
190 FILE_TYPE_BYTES + BUCKET_BYTES + BUCKET_PARTITION_BYTES + COMPRESSION_TYPE_BYTES + SHA3_BYTES;
191
192pub fn default_download_concurrency() -> NonZeroUsize {
193 const MAX_PARALLEL_DOWNLOADS: usize = 8;
194 let cores = std::thread::available_parallelism().map_or(1, NonZeroUsize::get);
195 NonZeroUsize::new(cores.saturating_sub(1).clamp(1, MAX_PARALLEL_DOWNLOADS))
196 .unwrap_or(NonZeroUsize::MIN)
197}
198
199#[derive(
200 Copy, Clone, Debug, Eq, PartialEq, Serialize, Deserialize, TryFromPrimitive, IntoPrimitive,
201)]
202#[repr(u8)]
203pub enum FileType {
204 Object = 0,
205 Reference = 1,
206 EpochInfo = 2,
208}
209
210#[derive(Debug, Clone, Serialize, Deserialize, Eq, PartialEq)]
211pub struct FileMetadata {
213 pub file_type: FileType,
214 pub bucket_num: u32,
215 pub part_num: u32,
216 pub file_compression: FileCompression,
217 pub sha3_digest: [u8; 32],
218}
219
220impl FileMetadata {
221 pub fn file_path(&self, dir_path: &Path) -> Path {
222 match self.file_type {
223 FileType::Object => {
224 dir_path.child(&*format!("{}_{}.obj", self.bucket_num, self.part_num))
225 }
226 FileType::Reference => {
227 dir_path.child(&*format!("{}_{}.ref", self.bucket_num, self.part_num))
228 }
229 FileType::EpochInfo => dir_path.child("EPOCH_INFO"),
232 }
233 }
234 pub fn local_file_path(&self, root_path: &std::path::Path, dir_path: &Path) -> Result<PathBuf> {
235 path_to_filesystem(root_path.to_path_buf(), &self.file_path(dir_path))
236 }
237}
238
239#[derive(Debug, Serialize, Deserialize, Eq, PartialEq)]
240pub struct ManifestV1 {
241 pub snapshot_version: u8,
242 pub address_length: u64,
243 pub file_metadata: Vec<FileMetadata>,
244 pub epoch: u64,
245}
246
247#[derive(Debug, Serialize, Deserialize, Eq, PartialEq)]
250pub struct ManifestV2 {
251 pub snapshot_version: u8,
252 pub address_length: u64,
253 pub file_metadata: Vec<FileMetadata>,
254 pub epoch: u64,
255 pub chain_id: ChainIdentifier,
256}
257
258#[derive(Debug, Serialize, Deserialize, Eq, PartialEq)]
259pub enum Manifest {
260 V1(ManifestV1),
261 V2(ManifestV2),
262}
263
264impl Manifest {
265 pub fn snapshot_version(&self) -> u8 {
266 match self {
267 Self::V1(manifest) => manifest.snapshot_version,
268 Self::V2(manifest) => manifest.snapshot_version,
269 }
270 }
271 pub fn address_length(&self) -> u64 {
272 match self {
273 Self::V1(manifest) => manifest.address_length,
274 Self::V2(manifest) => manifest.address_length,
275 }
276 }
277 pub fn file_metadata(&self) -> &Vec<FileMetadata> {
278 match self {
279 Self::V1(manifest) => &manifest.file_metadata,
280 Self::V2(manifest) => &manifest.file_metadata,
281 }
282 }
283 pub fn epoch(&self) -> u64 {
284 match self {
285 Self::V1(manifest) => manifest.epoch,
286 Self::V2(manifest) => manifest.epoch,
287 }
288 }
289 pub fn chain_id(&self) -> Option<ChainIdentifier> {
291 match self {
292 Self::V1(_) => None,
293 Self::V2(manifest) => Some(manifest.chain_id),
294 }
295 }
296}
297
298#[derive(Debug, Serialize, Deserialize)]
302pub enum EpochInfo {
303 V1(EpochInfoV1),
304}
305
306#[derive(Debug, Serialize, Deserialize)]
307pub struct EpochInfoV1 {
308 pub entries: Vec<EpochInfoV1Entry>,
309}
310
311impl EpochInfo {
312 pub fn entries(&self) -> &[EpochInfoV1Entry] {
313 match self {
314 Self::V1(info) => &info.entries,
315 }
316 }
317
318 pub fn into_entries(self) -> Vec<EpochInfoV1Entry> {
319 match self {
320 Self::V1(info) => info.entries,
321 }
322 }
323}
324
325#[derive(Debug)]
331pub struct VerifiedEpochInfo {
332 epoch_info: EpochInfo,
333 committees: Vec<Committee>,
334 start_system_states: Vec<IotaSystemState>,
335}
336
337impl VerifiedEpochInfo {
338 pub fn entries(&self) -> &[EpochInfoV1Entry] {
348 self.epoch_info.entries()
349 }
350
351 pub fn snapshot_entry(&self) -> &EpochInfoV1Entry {
353 self.entries()
356 .last()
357 .expect("a verified EPOCH_INFO covers epochs [0, snapshot_epoch]")
358 }
359
360 pub fn committees(&self) -> &[Committee] {
363 &self.committees
364 }
365
366 pub fn start_system_states(&self) -> &[IotaSystemState] {
371 &self.start_system_states
372 }
373
374 pub fn into_parts(self) -> (EpochInfo, Vec<Committee>, Vec<IotaSystemState>) {
380 (self.epoch_info, self.committees, self.start_system_states)
381 }
382
383 pub(crate) fn into_epoch_info_v2_rows(self) -> Vec<EpochInfoV2> {
387 let VerifiedEpochInfo {
388 epoch_info,
389 start_system_states,
390 ..
391 } = self;
392 let mut previous_end_checkpoint: Option<u64> = None;
397 epoch_info
398 .into_entries()
399 .into_iter()
400 .zip(start_system_states)
401 .map(|(entry, start_system_state)| {
402 let start_checkpoint = previous_end_checkpoint.map_or(0, |seq| seq + 1);
403 previous_end_checkpoint =
404 Some(entry.last_checkpoint_summary.data().sequence_number());
405 epoch_info_v2_row(entry, start_system_state, start_checkpoint)
406 })
407 .collect()
408 }
409
410 pub async fn restore_epoch_info(self, db: &impl RestoreEpochInfo) -> anyhow::Result<()> {
413 let rows = self.into_epoch_info_v2_rows();
414 db.restore_epoch_info(rows).await
415 }
416}
417
418pub fn verify_epoch_info_chain(
427 epoch_info: EpochInfo,
428 snapshot_epoch: EpochId,
429 genesis_committee: Committee,
430 genesis_system_state: IotaSystemState,
431 snapshot_chain_id: ChainIdentifier,
432 expected_chain_id: ChainIdentifier,
433) -> anyhow::Result<VerifiedEpochInfo> {
434 anyhow::ensure!(
435 snapshot_chain_id == expected_chain_id,
436 "snapshot chain_id {snapshot_chain_id} does not match this node's chain \
437 {expected_chain_id} (snapshot from the wrong network's bucket?)"
438 );
439 anyhow::ensure!(
440 genesis_committee.epoch == 0,
441 "the trust root must be the genesis committee, got epoch {}",
442 genesis_committee.epoch
443 );
444 let entry_count = epoch_info.entries().len();
448 anyhow::ensure!(
449 entry_count as u64 == snapshot_epoch + 1,
450 "EPOCH_INFO carries {entry_count} entries, but a snapshot of epoch \
451 {snapshot_epoch} must carry one entry per epoch in [0, {snapshot_epoch}]"
452 );
453
454 let mut chain_verifier = CommitteeChainVerifier::new(genesis_committee);
455 let mut committees = vec![chain_verifier.committee().clone()];
456 let mut start_system_states = vec![genesis_system_state];
459 for (index, entry) in epoch_info.entries().iter().enumerate() {
460 let epoch = entry.last_checkpoint_summary.epoch();
464 anyhow::ensure!(
465 epoch == index as u64,
466 "EPOCH_INFO entry at index {index} carries a summary for epoch {epoch}",
467 );
468
469 let start_committee = start_system_states[index].get_current_epoch_committee();
473 anyhow::ensure!(
474 start_committee.committee() == chain_verifier.committee(),
475 "EPOCH_INFO entry for epoch {index}: the committee in its start system \
476 state does not match the certified committee",
477 );
478
479 chain_verifier
480 .verify_epoch_close(entry.last_checkpoint_summary.clone())
481 .map_err(|e| {
482 anyhow::anyhow!("EPOCH_INFO entry for epoch {index} failed verification: {e}")
483 })?;
484
485 let next_start_state = verify_epoch_boundary_proof(entry)
488 .map_err(|e| anyhow::anyhow!("EPOCH_INFO entry for epoch {index}: {e}"))?;
489
490 committees.push(chain_verifier.committee().clone());
491 start_system_states.push(next_start_state);
492 }
493
494 Ok(VerifiedEpochInfo {
495 epoch_info,
496 committees,
497 start_system_states,
498 })
499}
500
501fn verify_epoch_boundary_proof(entry: &EpochInfoV1Entry) -> anyhow::Result<IotaSystemState> {
506 let summary = entry.last_checkpoint_summary.data();
507
508 anyhow::ensure!(
510 entry.last_checkpoint_contents.digest() == summary.contents_digest,
511 "last_checkpoint_contents does not hash to the signed contents_digest",
512 );
513
514 let expected_execution_digest = entry
516 .last_checkpoint_contents
517 .transactions()
518 .last()
519 .map(|info| ExecutionDigests::new(info.transaction, info.effects))
520 .ok_or_else(|| anyhow::anyhow!("the closing checkpoint has no transactions"))?;
521 let effects = &entry.end_of_epoch_tx_effects;
522 anyhow::ensure!(
523 effects.execution_digests() == expected_execution_digest,
524 "end_of_epoch_tx_effects digest pair does not match the closing checkpoint's last transaction",
525 );
526
527 match effects.events_digest() {
530 Some(events_digest) => anyhow::ensure!(
531 entry.end_of_epoch_tx_events.digest() == *events_digest,
532 "end_of_epoch_tx_events does not hash to the effects' events_digest",
533 ),
534 None => anyhow::ensure!(
535 entry.end_of_epoch_tx_events.is_empty(),
536 "the epoch-change effects carry no events_digest but \
537 end_of_epoch_tx_events is non-empty",
538 ),
539 }
540
541 let written: HashSet<ObjectReference> = effects
544 .all_changed_objects()
545 .into_iter()
546 .map(|(changed, _)| *changed.reference())
547 .collect();
548 let mut objects = Vec::with_capacity(entry.next_epoch_start_system_state_objects.len());
549 for raw in &entry.next_epoch_start_system_state_objects {
550 let object: Object = bcs::from_bytes(raw)
551 .map_err(|e| anyhow::anyhow!("decoding a next-epoch start-state object: {e}"))?;
552 anyhow::ensure!(
553 written.contains(&object.object_ref()),
554 "a next-epoch start-state object is not written by the epoch-change tx",
555 );
556 objects.push(object);
557 }
558 anyhow::ensure!(
559 objects
560 .iter()
561 .any(|o| o.id() == IOTA_SYSTEM_STATE_OBJECT_ID),
562 "the next-epoch start-state objects do not include the system-state object 0x5",
563 );
564 get_iota_system_state(&objects.as_slice())
565 .map_err(|e| anyhow::anyhow!("decoding the next-epoch system state: {e}"))
566}
567
568fn epoch_info_v2_row(
573 entry: EpochInfoV1Entry,
574 system_state: IotaSystemState,
575 start_checkpoint: CheckpointSequenceNumber,
576) -> EpochInfoV2 {
577 EpochInfoV2 {
578 epoch: entry.last_checkpoint_summary.epoch(),
579 start_checkpoint,
580 start_timestamp_ms: system_state.epoch_start_timestamp_ms(),
581 system_state,
582 epoch_close_proof: Some(entry),
583 }
584}
585
586pub fn create_file_metadata(
589 file_path: &std::path::Path,
590 file_compression: FileCompression,
591 file_type: FileType,
592 bucket_num: u32,
593 part_num: u32,
594) -> Result<FileMetadata> {
595 file_compression.compress(file_path)?;
597 let sha3_digest = compute_sha3_checksum(file_path)?;
599 let file_metadata = FileMetadata {
600 file_type,
601 bucket_num,
602 part_num,
603 file_compression,
604 sha3_digest,
605 };
606 Ok(file_metadata)
607}
608
609pub async fn setup_db_state(
610 epoch: u64,
611 state_hash: GlobalStateHash,
612 perpetual_db: Arc<AuthorityPerpetualTables>,
613 checkpoint_store: Arc<CheckpointStore>,
614 committee_store: Arc<CommitteeStore>,
615 verify: bool,
616 num_live_objects: u64,
617 m: MultiProgress,
618) -> Result<()> {
619 let system_state_object = get_iota_system_state(&perpetual_db)?;
622 let new_epoch_start_state = system_state_object.into_epoch_start_state();
623 let next_epoch_committee = new_epoch_start_state.get_iota_committee();
624 let root_digest: ECMHLiveObjectSetDigest = state_hash.digest().into();
625 let last_checkpoint = checkpoint_store
626 .get_epoch_last_checkpoint(epoch)
627 .expect("Error loading last checkpoint for current epoch")
628 .expect("Could not load last checkpoint for current epoch");
629 let flags = EpochFlag::default_for_no_config();
630 let epoch_start_configuration = EpochStartConfiguration::new(
631 new_epoch_start_state,
632 *last_checkpoint.digest(),
633 &perpetual_db,
634 flags,
635 )
636 .unwrap();
637 perpetual_db.set_epoch_start_configuration(&epoch_start_configuration)?;
638 perpetual_db.insert_root_state_hash(epoch, last_checkpoint.sequence_number, state_hash)?;
639 perpetual_db.set_highest_pruned_checkpoint_without_wb(last_checkpoint.sequence_number)?;
640 committee_store.insert_new_committee(&next_epoch_committee)?;
641 checkpoint_store.update_highest_executed_checkpoint(&last_checkpoint)?;
642
643 if verify {
644 let iter = perpetual_db.iter_live_object_set();
645 let local_digest = ECMHLiveObjectSetDigest::from(
646 accumulate_live_object_iter(Box::new(iter), m.clone(), num_live_objects)
647 .await
648 .digest(),
649 );
650 assert_eq!(
651 root_digest, local_digest,
652 "End of epoch {} root state digest {} does not match \
653 local root state hash {} after restoring db from formal snapshot",
654 epoch, root_digest.digest, local_digest.digest,
655 );
656 println!("DB live object state verification completed successfully!");
657 }
658
659 Ok(())
660}
661
662pub async fn accumulate_live_object_iter(
663 iter: Box<dyn Iterator<Item = LiveObject> + '_>,
664 m: MultiProgress,
665 num_live_objects: u64,
666) -> GlobalStateHash {
667 let accum_counter = Arc::new(AtomicU64::new(0));
670 let accum_ticker = ProgressTicker::spawn(
671 m.add(
672 ProgressBar::new(num_live_objects).with_style(
673 ProgressStyle::with_template(
674 "[{elapsed_precise}] {wide_bar} Accumulating DB live objects: {pos}/{len} \
675 ({per_sec}, ETA {eta})",
676 )
677 .unwrap(),
678 ),
679 ),
680 "Accumulating DB live objects",
681 ProgressUnit::Count("objects"),
682 Some(accum_counter.clone()),
683 );
684
685 let mut acc = GlobalStateHash::default();
687 for live_object in iter {
688 GlobalStateHasher::accumulate_live_object(&mut acc, &live_object);
689 accum_counter.fetch_add(1, Ordering::Relaxed);
690 }
691 let num_accumulated = accum_counter.load(Ordering::Relaxed);
692 assert!(
693 num_accumulated <= num_live_objects,
694 "Accumulated more objects ({num_accumulated}) than expected ({num_live_objects})"
695 );
696 accum_ticker.finish_with_message("DB live object accumulation completed");
697 acc
698}