Skip to content
This repository has been archived by the owner on Jan 11, 2024. It is now read-only.

Commit

Permalink
FM-362: Verify snapshot checksum
Browse files Browse the repository at this point in the history
  • Loading branch information
aakoshh committed Nov 21, 2023
1 parent 7216f53 commit a7770d7
Show file tree
Hide file tree
Showing 6 changed files with 119 additions and 34 deletions.
9 changes: 9 additions & 0 deletions fendermint/app/src/app.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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,
Expand Down
1 change: 1 addition & 0 deletions fendermint/vm/snapshot/src/car/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -96,6 +96,7 @@ mod tests {
.collect::<Result<Vec<_>, _>>()
.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
Expand Down
21 changes: 19 additions & 2 deletions fendermint/vm/snapshot/src/client.rs
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,7 @@ use fendermint_vm_interpreter::fvm::state::{
use tempfile::tempdir;

use crate::{
manifest,
state::{SnapshotDownload, SnapshotState},
SnapshotError, SnapshotItem, SnapshotManifest,
};
Expand Down Expand Up @@ -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)
}
Expand Down
2 changes: 2 additions & 0 deletions fendermint/vm/snapshot/src/error.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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),
}
45 changes: 13 additions & 32 deletions fendermint/vm/snapshot/src/manager.rs
Original file line number Diff line number Diff line change
@@ -1,18 +1,17 @@
// 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;
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.
Expand Down Expand Up @@ -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.
Expand Down Expand Up @@ -261,15 +260,6 @@ where
}
}

/// Create a Sha256 checksum of a file.
fn checksum(path: impl AsRef<Path>) -> anyhow::Result<tendermint::Hash> {
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<C>(client: C, is_syncing: TVar<bool>, poll_interval: Duration)
where
Expand Down Expand Up @@ -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::{
Expand All @@ -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]
Expand Down Expand Up @@ -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,
Expand Down
75 changes: 75 additions & 0 deletions fendermint/vm/snapshot/src/manifest.rs
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@ use fendermint_vm_interpreter::fvm::state::{
FvmStateParams,
};
use serde::{Deserialize, Serialize};
use sha2::{Digest, Sha256};

use crate::SnapshotItem;

Expand Down Expand Up @@ -101,6 +102,55 @@ pub fn list_manifests(snapshot_dir: impl AsRef<Path>) -> anyhow::Result<Vec<Snap
Ok(items)
}

/// Calculate the Sha256 checksum of a file.
pub fn file_checksum(path: impl AsRef<Path>) -> anyhow::Result<tendermint::Hash> {
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<Path>) -> anyhow::Result<tendermint::Hash> {
let mut hasher = Sha256::new();

let mut chunks = std::fs::read_dir(path.as_ref())
.unwrap()
.collect::<Result<Vec<_>, _>>()
.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::<u32>()
.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 {

Expand Down Expand Up @@ -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)
}
}

0 comments on commit a7770d7

Please sign in to comment.