From a7770d74635af355e3c2829a758ddcc44330cd29 Mon Sep 17 00:00:00 2001 From: Akosh Farkash Date: Tue, 21 Nov 2023 15:31:38 +0000 Subject: [PATCH] FM-362: Verify snapshot checksum --- fendermint/app/src/app.rs | 9 ++++ fendermint/vm/snapshot/src/car/mod.rs | 1 + fendermint/vm/snapshot/src/client.rs | 21 +++++++- fendermint/vm/snapshot/src/error.rs | 2 + fendermint/vm/snapshot/src/manager.rs | 45 +++++----------- fendermint/vm/snapshot/src/manifest.rs | 75 ++++++++++++++++++++++++++ 6 files changed, 119 insertions(+), 34 deletions(-) diff --git a/fendermint/app/src/app.rs b/fendermint/app/src/app.rs index 25bfacbc..8c573845 100644 --- a/fendermint/app/src/app.rs +++ b/fendermint/app/src/app.rs @@ -891,6 +891,15 @@ where ..default }); } + Err(SnapshotError::WrongChecksum(expected, got)) => { + tracing::warn!(?got, ?expected, "wrong snapshot checksum"); + // We could retry this snapshot, or try another one. + // If we retry, we have to tell which chunks to refetch. + return Ok(response::ApplySnapshotChunk { + result: response::ApplySnapshotChunkResult::RejectSnapshot, + ..default + }); + } Err(e) => { tracing::error!( chunk = request.index, diff --git a/fendermint/vm/snapshot/src/car/mod.rs b/fendermint/vm/snapshot/src/car/mod.rs index dff681c3..c1c38fed 100644 --- a/fendermint/vm/snapshot/src/car/mod.rs +++ b/fendermint/vm/snapshot/src/car/mod.rs @@ -96,6 +96,7 @@ mod tests { .collect::, _>>() .unwrap(); + // There are few enough that we can get away without converting to an integer. chunks.sort_unstable_by_key(|c| c.path().to_string_lossy().to_string()); let chunks = chunks diff --git a/fendermint/vm/snapshot/src/client.rs b/fendermint/vm/snapshot/src/client.rs index 6db2d4b2..5f819ac0 100644 --- a/fendermint/vm/snapshot/src/client.rs +++ b/fendermint/vm/snapshot/src/client.rs @@ -11,6 +11,7 @@ use fendermint_vm_interpreter::fvm::state::{ use tempfile::tempdir; use crate::{ + manifest, state::{SnapshotDownload, SnapshotState}, SnapshotError, SnapshotItem, SnapshotManifest, }; @@ -114,8 +115,24 @@ impl SnapshotClient { cd.next_index.write(next_index)?; if next_index == cd.manifest.chunks { - // TODO: Verify the checksum then load the snapshot and remove the current download from memory. - Ok(true) + // Verify the checksum then load the snapshot and remove the current download from memory. + match manifest::parts_checksum(cd.download_dir.as_ref()) { + Ok(checksum) => { + if checksum == cd.manifest.checksum { + // TODO: Import Snapshot. + Ok(true) + } else { + abort(SnapshotError::WrongChecksum( + cd.manifest.checksum, + checksum, + )) + } + } + Err(e) => abort(SnapshotError::IoError(std::io::Error::new( + std::io::ErrorKind::Other, + e.to_string(), + ))), + } } else { Ok(false) } diff --git a/fendermint/vm/snapshot/src/error.rs b/fendermint/vm/snapshot/src/error.rs index cd4e1aa0..7b6e7af2 100644 --- a/fendermint/vm/snapshot/src/error.rs +++ b/fendermint/vm/snapshot/src/error.rs @@ -14,4 +14,6 @@ pub enum SnapshotError { NoDownload, #[error("unexpected chunk index; expected {0}, got {1}")] UnexpectedChunk(u32, u32), + #[error("wrong checksum; expected {0}, got {1}")] + WrongChecksum(tendermint::Hash, tendermint::Hash), } diff --git a/fendermint/vm/snapshot/src/manager.rs b/fendermint/vm/snapshot/src/manager.rs index 33e3e3d7..1135bb7b 100644 --- a/fendermint/vm/snapshot/src/manager.rs +++ b/fendermint/vm/snapshot/src/manager.rs @@ -1,10 +1,10 @@ // Copyright 2022-2023 Protocol Labs // SPDX-License-Identifier: Apache-2.0, MIT -use std::path::{Path, PathBuf}; +use std::path::PathBuf; use std::time::Duration; -use crate::manifest::{list_manifests, write_manifest, SnapshotManifest}; +use crate::manifest::{file_checksum, list_manifests, write_manifest, SnapshotManifest}; use crate::state::SnapshotState; use crate::{car, SnapshotClient, SnapshotItem}; use anyhow::Context; @@ -12,7 +12,6 @@ use async_stm::{atomically, retry, TVar}; use fendermint_vm_interpreter::fvm::state::snapshot::{BlockHeight, Snapshot}; use fendermint_vm_interpreter::fvm::state::FvmStateParams; use fvm_ipld_blockstore::Blockstore; -use sha2::{Digest, Sha256}; use tendermint_rpc::Client; /// The file name to export the CAR to. @@ -222,7 +221,7 @@ where .len() as usize; // Create a checksum over the CAR file. - let checksum_bytes = checksum(&snapshot_path).context("failed to compute checksum")?; + let checksum_bytes = file_checksum(&snapshot_path).context("failed to compute checksum")?; std::fs::write(&checksum_path, checksum_bytes).context("failed to write checksum file")?; // Create a directory for the parts. @@ -261,15 +260,6 @@ where } } -/// Create a Sha256 checksum of a file. -fn checksum(path: impl AsRef) -> anyhow::Result { - let mut file = std::fs::File::open(&path)?; - let mut hasher = Sha256::new(); - let _ = std::io::copy(&mut file, &mut hasher)?; - let hash = hasher.finalize().into(); - Ok(tendermint::Hash::Sha256(hash)) -} - /// Periodically ask CometBFT if it has caught up with the chain. async fn poll_sync_status(client: C, is_syncing: TVar, poll_interval: Duration) where @@ -298,10 +288,9 @@ where #[cfg(test)] mod tests { - use std::{io::Write, sync::Arc, time::Duration}; + use std::{sync::Arc, time::Duration}; use async_stm::{atomically, retry}; - use cid::multihash::MultihashDigest; use fendermint_vm_genesis::Genesis; use fendermint_vm_interpreter::{ fvm::{ @@ -314,26 +303,10 @@ mod tests { }; use fvm::engine::MultiEngine; use quickcheck::Arbitrary; - use tempfile::NamedTempFile; use crate::manifest; - use super::{checksum, SnapshotManager}; - - #[test] - fn file_checksum() { - let content = b"Hello Checksum!"; - - let mut file = NamedTempFile::new().expect("new temp file"); - file.write_all(content).expect("write contents"); - let file_path = file.into_temp_path(); - let file_digest = checksum(file_path).expect("checksum"); - - let content_digest = cid::multihash::Code::Sha2_256.digest(content); - let content_digest = content_digest.digest(); - - assert_eq!(file_digest.as_bytes(), content_digest) - } + use super::SnapshotManager; // Initialise genesis and export it directly to see if it works. #[tokio::test] @@ -415,6 +388,14 @@ mod tests { assert_eq!(snapshots.len(), 1, "can list manifests"); assert_eq!(snapshots[0], snapshot); + let checksum = manifest::parts_checksum(snapshot.snapshot_dir.as_path().join("parts")) + .expect("parts checksum can be calculated"); + + assert_eq!( + checksum, snapshot.manifest.checksum, + "checksum should match" + ); + // Create a new manager instance let (_, new_client) = SnapshotManager::new( store, diff --git a/fendermint/vm/snapshot/src/manifest.rs b/fendermint/vm/snapshot/src/manifest.rs index 15298365..6d8bf722 100644 --- a/fendermint/vm/snapshot/src/manifest.rs +++ b/fendermint/vm/snapshot/src/manifest.rs @@ -9,6 +9,7 @@ use fendermint_vm_interpreter::fvm::state::{ FvmStateParams, }; use serde::{Deserialize, Serialize}; +use sha2::{Digest, Sha256}; use crate::SnapshotItem; @@ -101,6 +102,55 @@ pub fn list_manifests(snapshot_dir: impl AsRef) -> anyhow::Result) -> anyhow::Result { + let mut file = std::fs::File::open(&path)?; + let mut hasher = Sha256::new(); + let _ = std::io::copy(&mut file, &mut hasher)?; + let hash = hasher.finalize().into(); + Ok(tendermint::Hash::Sha256(hash)) +} + +/// Calculate the Sha256 checksum of all `{idx}.part` files in a directory. +pub fn parts_checksum(path: impl AsRef) -> anyhow::Result { + let mut hasher = Sha256::new(); + + let mut chunks = std::fs::read_dir(path.as_ref()) + .unwrap() + .collect::, _>>() + .with_context(|| { + format!( + "failed to collect parts in directory: {}", + path.as_ref().to_string_lossy() + ) + })?; + + chunks.retain(|item| { + item.path() + .extension() + .map(|x| x.to_string_lossy().to_string()) + .unwrap_or_default() + == "part" + }); + + chunks.sort_by_cached_key(|item| { + item.path() + .file_stem() + .map(|n| n.to_string_lossy()) + .unwrap_or_default() + .parse::() + .expect("file part names are prefixed by index") + }); + + for entry in chunks { + let mut file = std::fs::File::open(&entry.path()).context("failed to open part")?; + let _ = std::io::copy(&mut file, &mut hasher)?; + } + + let hash = hasher.finalize().into(); + Ok(tendermint::Hash::Sha256(hash)) +} + #[cfg(feature = "arb")] mod arb { @@ -141,3 +191,28 @@ mod arb { } } } + +#[cfg(test)] +mod tests { + use std::io::Write; + + use cid::multihash::MultihashDigest; + use tempfile::NamedTempFile; + + use crate::manifest::file_checksum; + + #[test] + fn test_file_checksum() { + let content = b"Hello Checksum!"; + + let mut file = NamedTempFile::new().expect("new temp file"); + file.write_all(content).expect("write contents"); + let file_path = file.into_temp_path(); + let file_digest = file_checksum(file_path).expect("checksum"); + + let content_digest = cid::multihash::Code::Sha2_256.digest(content); + let content_digest = content_digest.digest(); + + assert_eq!(file_digest.as_bytes(), content_digest) + } +}