diff --git a/src/replication/config.rs b/src/replication/config.rs index c762cbdd..575008a7 100644 --- a/src/replication/config.rs +++ b/src/replication/config.rs @@ -187,9 +187,9 @@ pub const AUDIT_TICK_INTERVAL_MAX: Duration = Duration::from_secs(AUDIT_TICK_INT /// Sized to absorb worst-case global RTT for the audit envelope /// (the request + response messages are KB-scale, not chunk-scale) /// plus scheduling jitter. Tokyo↔NY round-trip is ~150ms each way, -/// so 2 seconds comfortably covers cross-continent communication +/// so 4 seconds comfortably covers cross-continent communication /// for the round-1 proof, whose payload is hashes (KB-scale). -const AUDIT_RESPONSE_FLOOR_SECS: u64 = 2; +const AUDIT_RESPONSE_FLOOR_SECS: u64 = 4; /// Floor on the round-2 BYTE-challenge deadline. /// @@ -197,11 +197,12 @@ const AUDIT_RESPONSE_FLOOR_SECS: u64 = 2; /// `MAX_BYTE_CHALLENGE_KEYS` full chunks (2 × 4 MiB = 8 MiB) back over the /// wire, so the envelope must also cover a cold QUIC handshake, the /// multi-MiB upload back to the auditor, and a busy honest peer's disk read. -/// The round-1 2 s floor (sized for a hashes-only reply) is too tight here — -/// the §4 finding. 5 s matches the cross-continent-RTT + handshake + 8 MiB -/// transfer budget while keeping a relay that must fetch the bytes over a -/// residential link outside it (the scaled term adds the per-byte estimate on -/// top). Mirrors main's more generous byte-round base. +/// The round-1 4 s floor is still sized for a hashes-only reply; round 2 needs +/// a larger base for the §4 byte-serving envelope. 5 s matches the +/// cross-continent-RTT + handshake + 8 MiB transfer budget while keeping a relay +/// that must fetch the bytes over a residential link outside it (the scaled +/// term adds the per-byte estimate on top). Mirrors main's more generous +/// byte-round base. const BYTE_AUDIT_RESPONSE_FLOOR_SECS: u64 = 5; /// Conservative honest-responder read throughput, in bytes per second. @@ -233,20 +234,6 @@ const AUDIT_HONEST_READ_BPS: u64 = 50 * 1024 * 1024; /// time (see the §7 note on `audit_response_timeout`). const AUDIT_RESPONSE_HONEST_MULTIPLIER: u64 = 5; -/// Single-key prune audit response deadline. -/// -/// Prune audits ask a peer whether they still hold one specific key -/// they previously claimed. The relay-defence rationale that motivates -/// the tight commitment-bound timeout does NOT apply here: the -/// auditor's own out-of-range hysteresis (`PRUNE_HYSTERESIS_DURATION`, -/// 3 days) already makes "fetch on demand" infeasible as a sustained -/// strategy. -/// -/// Sized to comfortably accommodate cold cross-continent QUIC -/// handshake plus scheduling jitter on a busy honest peer answering -/// a single-key challenge: 10 s. -const PRUNE_AUDIT_RESPONSE_SECS: u64 = 10; - /// Maximum duration a peer may claim bootstrap status before penalties apply. const BOOTSTRAP_CLAIM_GRACE_PERIOD_SECS: u64 = 24 * 60 * 60; // 24 h /// Maximum duration a peer may claim bootstrap status before penalties apply. @@ -403,11 +390,6 @@ pub struct ReplicationConfig { /// Slack multiplier on the honest-read estimate before /// declaring an audit timed out. pub audit_response_honest_multiplier: u64, - /// Single-key prune-audit response deadline. Has its own constant - /// because the relay-defence rationale that motivates the tight - /// commitment-bound budget does not apply to a single-key prune - /// challenge. - pub prune_audit_response_timeout: Duration, /// Maximum duration a peer may claim bootstrap status. pub bootstrap_claim_grace_period: Duration, /// Minimum continuous out-of-range duration before pruning a key. @@ -446,7 +428,6 @@ impl Default for ReplicationConfig { audit_response_floor: Duration::from_secs(AUDIT_RESPONSE_FLOOR_SECS), audit_honest_read_bps: AUDIT_HONEST_READ_BPS, audit_response_honest_multiplier: AUDIT_RESPONSE_HONEST_MULTIPLIER, - prune_audit_response_timeout: Duration::from_secs(PRUNE_AUDIT_RESPONSE_SECS), bootstrap_claim_grace_period: BOOTSTRAP_CLAIM_GRACE_PERIOD, prune_hysteresis_duration: PRUNE_HYSTERESIS_DURATION, verification_request_timeout: VERIFICATION_REQUEST_TIMEOUT, @@ -621,9 +602,9 @@ impl ReplicationConfig { // Resolve the scaled term in MILLISECONDS, not seconds: at the // byte-round sizes (MAX_BYTE_CHALLENGE_KEYS = 2 → 8 MiB) the per-second // quotient `multiplied / bps` integer-truncates to 0, leaving only the - // floor (the §4 finding: a 2×4 MiB honest serve under load could blow a - // 2 s budget). Computing in ms keeps the sub-second honest-read estimate - // (e.g. 8 MiB × 5 / 50 MB/s ≈ 840 ms) instead of dropping it. + // floor. The §4 finding was that byte-serving challenges need the + // sub-second honest-read estimate (e.g. 8 MiB × 5 / 50 MB/s ≈ 840 ms) + // instead of dropping it. let scaled_ms = multiplied.saturating_mul(1000) / bps; // saturating_add avoids a panic if the floor plus the scaled term would // overflow `Duration::MAX`. @@ -767,19 +748,19 @@ mod tests { // Scaling now resolves in MILLISECONDS so a sub-second honest read no // longer truncates to zero (§4). For k=1: - // (4_194_304 × 5 × 1000) / 52_428_800 = 400 ms, + 2 s round-1 floor = - // 2.4 s (previously collapsed to the bare 2 s floor). - assert_eq!(t1, Duration::from_millis(2400)); + // (4_194_304 × 5 × 1000) / 52_428_800 = 400 ms, + 4 s round-1 floor = + // 4.4 s. + assert_eq!(t1, Duration::from_millis(4400)); // For k=10: (10 × 4_194_304 × 5 × 1000) / 52_428_800 = 4000 ms scaled, - // + 2 s floor = 6 s. An HDD-backed honest peer at 20 MB/s reads 40 MiB + // + 4 s floor = 8 s. An HDD-backed honest peer at 20 MB/s reads 40 MiB // in ~2 s, comfortably inside; a relay fetching 40 MiB at 5 MB/s // residential bandwidth needs ~8 s for the data alone, outside. - assert_eq!(t10, Duration::from_secs(6)); + assert_eq!(t10, Duration::from_secs(8)); // For k=100: (100 × 4_194_304 × 5 × 1000) / 52_428_800 = 40_000 ms - // scaled, + 2 s floor = 42 s. - assert_eq!(t100, Duration::from_secs(42)); + // scaled, + 4 s floor = 44 s. + assert_eq!(t100, Duration::from_secs(44)); } #[test] @@ -790,7 +771,7 @@ mod tests { // well below modern HDDs which sustain 80-150 MB/s sequential) // reads 10 × 4 MiB = 40 MiB in ~2 s. Add 300 ms cross-continent // RTT, ~10 ms scheduling, ~3 ms ML-DSA sign, and the honest - // envelope is ~2.3 s. The 6 s budget at k=10 leaves >3 s of + // envelope is ~2.3 s. The 8 s budget at k=10 leaves >5 s of // slack. let config = ReplicationConfig::default(); let budget = config.audit_response_timeout(10); @@ -810,7 +791,7 @@ mod tests { // read budget fits inside `audit_response_timeout(k)`, while a // relay attacker fetching k*4MiB over residential bandwidth // (≈ 5 MB/s realistic for sustained download) does NOT. Spot- - // check this at k=100: honest budget is 42s, relay needs at + // check this at k=100: honest budget is 44s, relay needs at // least 100 * 4 MiB / 5 MB/s = 80s for the data alone, which // exceeds the budget. let config = ReplicationConfig::default(); diff --git a/src/replication/pruning.rs b/src/replication/pruning.rs index e1e0d3f1..79db6c9a 100644 --- a/src/replication/pruning.rs +++ b/src/replication/pruning.rs @@ -1151,8 +1151,9 @@ async fn peer_proves_record( let mut rng = rand::thread_rng(); (rng.gen::(), rng.gen::<[u8; 32]>()) }; - let encoded = encode_prune_audit_challenge(&peer, key, challenge_id, nonce)?; - let Some(decoded) = send_prune_audit_challenge(&peer, &key, encoded, p2p_node, config).await + let (encoded, key_count) = encode_prune_audit_challenge(&peer, key, challenge_id, nonce)?; + let Some(decoded) = + send_prune_audit_challenge(&peer, &key, encoded, key_count, p2p_node, config).await else { // No decoded response means a timeout or malformed reply. Prune // confirmation reuses `AuditChallenge` semantics, so this is an immediate @@ -1195,13 +1196,14 @@ fn encode_prune_audit_challenge( key: XorName, challenge_id: u64, nonce: [u8; 32], -) -> Option> { +) -> Option<(Vec, usize)> { let challenge = AuditChallenge { challenge_id, nonce, challenged_peer_id: *peer.as_bytes(), keys: vec![key], }; + let key_count = challenge.keys.len(); let msg = ReplicationMessage { request_id: challenge_id, body: ReplicationMessageBody::AuditChallenge(challenge), @@ -1216,23 +1218,20 @@ fn encode_prune_audit_challenge( return None; } }; - Some(encoded) + Some((encoded, key_count)) } async fn send_prune_audit_challenge( peer: &PeerId, key: &XorName, encoded: Vec, + key_count: usize, p2p_node: &Arc, config: &ReplicationConfig, ) -> Option { + let timeout = config.audit_response_timeout(key_count); let response = match p2p_node - .send_request( - peer, - REPLICATION_PROTOCOL_ID, - encoded, - config.prune_audit_response_timeout, - ) + .send_request(peer, REPLICATION_PROTOCOL_ID, encoded, timeout) .await { Ok(response) => response,