Skip to main content

sui_tool/
lib.rs

1// Copyright (c) 2021, Facebook, Inc. and its affiliates
2// Copyright (c) Mysten Labs, Inc.
3// SPDX-License-Identifier: Apache-2.0
4
5use anyhow::{Context, Result};
6use backoff::ExponentialBackoff;
7use fastcrypto::traits::ToFromBytes;
8use futures::future::AbortHandle;
9use futures::future::join_all;
10use itertools::Itertools;
11use std::collections::BTreeMap;
12use std::fmt::Write;
13use std::net::{IpAddr, Ipv4Addr, SocketAddr};
14use std::num::NonZeroUsize;
15use std::path::{Path, PathBuf};
16use std::sync::Arc;
17use std::sync::atomic::{AtomicU64, AtomicUsize, Ordering};
18use std::time::Duration;
19use std::{fs, io};
20use sui_config::{NodeConfig, genesis::Genesis};
21use sui_core::authority_client::{AuthorityAPI, NetworkAuthorityClient};
22use sui_core::execution_cache::build_execution_cache_from_env;
23use sui_network::default_mysten_network_config;
24use sui_protocol_config::Chain;
25use sui_rpc_api::Client;
26use sui_storage::object_store::http::HttpDownloaderBuilder;
27use sui_storage::object_store::util::MANIFEST_FILENAME;
28use sui_storage::object_store::util::Manifest;
29use sui_storage::object_store::util::{build_object_store, end_of_epoch_data, fetch_checkpoint};
30use sui_types::committee::QUORUM_THRESHOLD;
31use sui_types::crypto::AuthorityPublicKeyBytes;
32use sui_types::digests::ChainIdentifier;
33use sui_types::global_state_hash::GlobalStateHash;
34use sui_types::messages_grpc::LayoutGenerationOption;
35use sui_types::multiaddr::Multiaddr;
36use sui_types::{base_types::*, object::Owner};
37use tokio::sync::{mpsc, oneshot};
38use tokio::task::JoinHandle;
39use tokio::time::Instant;
40
41use anyhow::anyhow;
42use clap::ValueEnum;
43use eyre::ContextCompat;
44use fastcrypto::hash::MultisetHash;
45use futures::{StreamExt, TryStreamExt};
46use indicatif::{MultiProgress, ProgressBar, ProgressStyle};
47use prometheus::Registry;
48use serde::{Deserialize, Serialize};
49use sui_config::object_storage_config::{ObjectStoreConfig, ObjectStoreType};
50use sui_core::authority::AuthorityStore;
51use sui_core::authority::authority_store_tables::AuthorityPerpetualTables;
52use sui_core::checkpoints::CheckpointStore;
53use sui_core::epoch::committee_store::CommitteeStore;
54use sui_core::storage::RocksDbStore;
55use sui_snapshot::reader::{StateAccumulatorSender, StateSnapshotReaderV1};
56use sui_snapshot::setup_db_state;
57use sui_storage::object_store::ObjectStoreGetExt;
58use sui_storage::object_store::util::{exists, get_path};
59use sui_types::full_checkpoint_content::CheckpointData;
60use sui_types::messages_checkpoint::{CheckpointCommitment, ECMHLiveObjectSetDigest};
61use sui_types::messages_grpc::{
62    ObjectInfoRequest, ObjectInfoRequestKind, ObjectInfoResponse, TransactionInfoRequest,
63    TransactionStatus,
64};
65
66use crate::formal_snapshot_util::read_summaries_for_list_no_verify;
67use sui_core::authority::authority_store_pruner::PrunerWatermarks;
68use sui_types::storage::ReadStore;
69use tracing::info;
70use typed_store::DBMetrics;
71
72pub mod commands;
73pub mod db_shell;
74pub mod db_tool;
75mod formal_snapshot_util;
76#[cfg(all(feature = "tideconsole", not(windows)))]
77pub mod tideconsole_cmd;
78
79#[derive(
80    Clone, Serialize, Deserialize, Debug, PartialEq, Copy, PartialOrd, Ord, Eq, ValueEnum, Default,
81)]
82pub enum SnapshotVerifyMode {
83    /// verification of both db state and downloaded checkpoints are skipped.
84    /// This is the fastest mode, but is unsafe, and thus should only be used
85    /// if you fully trust the source for both the snapshot and the checkpoint
86    /// archive.
87    None,
88    /// verify snapshot state during download, but no post-restore db verification.
89    /// Checkpoint verification is performed.
90    #[default]
91    Normal,
92    /// In ADDITION to the behavior of `--verify normal`, verify db state post-restore
93    /// against the end of epoch state root commitment.
94    Strict,
95}
96
97// This functions requires at least one of genesis or fullnode_rpc to be `Some`.
98async fn make_clients(
99    sui_client: &Client,
100) -> Result<BTreeMap<AuthorityName, (Multiaddr, NetworkAuthorityClient)>> {
101    let mut net_config = default_mysten_network_config();
102    net_config.connect_timeout = Some(Duration::from_secs(5));
103    let mut authority_clients = BTreeMap::new();
104
105    let active_validators = sui_client
106        .get_system_state_summary(None)
107        .await?
108        .active_validators;
109
110    for validator in active_validators {
111        let net_addr = Multiaddr::try_from(validator.net_address)
112            .unwrap()
113            .rewrite_http_to_https();
114        let tls_config = sui_tls::create_rustls_client_config(
115            sui_types::crypto::NetworkPublicKey::from_bytes(&validator.network_pubkey_bytes)?,
116            sui_tls::SUI_VALIDATOR_SERVER_NAME.to_string(),
117            None,
118        );
119        let channel = net_config
120            .connect_lazy(&net_addr, tls_config)
121            .map_err(|err| anyhow!(err.to_string()))?;
122        let client = NetworkAuthorityClient::new(channel);
123        let public_key_bytes =
124            AuthorityPublicKeyBytes::from_bytes(&validator.protocol_pubkey_bytes)?;
125        authority_clients.insert(public_key_bytes, (net_addr.clone(), client));
126    }
127
128    Ok(authority_clients)
129}
130
131type ObjectVersionResponses = (Option<SequenceNumber>, Result<ObjectInfoResponse>, f64);
132pub struct ObjectData {
133    requested_id: ObjectID,
134    responses: Vec<(AuthorityName, Multiaddr, ObjectVersionResponses)>,
135}
136
137trait OptionDebug<T> {
138    fn opt_debug(&self, def_str: &str) -> String;
139}
140
141impl<T> OptionDebug<T> for Option<T>
142where
143    T: std::fmt::Debug,
144{
145    fn opt_debug(&self, def_str: &str) -> String {
146        match self {
147            None => def_str.to_string(),
148            Some(t) => format!("{:?}", t),
149        }
150    }
151}
152
153#[allow(clippy::type_complexity)]
154pub struct GroupedObjectOutput {
155    pub grouped_results: BTreeMap<
156        Option<(
157            Option<SequenceNumber>,
158            ObjectDigest,
159            TransactionDigest,
160            Owner,
161            Option<TransactionDigest>,
162        )>,
163        Vec<AuthorityName>,
164    >,
165    pub voting_power: Vec<(
166        Option<(
167            Option<SequenceNumber>,
168            ObjectDigest,
169            TransactionDigest,
170            Owner,
171            Option<TransactionDigest>,
172        )>,
173        u64,
174    )>,
175    pub available_voting_power: u64,
176    pub fully_locked: bool,
177}
178
179impl GroupedObjectOutput {
180    pub fn new(
181        object_data: ObjectData,
182        committee: Arc<BTreeMap<AuthorityPublicKeyBytes, u64>>,
183    ) -> Self {
184        let mut grouped_results = BTreeMap::new();
185        let mut voting_power = BTreeMap::new();
186        let mut available_voting_power = 0;
187        for (name, _, (version, resp, _elapsed)) in &object_data.responses {
188            let stake = committee.get(name).unwrap();
189            let key = match resp {
190                Ok(r) => {
191                    let obj_digest = r.object.compute_object_reference().2;
192                    let parent_tx_digest = r.object.previous_transaction;
193                    let owner = r.object.owner.clone();
194                    let lock = r.lock_for_debugging.as_ref().map(|lock| *lock.digest());
195                    if lock.is_none() {
196                        available_voting_power += stake;
197                    }
198                    Some((*version, obj_digest, parent_tx_digest, owner, lock))
199                }
200                Err(_) => None,
201            };
202            let entry = grouped_results.entry(key.clone()).or_insert_with(Vec::new);
203            entry.push(*name);
204            let entry: &mut u64 = voting_power.entry(key).or_default();
205            *entry += stake;
206        }
207        let voting_power = voting_power
208            .into_iter()
209            .sorted_by(|(_, v1), (_, v2)| Ord::cmp(v2, v1))
210            .collect::<Vec<_>>();
211        let mut fully_locked = false;
212        if !voting_power.is_empty()
213            && voting_power.first().unwrap().1 + available_voting_power < QUORUM_THRESHOLD
214        {
215            fully_locked = true;
216        }
217        Self {
218            grouped_results,
219            voting_power,
220            available_voting_power,
221            fully_locked,
222        }
223    }
224}
225
226#[allow(clippy::format_in_format_args)]
227impl std::fmt::Display for GroupedObjectOutput {
228    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
229        writeln!(f, "available stake: {}", self.available_voting_power)?;
230        writeln!(f, "fully locked: {}", self.fully_locked)?;
231        writeln!(f, "{:<100}\n", "-".repeat(100))?;
232        for (key, stake) in &self.voting_power {
233            let val = self.grouped_results.get(key).unwrap();
234            writeln!(f, "total stake: {stake}")?;
235            match key {
236                Some((_version, obj_digest, parent_tx_digest, owner, lock)) => {
237                    let lock = lock.opt_debug("no-known-lock");
238                    writeln!(f, "obj ref: {obj_digest}")?;
239                    writeln!(f, "parent tx: {parent_tx_digest}")?;
240                    writeln!(f, "owner: {owner}")?;
241                    writeln!(f, "lock: {lock}")?;
242                    for (i, name) in val.iter().enumerate() {
243                        writeln!(f, "        {:<4} {:<20}", i, name.concise(),)?;
244                    }
245                }
246                None => {
247                    writeln!(f, "ERROR")?;
248                    for (i, name) in val.iter().enumerate() {
249                        writeln!(f, "        {:<4} {:<20}", i, name.concise(),)?;
250                    }
251                }
252            };
253            writeln!(f, "{:<100}\n", "-".repeat(100))?;
254        }
255        Ok(())
256    }
257}
258
259struct ConciseObjectOutput(ObjectData);
260
261impl ConciseObjectOutput {
262    fn header() -> String {
263        format!(
264            "{:<20} {:<8} {:<66} {:<45} {}",
265            "validator", "version", "digest", "parent_cert", "owner"
266        )
267    }
268}
269
270impl std::fmt::Display for ConciseObjectOutput {
271    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
272        for (name, _multi_addr, (version, resp, _time_elapsed)) in &self.0.responses {
273            write!(
274                f,
275                "{:<20} {:<8}",
276                format!("{:?}", name.concise()),
277                version.map(|s| s.value()).opt_debug("-")
278            )?;
279            match resp {
280                Err(_) => writeln!(
281                    f,
282                    "{:<66} {:<45} {:<51}",
283                    "object-fetch-failed", "no-cert-available", "no-owner-available"
284                )?,
285                Ok(resp) => {
286                    let obj_digest = resp.object.compute_object_reference().2;
287                    let parent = resp.object.previous_transaction;
288                    let owner = resp.object.owner.clone();
289                    write!(f, " {:<66} {:<45} {:<51}", obj_digest, parent, owner)?;
290                }
291            }
292            writeln!(f)?;
293        }
294        Ok(())
295    }
296}
297
298struct VerboseObjectOutput(ObjectData);
299
300impl std::fmt::Display for VerboseObjectOutput {
301    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
302        writeln!(f, "Object: {}", self.0.requested_id)?;
303
304        for (name, multiaddr, (version, resp, timespent)) in &self.0.responses {
305            writeln!(f, "validator: {:?}, addr: {:?}", name.concise(), multiaddr)?;
306            writeln!(
307                f,
308                "-- version: {} ({:.3}s)",
309                version.opt_debug("<version not available>"),
310                timespent,
311            )?;
312
313            match resp {
314                Err(e) => writeln!(f, "Error fetching object: {}", e)?,
315                Ok(resp) => {
316                    writeln!(
317                        f,
318                        "  -- object digest: {}",
319                        resp.object.compute_object_reference().2
320                    )?;
321                    if resp.object.is_package() {
322                        writeln!(f, "  -- object: <Move Package>")?;
323                    } else if let Some(layout) = &resp.layout {
324                        writeln!(
325                            f,
326                            "  -- object: Move Object: {}",
327                            resp.object
328                                .data
329                                .try_as_move()
330                                .unwrap()
331                                .to_move_struct(layout)
332                                .unwrap()
333                        )?;
334                    }
335                    writeln!(f, "  -- owner: {}", resp.object.owner)?;
336                    writeln!(
337                        f,
338                        "  -- locked by: {}",
339                        resp.lock_for_debugging.opt_debug("<not locked>")
340                    )?;
341                }
342            }
343        }
344        Ok(())
345    }
346}
347
348pub async fn get_object(
349    obj_id: ObjectID,
350    version: Option<u64>,
351    validator: Option<AuthorityName>,
352    clients: Arc<BTreeMap<AuthorityName, (Multiaddr, NetworkAuthorityClient)>>,
353) -> Result<ObjectData> {
354    let responses = join_all(
355        clients
356            .iter()
357            .filter(|(name, _)| {
358                if let Some(v) = validator {
359                    v == **name
360                } else {
361                    true
362                }
363            })
364            .map(|(name, (address, client))| async {
365                let object_version = get_object_impl(client, obj_id, version).await;
366                (*name, address.clone(), object_version)
367            }),
368    )
369    .await;
370
371    Ok(ObjectData {
372        requested_id: obj_id,
373        responses,
374    })
375}
376
377pub async fn get_transaction_block(
378    tx_digest: TransactionDigest,
379    show_input_tx: bool,
380    fullnode_rpc: String,
381) -> Result<String> {
382    let sui_client = Client::new(fullnode_rpc)?;
383    let clients = make_clients(&sui_client).await?;
384    let timer = Instant::now();
385    let responses = join_all(clients.iter().map(|(name, (address, client))| async {
386        let result = client
387            .handle_transaction_info_request(TransactionInfoRequest {
388                transaction_digest: tx_digest,
389            })
390            .await;
391        (
392            *name,
393            address.clone(),
394            result,
395            timer.elapsed().as_secs_f64(),
396        )
397    }))
398    .await;
399
400    // Grab one validator that return Some(TransactionInfoResponse)
401    let validator_aware_of_tx = responses.iter().find(|r| r.2.is_ok());
402
403    let responses = responses
404        .iter()
405        .map(|r| {
406            let key =
407                r.2.as_ref()
408                    .map(|ok_result| match &ok_result.status {
409                        TransactionStatus::Signed(_) => None,
410                        TransactionStatus::Executed(_, effects, _) => Some(effects.digest()),
411                    })
412                    .ok();
413            let err = r.2.as_ref().err();
414            (key, err, r)
415        })
416        .sorted_by(|(k1, err1, _), (k2, err2, _)| {
417            Ord::cmp(k1, k2).then_with(|| Ord::cmp(err1, err2))
418        })
419        .chunk_by(|(_, _err, r)| {
420            r.2.as_ref().map(|ok_result| match &ok_result.status {
421                TransactionStatus::Signed(_) => None,
422                TransactionStatus::Executed(_, effects, _) => Some((
423                    ok_result.transaction.transaction_data(),
424                    effects.data(),
425                    effects.digest(),
426                )),
427            })
428        });
429    let mut s = String::new();
430    for (i, (key, group)) in responses.into_iter().enumerate() {
431        match key {
432            Ok(Some((tx, effects, effects_digest))) => {
433                writeln!(
434                    &mut s,
435                    "#{:<2} tx_digest: {:<68?} effects_digest: {:?}",
436                    i, tx_digest, effects_digest,
437                )?;
438                writeln!(&mut s, "{:#?}", effects)?;
439                if show_input_tx {
440                    writeln!(&mut s, "{:#?}", tx)?;
441                }
442            }
443            Ok(None) => {
444                writeln!(
445                    &mut s,
446                    "#{:<2} tx_digest: {:<68?} Signed but not executed",
447                    i, tx_digest
448                )?;
449                if show_input_tx {
450                    // In this case, we expect at least one validator knows about this tx
451                    let validator_aware_of_tx = validator_aware_of_tx.unwrap();
452                    let client = &clients.get(&validator_aware_of_tx.0).unwrap().1;
453                    let tx_info = client.handle_transaction_info_request(TransactionInfoRequest {
454                        transaction_digest: tx_digest,
455                    }).await.unwrap_or_else(|e| panic!("Validator {:?} should have known about tx_digest: {:?}, got error: {:?}", validator_aware_of_tx.0, tx_digest, e));
456                    writeln!(&mut s, "{:#?}", tx_info)?;
457                }
458            }
459            other => {
460                writeln!(&mut s, "#{:<2} {:#?}", i, other)?;
461            }
462        }
463        for (j, (_, _, res)) in group.enumerate() {
464            writeln!(
465                &mut s,
466                "        {:<4} {:<20} {:<56} ({:.3}s)",
467                j,
468                res.0.concise(),
469                format!("{}", res.1),
470                res.3
471            )?;
472        }
473        writeln!(&mut s, "{:<100}\n", "-".repeat(100))?;
474    }
475    Ok(s)
476}
477
478async fn get_object_impl(
479    client: &NetworkAuthorityClient,
480    id: ObjectID,
481    version: Option<u64>,
482) -> (Option<SequenceNumber>, Result<ObjectInfoResponse>, f64) {
483    let start = Instant::now();
484    let resp = client
485        .handle_object_info_request(ObjectInfoRequest {
486            object_id: id,
487            generate_layout: LayoutGenerationOption::Generate,
488            request_kind: match version {
489                None => ObjectInfoRequestKind::LatestObjectInfo,
490                Some(v) => ObjectInfoRequestKind::PastObjectInfoDebug(SequenceNumber::from_u64(v)),
491            },
492        })
493        .await
494        .map_err(anyhow::Error::from);
495    let elapsed = start.elapsed().as_secs_f64();
496
497    let resp_version = resp.as_ref().ok().map(|r| r.object.version().value());
498    (resp_version.map(SequenceNumber::from), resp, elapsed)
499}
500
501pub(crate) fn make_anemo_config() -> anemo_cli::Config {
502    use sui_network::discovery::*;
503    use sui_network::state_sync::*;
504
505    // TODO: implement `ServiceInfo` generation in anemo-build and use here.
506    anemo_cli::Config::new()
507        // Sui discovery
508        .add_service(
509            "Discovery",
510            anemo_cli::ServiceInfo::new().add_method(
511                "GetKnownPeersV2",
512                anemo_cli::ron_method!(DiscoveryClient, get_known_peers_v2, ()),
513            ),
514        )
515        // Sui state sync
516        .add_service(
517            "StateSync",
518            anemo_cli::ServiceInfo::new()
519                .add_method(
520                    "PushCheckpointSummary",
521                    anemo_cli::ron_method!(
522                        StateSyncClient,
523                        push_checkpoint_summary,
524                        sui_types::messages_checkpoint::CertifiedCheckpointSummary
525                    ),
526                )
527                .add_method(
528                    "GetCheckpointSummary",
529                    anemo_cli::ron_method!(
530                        StateSyncClient,
531                        get_checkpoint_summary,
532                        GetCheckpointSummaryRequest
533                    ),
534                )
535                .add_method(
536                    "GetCheckpointContents",
537                    anemo_cli::ron_method!(
538                        StateSyncClient,
539                        get_checkpoint_contents,
540                        sui_types::messages_checkpoint::CheckpointContentsDigest
541                    ),
542                )
543                .add_method(
544                    "GetCheckpointAvailability",
545                    anemo_cli::ron_method!(StateSyncClient, get_checkpoint_availability, ()),
546                ),
547        )
548}
549
550fn copy_dir_all(
551    src: impl AsRef<Path>,
552    dst: impl AsRef<Path>,
553    skip: Vec<PathBuf>,
554) -> io::Result<()> {
555    fs::create_dir_all(&dst)?;
556    for entry in fs::read_dir(src)? {
557        let entry = entry?;
558        let ty = entry.file_type()?;
559        if skip.contains(&entry.path()) {
560            continue;
561        }
562        if ty.is_dir() {
563            copy_dir_all(
564                entry.path(),
565                dst.as_ref().join(entry.file_name()),
566                skip.clone(),
567            )?;
568        } else {
569            fs::copy(entry.path(), dst.as_ref().join(entry.file_name()))?;
570        }
571    }
572    Ok(())
573}
574
575pub async fn restore_from_db_checkpoint(
576    config: &NodeConfig,
577    db_checkpoint_path: &Path,
578) -> Result<(), anyhow::Error> {
579    copy_dir_all(db_checkpoint_path, config.db_path(), vec![])?;
580    Ok(())
581}
582
583fn start_summary_sync(
584    perpetual_db: Arc<AuthorityPerpetualTables>,
585    committee_store: Arc<CommitteeStore>,
586    checkpoint_store: Arc<CheckpointStore>,
587    m: MultiProgress,
588    genesis: Genesis,
589    ingestion_url: String,
590    num_parallel_downloads: usize,
591    verify: bool,
592    end_of_epoch_checkpoint_seq_nums: Vec<u64>,
593) -> JoinHandle<Result<(), anyhow::Error>> {
594    tokio::spawn(async move {
595        let store = AuthorityStore::open_no_genesis(perpetual_db, false, &Registry::default())?;
596        let cache_traits = build_execution_cache_from_env(&Registry::default(), &store);
597        let state_sync_store =
598            RocksDbStore::new(cache_traits, committee_store, checkpoint_store.clone());
599        // Only insert the genesis checkpoint if the DB is empty and doesn't have it already
600        if checkpoint_store
601            .get_checkpoint_by_digest(genesis.checkpoint().digest())
602            .unwrap()
603            .is_none()
604        {
605            checkpoint_store.insert_checkpoint_contents(genesis.checkpoint_contents().clone())?;
606            checkpoint_store.insert_verified_checkpoint(&genesis.checkpoint())?;
607            checkpoint_store.update_highest_synced_checkpoint(&genesis.checkpoint())?;
608        }
609
610        let last_checkpoint = end_of_epoch_checkpoint_seq_nums
611            .last()
612            .expect("Expected at least one checkpoint");
613
614        let num_to_sync = end_of_epoch_checkpoint_seq_nums.len() as u64;
615        let sync_progress_bar = m.add(
616            ProgressBar::new(num_to_sync).with_style(
617                ProgressStyle::with_template("[{elapsed_precise}] {wide_bar} {pos}/{len} ({msg})")
618                    .unwrap(),
619            ),
620        );
621
622        let cloned_progress_bar = sync_progress_bar.clone();
623        let sync_checkpoint_counter = Arc::new(AtomicU64::new(0));
624        let s_instant = Instant::now();
625
626        let cloned_counter = sync_checkpoint_counter.clone();
627        let latest_synced = checkpoint_store
628            .get_highest_synced_checkpoint()?
629            .map(|c| c.sequence_number)
630            .unwrap_or(0);
631        let s_start = latest_synced
632            .checked_add(1)
633            .wrap_err("Checkpoint overflow")
634            .map_err(|_| anyhow!("Failed to increment checkpoint"))?;
635        tokio::spawn(async move {
636            loop {
637                if cloned_progress_bar.is_finished() {
638                    break;
639                }
640                let num_summaries = cloned_counter.load(Ordering::Relaxed);
641                let total_checkpoints_per_sec =
642                    num_summaries as f64 / s_instant.elapsed().as_secs_f64();
643                cloned_progress_bar.set_position(s_start + num_summaries);
644                cloned_progress_bar.set_message(format!(
645                    "checkpoints synced per sec: {}",
646                    total_checkpoints_per_sec
647                ));
648                tokio::time::sleep(Duration::from_secs(1)).await;
649            }
650        });
651
652        read_summaries_for_list_no_verify(
653            ingestion_url,
654            num_parallel_downloads,
655            state_sync_store.clone(),
656            end_of_epoch_checkpoint_seq_nums.clone(),
657            sync_checkpoint_counter,
658        )
659        .await?;
660        sync_progress_bar.finish_with_message("Checkpoint summary sync is complete");
661        info!("Checkpoint summary sync is complete");
662
663        let checkpoint = checkpoint_store
664            .get_checkpoint_by_sequence_number(*last_checkpoint)?
665            .ok_or(anyhow!("Failed to read last checkpoint"))?;
666        if verify {
667            let verify_progress_bar = m.add(
668                ProgressBar::new(num_to_sync).with_style(
669                    ProgressStyle::with_template(
670                        "[{elapsed_precise}] {wide_bar} {pos}/{len} ({msg})",
671                    )
672                    .unwrap(),
673                ),
674            );
675            let cloned_verify_progress_bar = verify_progress_bar.clone();
676            let verify_checkpoint_counter = Arc::new(AtomicU64::new(0));
677            let cloned_verify_counter = verify_checkpoint_counter.clone();
678            let v_instant = Instant::now();
679
680            tokio::spawn(async move {
681                loop {
682                    if cloned_verify_progress_bar.is_finished() {
683                        break;
684                    }
685                    let num_summaries = cloned_verify_counter.load(Ordering::Relaxed);
686                    let total_checkpoints_per_sec =
687                        num_summaries as f64 / v_instant.elapsed().as_secs_f64();
688                    cloned_verify_progress_bar.set_position(num_summaries);
689                    cloned_verify_progress_bar.set_message(format!(
690                        "checkpoints verified per sec: {}",
691                        total_checkpoints_per_sec
692                    ));
693                    tokio::time::sleep(Duration::from_secs(1)).await;
694                }
695            });
696
697            for (cp_epoch, epoch_last_cp_seq_num) in
698                end_of_epoch_checkpoint_seq_nums.iter().enumerate()
699            {
700                let epoch_last_checkpoint = checkpoint_store
701                    .get_checkpoint_by_sequence_number(*epoch_last_cp_seq_num)?
702                    .ok_or(anyhow!("Failed to read checkpoint"))?;
703                let committee = state_sync_store.get_committee(cp_epoch as u64).expect(
704                    "Expected committee to exist after syncing all end of epoch checkpoints",
705                );
706                epoch_last_checkpoint
707                    .verify_authority_signatures(&committee)
708                    .expect("Failed to verify checkpoint");
709                verify_checkpoint_counter.fetch_add(1, Ordering::Relaxed);
710            }
711
712            verify_progress_bar.finish_with_message("Checkpoint summary verification is complete");
713        }
714
715        checkpoint_store.update_highest_verified_checkpoint(&checkpoint)?;
716        checkpoint_store.update_highest_synced_checkpoint(&checkpoint)?;
717        checkpoint_store.update_highest_executed_checkpoint(&checkpoint)?;
718        checkpoint_store.update_highest_pruned_checkpoint(&checkpoint)?;
719        Ok::<(), anyhow::Error>(())
720    })
721}
722
723pub async fn get_latest_available_epoch(
724    snapshot_store_config: &ObjectStoreConfig,
725) -> Result<u64, anyhow::Error> {
726    let remote_object_store = if snapshot_store_config.no_sign_request {
727        snapshot_store_config.make_http()?
728    } else {
729        snapshot_store_config.make().map(Arc::new)?
730    };
731    let manifest_contents = remote_object_store
732        .get_bytes(&get_path(MANIFEST_FILENAME))
733        .await?;
734    let root_manifest: Manifest = serde_json::from_slice(&manifest_contents)
735        .map_err(|err| anyhow!("Error parsing MANIFEST from bytes: {}", err))?;
736    let epoch = root_manifest
737        .available_epochs
738        .iter()
739        .max()
740        .ok_or(anyhow!("No snapshot found in manifest"))?;
741    Ok(*epoch)
742}
743
744pub async fn check_completed_snapshot(
745    snapshot_store_config: &ObjectStoreConfig,
746    epoch: EpochId,
747) -> Result<(), anyhow::Error> {
748    let success_marker = format!("epoch_{}/_SUCCESS", epoch);
749    let archive_success_marker = format!("archive/epoch_{}/_SUCCESS", epoch);
750    let remote_object_store = if snapshot_store_config.no_sign_request {
751        snapshot_store_config.make_http()?
752    } else {
753        snapshot_store_config.make().map(Arc::new)?
754    };
755
756    // Check regular location first, then archive location
757    if exists(&remote_object_store, &get_path(success_marker.as_str())).await
758        || exists(
759            &remote_object_store,
760            &get_path(archive_success_marker.as_str()),
761        )
762        .await
763    {
764        Ok(())
765    } else {
766        Err(anyhow!(
767            "missing success marker at {}/{} or {}/{}",
768            snapshot_store_config.bucket.as_ref().unwrap_or(
769                &snapshot_store_config
770                    .clone()
771                    .aws_endpoint
772                    .unwrap_or("unknown_bucket".to_string())
773            ),
774            success_marker,
775            snapshot_store_config.bucket.as_ref().unwrap_or(
776                &snapshot_store_config
777                    .clone()
778                    .aws_endpoint
779                    .unwrap_or("unknown_bucket".to_string())
780            ),
781            archive_success_marker
782        ))
783    }
784}
785
786pub async fn download_formal_snapshot(
787    path: &Path,
788    epoch: EpochId,
789    genesis: &Path,
790    snapshot_store_config: ObjectStoreConfig,
791    ingestion_url: &str,
792    num_parallel_downloads: usize,
793    num_parallel_chunks: usize,
794    network: Chain,
795    verify: SnapshotVerifyMode,
796    max_retries: usize,
797    metrics_port: u16,
798) -> Result<(), anyhow::Error> {
799    let m = MultiProgress::new();
800    let msg = format!(
801        "Beginning formal snapshot restore to end of epoch {}, network: {:?}, verification mode: {:?}",
802        epoch, network, verify
803    );
804    m.println(&msg).unwrap();
805    info!("{}", msg);
806
807    let path = path.join("staging").to_path_buf();
808    if path.exists() {
809        fs::remove_dir_all(path.clone())?;
810    }
811
812    // Start prometheus server so that we can serve metrics during snapshot download
813    let metrics_addr = SocketAddr::new(IpAddr::V4(Ipv4Addr::new(127, 0, 0, 1)), metrics_port);
814    let registry_service = mysten_metrics::start_prometheus_server(metrics_addr);
815    let prometheus_registry = registry_service.default_registry();
816    DBMetrics::init(registry_service.clone());
817    mysten_metrics::init_metrics(&prometheus_registry);
818
819    let perpetual_db = Arc::new(AuthorityPerpetualTables::open(
820        &path.join("store"),
821        None,
822        None,
823    ));
824    let genesis = Genesis::load(genesis)?;
825    let genesis_chain = ChainIdentifier::from(*genesis.checkpoint().digest()).chain();
826    if genesis_chain != network {
827        return Err(anyhow!(
828            "Genesis file is for chain {}, but formal snapshot download is configured for --network {}. \
829            Use the matching genesis.blob or pass the correct --network flag.",
830            genesis_chain.as_str(),
831            network.as_str(),
832        ));
833    }
834    let genesis_committee = genesis.committee();
835    let committee_store = Arc::new(CommitteeStore::new(
836        path.join("epochs"),
837        &genesis_committee,
838        None,
839    ));
840    let checkpoint_store = CheckpointStore::new(
841        &path.join("checkpoints"),
842        Arc::new(PrunerWatermarks::default()),
843    );
844
845    let end_of_epoch_checkpoint_seq_nums: Vec<_> = end_of_epoch_data(ingestion_url, vec![])
846        .await?
847        .into_iter()
848        .take((epoch + 1) as usize)
849        .collect();
850
851    let summaries_handle = start_summary_sync(
852        perpetual_db.clone(),
853        committee_store.clone(),
854        checkpoint_store.clone(),
855        m.clone(),
856        genesis.clone(),
857        ingestion_url.to_string(),
858        num_parallel_downloads,
859        verify != SnapshotVerifyMode::None,
860        end_of_epoch_checkpoint_seq_nums.clone(),
861    );
862
863    // Start transaction backfill in parallel with summary sync
864    let backfill_handle = {
865        let perpetual_db = perpetual_db.clone();
866        let ingestion_url = ingestion_url.to_string();
867        let m = m.clone();
868        let end_of_epoch_checkpoint_seq_nums = end_of_epoch_checkpoint_seq_nums.clone();
869        tokio::spawn(async move {
870            backfill_epoch_transaction_digests(
871                perpetual_db,
872                epoch,
873                ingestion_url,
874                num_parallel_downloads,
875                max_retries,
876                m,
877                end_of_epoch_checkpoint_seq_nums,
878            )
879            .await
880        })
881    };
882
883    let (_abort_handle, abort_registration) = AbortHandle::new_pair();
884    let perpetual_db_clone = perpetual_db.clone();
885    let snapshot_dir = path.parent().unwrap().join("snapshot");
886    if snapshot_dir.exists() {
887        fs::remove_dir_all(snapshot_dir.clone())?;
888    }
889    let snapshot_dir_clone = snapshot_dir.clone();
890
891    // TODO if verify is false, we should skip generating these and
892    // not pass in a channel to the reader
893    let (sender, mut receiver) = mpsc::channel(num_parallel_downloads);
894    let (accumulation_done_sender, accumulation_done_receiver) = oneshot::channel();
895    let m_clone = m.clone();
896
897    let snapshot_handle = tokio::spawn(async move {
898        let local_store_config = ObjectStoreConfig {
899            object_store: Some(ObjectStoreType::File),
900            directory: Some(snapshot_dir_clone.to_path_buf()),
901            ..Default::default()
902        };
903        let mut reader = StateSnapshotReaderV1::new(
904            epoch,
905            &snapshot_store_config,
906            &local_store_config,
907            NonZeroUsize::new(num_parallel_downloads).unwrap(),
908            m_clone,
909            false, // skip_reset_local_store
910            max_retries,
911            num_parallel_chunks,
912        )
913        .await
914        .context("Failed to create snapshot reader")?;
915        reader
916            .read(
917                perpetual_db_clone.clone(),
918                abort_registration,
919                Some(StateAccumulatorSender {
920                    partials: sender,
921                    completion: accumulation_done_sender,
922                }),
923            )
924            .await
925            .context("Failed to read snapshot")?;
926        info!("Snapshot download complete");
927        Ok::<(), anyhow::Error>(())
928    });
929    tokio::pin!(summaries_handle);
930    tokio::pin!(snapshot_handle);
931    tokio::pin!(backfill_handle);
932
933    let mut root_global_state_hash = GlobalStateHash::default();
934    let mut num_live_objects = 0;
935    while let Some((partial_hash, num_objects)) = receiver.recv().await {
936        num_live_objects += num_objects;
937        root_global_state_hash.union(&partial_hash);
938    }
939    if accumulation_done_receiver.await.is_err() {
940        (&mut snapshot_handle)
941            .await
942            .map_err(|error| anyhow!("Snapshot task failed: {error}"))??;
943        return Err(anyhow!("Snapshot accumulation did not complete"));
944    }
945
946    let mut summaries_done = false;
947    let mut snapshot_done = false;
948    let mut backfill_done = false;
949
950    while !summaries_done {
951        tokio::select! {
952            result = &mut summaries_handle, if !summaries_done => {
953                summaries_done = true;
954                result.map_err(|error| anyhow!("Summaries task failed: {error}"))??;
955            }
956            result = &mut backfill_handle, if !backfill_done => {
957                backfill_done = true;
958                result.map_err(|error| anyhow!("Backfill task failed: {error}"))??;
959            }
960            result = &mut snapshot_handle, if !snapshot_done => {
961                snapshot_done = true;
962                result.map_err(|error| anyhow!("Snapshot task failed: {error}"))??;
963            }
964        }
965    }
966
967    let last_checkpoint = checkpoint_store
968        .get_highest_verified_checkpoint()?
969        .expect("Expected nonempty checkpoint store");
970
971    // Perform snapshot state verification
972    if verify != SnapshotVerifyMode::None {
973        assert_eq!(
974            last_checkpoint.epoch(),
975            epoch,
976            "Expected highest verified checkpoint ({}) to be for epoch {} but was for epoch {}",
977            last_checkpoint.sequence_number,
978            epoch,
979            last_checkpoint.epoch()
980        );
981        let commitment = last_checkpoint
982            .end_of_epoch_data
983            .as_ref()
984            .expect("Expected highest verified checkpoint to have end of epoch data")
985            .epoch_commitments
986            .last()
987            .expect(
988                "End of epoch has no commitments. This likely means that the epoch \
989                you are attempting to restore from does not support end of epoch state \
990                digest commitment. If restoring from mainnet, `--epoch` must be > 20, \
991                and for testnet, `--epoch` must be > 12.",
992            );
993        match commitment {
994            CheckpointCommitment::ECMHLiveObjectSetDigest(consensus_digest) => {
995                let local_digest: ECMHLiveObjectSetDigest = root_global_state_hash.digest().into();
996                assert_eq!(
997                    *consensus_digest, local_digest,
998                    "End of epoch {} root state digest {} does not match \
999                    local root state hash {} computed from snapshot data",
1000                    epoch, consensus_digest.digest, local_digest.digest,
1001                );
1002                let progress_bar = m.add(
1003                    ProgressBar::new(1).with_style(
1004                        ProgressStyle::with_template(
1005                            "[{elapsed_precise}] {wide_bar} Verifying snapshot contents against root state hash ({msg})",
1006                        )
1007                        .unwrap(),
1008                    ),
1009                );
1010                progress_bar.finish_with_message("Verification complete");
1011            }
1012            _ => return Err(anyhow!("Expected ECMHLiveObjectSetDigest")),
1013        };
1014    } else {
1015        m.println(
1016            "WARNING: Skipping snapshot verification! \
1017            This is highly discouraged unless you fully trust the source of this snapshot and its contents.
1018            If this was unintentional, rerun with `--verify` set to `normal` or `strict`.",
1019        )?;
1020    }
1021
1022    while !snapshot_done || !backfill_done {
1023        tokio::select! {
1024            result = &mut backfill_handle, if !backfill_done => {
1025                backfill_done = true;
1026                result.map_err(|error| anyhow!("Backfill task failed: {error}"))??;
1027            }
1028            result = &mut snapshot_handle, if !snapshot_done => {
1029                snapshot_done = true;
1030                result.map_err(|error| anyhow!("Snapshot task failed: {error}"))??;
1031            }
1032        }
1033    }
1034
1035    // TODO we should ensure this map is being updated for all end of epoch
1036    // checkpoints during summary sync. This happens in `insert_{verified|certified}_checkpoint`
1037    // in checkpoint store, but not in the corresponding functions in ObjectStore trait
1038    checkpoint_store.insert_epoch_last_checkpoint(epoch, &last_checkpoint)?;
1039
1040    setup_db_state(
1041        epoch,
1042        root_global_state_hash.clone(),
1043        perpetual_db.clone(),
1044        checkpoint_store.clone(),
1045        committee_store,
1046        network,
1047        verify == SnapshotVerifyMode::Strict,
1048        num_live_objects,
1049        m.clone(),
1050    )
1051    .await?;
1052
1053    // After a large backfill, rebuild the tidehunter control region to reclaim disk space
1054    // and reduce startup time. No-op when compiled without tidehunter.
1055    #[cfg(tidehunter)]
1056    {
1057        perpetual_db
1058            .force_rebuild_control_region()
1059            .expect("Failed to rebuild tidehunter control region after snapshot restore");
1060        // The tidehunter Db spawns background threads (periodic snapshot, relocator,
1061        // flusher pool, etc.) that own file handles inside the staging directory.
1062        // Wait for them to exit before renaming staging -> live, otherwise a
1063        // periodic timer could try to open a new file under the (now missing) path.
1064        println!(
1065            "Waiting for tidehunter background threads to finish before renaming staging to live"
1066        );
1067        perpetual_db.wait_for_tidehunter_background_threads();
1068        println!("Tidehunter background threads finished, proceeding with rename");
1069    }
1070
1071    let new_path = path.parent().unwrap().join("live");
1072    if new_path.exists() {
1073        fs::remove_dir_all(new_path.clone())?;
1074    }
1075    fs::rename(&path, &new_path)?;
1076    fs::remove_dir_all(snapshot_dir.clone())?;
1077    println!(
1078        "Successfully restored state from snapshot at end of epoch {}",
1079        epoch
1080    );
1081
1082    Ok(())
1083}
1084
1085async fn backfill_epoch_transaction_digests(
1086    perpetual_db: Arc<AuthorityPerpetualTables>,
1087    epoch: EpochId,
1088    ingestion_url: String,
1089    concurrency: usize,
1090    max_retries: usize,
1091    m: MultiProgress,
1092    end_of_epoch_checkpoint_seq_nums: Vec<u64>,
1093) -> Result<()> {
1094    if epoch == 0 {
1095        return Ok(());
1096    }
1097
1098    // Use end_of_epoch_checkpoint_seq_nums to get checkpoint ranges
1099    // we're backfilling up to the last checkpoint of the previous epoch
1100    // end_of_epoch_checkpoint_seq_nums[890] == end of epoch checkpoint for epoch_891
1101    // if restoring from epoch_891, we want to backfill checkpoints from end of epoch_890 to end of epoch_891
1102    // (when restoring from epoch_891, node will immediately start in epoch 892, so prev_epoch == epoch_891)
1103    let epoch_last_cp_seq = end_of_epoch_checkpoint_seq_nums
1104        .get(epoch as usize)
1105        .ok_or_else(|| anyhow!("No checkpoint sequence found for epoch {}", epoch))?;
1106
1107    let epoch_start_cp = if epoch == 0 {
1108        0
1109    } else {
1110        end_of_epoch_checkpoint_seq_nums
1111            .get(epoch as usize - 1)
1112            .map(|cp| cp + 1)
1113            .unwrap_or(0)
1114    };
1115    let msg = format!(
1116        "Beginning transaction digest backfill for epoch: {:?}, backfilling from: {:?}..{:?}",
1117        epoch, epoch_start_cp, epoch_last_cp_seq
1118    );
1119    m.println(&msg).ok();
1120    info!("{}", msg);
1121
1122    let checkpoints_to_fetch: Vec<_> = (epoch_start_cp..=*epoch_last_cp_seq).collect();
1123    let num_checkpoints = checkpoints_to_fetch.len();
1124
1125    let progress_bar = m.add(
1126        ProgressBar::new(num_checkpoints as u64).with_style(
1127            ProgressStyle::with_template(
1128                "[{elapsed_precise}] {wide_bar} {pos}/{len} transactions backfilled ({msg})",
1129            )
1130            .unwrap(),
1131        ),
1132    );
1133
1134    let client = build_object_store(&ingestion_url, vec![], vec![]);
1135    let checkpoint_counter = Arc::new(AtomicU64::new(0));
1136    let tx_counter = Arc::new(AtomicU64::new(0));
1137    let cloned_checkpoint_counter = checkpoint_counter.clone();
1138    let cloned_progress_bar = progress_bar.clone();
1139    let start_instant = Instant::now();
1140
1141    tokio::spawn(async move {
1142        loop {
1143            if cloned_progress_bar.is_finished() {
1144                break;
1145            }
1146            let num_checkpoints_processed = cloned_checkpoint_counter.load(Ordering::Relaxed);
1147            let elapsed = start_instant.elapsed().as_secs_f64();
1148            let chkpts_per_sec = if elapsed > 0.0 {
1149                num_checkpoints_processed as f64 / elapsed
1150            } else {
1151                0.0
1152            };
1153            cloned_progress_bar.set_position(num_checkpoints_processed);
1154            cloned_progress_bar.set_message(format!("{:.1} chkpts/sec", chkpts_per_sec));
1155            tokio::time::sleep(Duration::from_millis(100)).await;
1156        }
1157    });
1158
1159    futures::stream::iter(checkpoints_to_fetch)
1160        .map(|sq| {
1161            let client = client.clone();
1162            async move {
1163                // Retry with exponential backoff. This backfill runs concurrently with the
1164                // CPU-bound state accumulation; on a busy host the async runtime can be starved
1165                // long enough that a single checkpoint fetch times out, and the
1166                // `.try_for_each().await?` below then aborts the entire (multi-hour) snapshot
1167                // restore, discarding all progress. Retry (up to `max_retries` attempts) so the
1168                // fetch waits out the contention and completes instead.
1169                let attempts = AtomicUsize::new(0);
1170                backoff::future::retry_notify(
1171                    ExponentialBackoff {
1172                        max_interval: Duration::from_secs(30),
1173                        max_elapsed_time: None,
1174                        ..Default::default()
1175                    },
1176                    || async {
1177                        fetch_checkpoint(&client, sq).await.map_err(|e| {
1178                            if attempts.fetch_add(1, Ordering::Relaxed) + 1 >= max_retries {
1179                                backoff::Error::permanent(e)
1180                            } else {
1181                                backoff::Error::transient(e)
1182                            }
1183                        })
1184                    },
1185                    |e, delay: Duration| {
1186                        tracing::warn!(
1187                            "backfill: checkpoint {} fetch failed (attempt {}/{}): {}; retrying in {:?}",
1188                            sq,
1189                            attempts.load(Ordering::Relaxed),
1190                            max_retries,
1191                            e,
1192                            delay,
1193                        );
1194                    },
1195                )
1196                .await
1197                .map(|c| Arc::new(CheckpointData::from(c)))
1198            }
1199        })
1200        .buffer_unordered(concurrency)
1201        .try_for_each(|checkpoint| {
1202            let perpetual_db = perpetual_db.clone();
1203            let tx_counter = tx_counter.clone();
1204            let checkpoint_counter = checkpoint_counter.clone();
1205            let checkpoint_data = checkpoint;
1206
1207            async move {
1208                let tx_digests: Vec<_> = checkpoint_data
1209                    .transactions
1210                    .iter()
1211                    .map(|tx_data| *tx_data.transaction.digest())
1212                    .collect();
1213                let num_txs = tx_digests.len();
1214                perpetual_db
1215                    .insert_executed_transaction_digests_batch(epoch, tx_digests.into_iter())?;
1216                tx_counter.fetch_add(num_txs as u64, Ordering::Relaxed);
1217                checkpoint_counter.fetch_add(1, Ordering::Relaxed);
1218                Ok::<(), anyhow::Error>(())
1219            }
1220        })
1221        .await?;
1222
1223    let tx_count = tx_counter.load(Ordering::Relaxed);
1224    progress_bar.finish_with_message(format!(
1225        "Backfill complete: {} transactions from {} checkpoints",
1226        tx_count, num_checkpoints
1227    ));
1228    info!(
1229        "Backfill complete: {} transactions from {} checkpoints",
1230        tx_counter.load(Ordering::Relaxed),
1231        checkpoint_counter.load(Ordering::Relaxed)
1232    );
1233
1234    Ok(())
1235}