From 1a7094c23c613b1c2a671ba38f7f2e9fcc23b9da Mon Sep 17 00:00:00 2001 From: Ilia Churin Date: Mon, 1 Jun 2026 19:34:27 +0900 Subject: [PATCH 1/2] fix(fs-client): hard-fail on provider CID mismatch instead of warn `create_drive` previously logged a `tracing::warn!` and continued when the provider's returned data_root disagreed with the locally-computed CID, defeating content-addressing's only integrity guarantee. Now returns a new `FsClientError::CidMismatch` and refuses to cache or proceed. Adds unit coverage for both the matching and mismatching path via a small `verify_cid` helper. --- .../file-system/client/src/lib.rs | 44 ++++++++++++++++--- 1 file changed, 39 insertions(+), 5 deletions(-) diff --git a/storage-interfaces/file-system/client/src/lib.rs b/storage-interfaces/file-system/client/src/lib.rs index 010051dd..612f5138 100644 --- a/storage-interfaces/file-system/client/src/lib.rs +++ b/storage-interfaces/file-system/client/src/lib.rs @@ -101,6 +101,9 @@ pub enum FsClientError { #[error("Configuration error: {0}")] Config(String), + + #[error("CID mismatch: expected {expected:?}, got {got:?}")] + CidMismatch { expected: Cid, got: Cid }, } pub type Result = std::result::Result; @@ -218,11 +221,10 @@ impl FileSystemClient { let root_dir_bytes = root_dir.to_scale_bytes(); let root_cid = self.upload_blob(bucket_id, &root_dir_bytes).await?; - // Verify the CID matches what we would compute locally - let expected_cid = compute_cid(&root_dir_bytes); - if root_cid != expected_cid { - tracing::warn!("CID mismatch: data_root={root_cid:?}, expected={expected_cid:?}"); - } + // Verify the provider returned the CID we expect for these bytes. + // A mismatch means the provider's content-addressing disagrees with ours + // (corruption, tampering, or hash-algo drift) — refuse to continue. + Self::verify_cid(compute_cid(&root_dir_bytes), root_cid)?; // Cache the root CID (now managed off-chain only) tracing::debug!("create_drive: caching root_cid={root_cid:?} for drive {drive_id}"); @@ -807,6 +809,16 @@ impl FileSystemClient { Ok(data) } + /// Compare a locally-computed CID against the CID a provider returned for + /// the same bytes. Returns `CidMismatch` on disagreement so callers can + /// refuse to trust the provider's response. + fn verify_cid(expected: Cid, got: Cid) -> Result<()> { + if expected != got { + return Err(FsClientError::CidMismatch { expected, got }); + } + Ok(()) + } + /// Split a path into (parent_path, name) fn split_path(path: &str) -> Result<(&str, &str)> { if !path.starts_with('/') { @@ -984,4 +996,26 @@ mod tests { assert!(FileSystemClient::split_path("/").is_err()); assert!(FileSystemClient::split_path("no-slash").is_err()); } + + #[test] + fn verify_cid_accepts_matching_pair() { + let cid = compute_cid(b"hello world"); + FileSystemClient::verify_cid(cid, cid).expect("matching CIDs must be accepted"); + } + + #[test] + fn verify_cid_rejects_provider_returning_different_blob() { + let uploaded = compute_cid(b"the bytes we uploaded"); + let returned = compute_cid(b"a different blob the provider gave back"); + assert_ne!(uploaded, returned, "test setup: CIDs must differ"); + let err = FileSystemClient::verify_cid(uploaded, returned) + .expect_err("mismatched CIDs must be rejected"); + match err { + FsClientError::CidMismatch { expected, got } => { + assert_eq!(expected, uploaded); + assert_eq!(got, returned); + } + other => panic!("expected CidMismatch, got {other:?}"), + } + } } From 6b05887cb123bd4d405aef95b6489fe7df89cedd Mon Sep 17 00:00:00 2001 From: Ilia Churin Date: Wed, 3 Jun 2026 17:58:29 +0900 Subject: [PATCH 2/2] =?UTF-8?q?refactor(fs-client):=20rename=20verify=5Fci?= =?UTF-8?q?d=20=E2=86=92=20ensure=5Fcid=5Fmatches?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Match FRAME's `ensure!`-style naming convention for predicate helpers that return `Err` on a failed invariant rather than performing verification with side effects. No behavioural change. --- storage-interfaces/file-system/client/src/lib.rs | 14 +++++++------- 1 file changed, 7 insertions(+), 7 deletions(-) diff --git a/storage-interfaces/file-system/client/src/lib.rs b/storage-interfaces/file-system/client/src/lib.rs index 612f5138..057136b8 100644 --- a/storage-interfaces/file-system/client/src/lib.rs +++ b/storage-interfaces/file-system/client/src/lib.rs @@ -224,7 +224,7 @@ impl FileSystemClient { // Verify the provider returned the CID we expect for these bytes. // A mismatch means the provider's content-addressing disagrees with ours // (corruption, tampering, or hash-algo drift) — refuse to continue. - Self::verify_cid(compute_cid(&root_dir_bytes), root_cid)?; + Self::ensure_cid_matches(compute_cid(&root_dir_bytes), root_cid)?; // Cache the root CID (now managed off-chain only) tracing::debug!("create_drive: caching root_cid={root_cid:?} for drive {drive_id}"); @@ -809,10 +809,10 @@ impl FileSystemClient { Ok(data) } - /// Compare a locally-computed CID against the CID a provider returned for + /// Ensure a locally-computed CID matches the CID a provider returned for /// the same bytes. Returns `CidMismatch` on disagreement so callers can /// refuse to trust the provider's response. - fn verify_cid(expected: Cid, got: Cid) -> Result<()> { + fn ensure_cid_matches(expected: Cid, got: Cid) -> Result<()> { if expected != got { return Err(FsClientError::CidMismatch { expected, got }); } @@ -998,17 +998,17 @@ mod tests { } #[test] - fn verify_cid_accepts_matching_pair() { + fn ensure_cid_matches_accepts_matching_pair() { let cid = compute_cid(b"hello world"); - FileSystemClient::verify_cid(cid, cid).expect("matching CIDs must be accepted"); + FileSystemClient::ensure_cid_matches(cid, cid).expect("matching CIDs must be accepted"); } #[test] - fn verify_cid_rejects_provider_returning_different_blob() { + fn ensure_cid_matches_rejects_provider_returning_different_blob() { let uploaded = compute_cid(b"the bytes we uploaded"); let returned = compute_cid(b"a different blob the provider gave back"); assert_ne!(uploaded, returned, "test setup: CIDs must differ"); - let err = FileSystemClient::verify_cid(uploaded, returned) + let err = FileSystemClient::ensure_cid_matches(uploaded, returned) .expect_err("mismatched CIDs must be rejected"); match err { FsClientError::CidMismatch { expected, got } => {