diff --git a/Cargo.lock b/Cargo.lock index fe2dbf8..e907980 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -2266,7 +2266,7 @@ dependencies = [ [[package]] name = "dig-node-service" -version = "0.72.4" +version = "0.72.5" dependencies = [ "async-trait", "axum", diff --git a/Cargo.toml b/Cargo.toml index 2f16896..1a2a9b6 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -31,7 +31,7 @@ edition = "2021" # the ROOT manifest (`[workspace.package].version`), so it MUST be set here for a # release to fire (§3.6). The library crates (dig-node-core/dig-runtime/dig-wallet) # keep their own independent versions — only the released binary tracks the workspace version. -version = "0.72.4" +version = "0.72.5" # Release hardening, matching digstore: keep integer-overflow checks ON in release. # The node parses untrusted serialized input and does offset/length arithmetic over diff --git a/crates/dig-node-core/src/download.rs b/crates/dig-node-core/src/download.rs index e1235da..83daa63 100644 --- a/crates/dig-node-core/src/download.rs +++ b/crates/dig-node-core/src/download.rs @@ -591,6 +591,25 @@ impl StateStore for CapturingStateStore { // Keep the captured commitment (do NOT drop it on clear) — clear only the on-disk checkpoint. self.inner.clear(key).await } + + // Bad-descriptor reputation (#1611) must reach the inner file store, NOT the trait's forgetful + // no-op defaults: a holder demoted for serving a lying descriptor stays demoted across a restart, + // so a later call/process never re-pays for the same lie. This wrapper only adds commitment + // capture — it delegates reputation verbatim. + async fn record_bad_descriptor( + &self, + target_key: &str, + peer_id: &str, + ) -> Result<(), dig_download::DownloadError> { + self.inner.record_bad_descriptor(target_key, peer_id).await + } + + async fn bad_descriptor_peers( + &self, + target_key: &str, + ) -> Result, dig_download::DownloadError> { + self.inner.bad_descriptor_peers(target_key).await + } } /// A [`RangeTransport`] wrapper that BYPASSES the `getAvailability` confirm probe for a holder the node @@ -3112,4 +3131,30 @@ pub(crate) mod tests { "DIG_NODE_BACKFILL_ON_MISS=off must refuse even a Local-origin read" ); } + + #[tokio::test] + async fn capturing_state_store_persists_bad_descriptor_reputation_across_restart() { + // #1629: `CapturingStateStore` wraps `FileStateStore` but must DELEGATE the bad-descriptor + // reputation methods, not inherit the trait's forgetful no-op defaults — otherwise a holder + // that served a lying descriptor is re-asked from scratch after every restart, paying the + // same wasted pull attempts again (#1611). Record a verdict through one wrapper, then read it + // back through a FRESH wrapper over the SAME on-disk store (a simulated process restart): the + // verdict must survive. Without delegation this returns empty, because the record went to the + // no-op default and never reached the file store. + let dir = tempfile::tempdir().unwrap(); + let state_dir = dir.path().join("state"); + let target = "a".repeat(64); + let peer = "b".repeat(64); + + let before = CapturingStateStore::new(FileStateStore::new(state_dir.clone())); + before.record_bad_descriptor(&target, &peer).await.unwrap(); + + let after = CapturingStateStore::new(FileStateStore::new(state_dir)); + let peers = after.bad_descriptor_peers(&target).await.unwrap(); + assert_eq!( + peers, + vec![peer], + "a recorded bad-descriptor verdict must persist across a restart" + ); + } }