Protocol v2: trusted-peer push, truthful partials, split downloads

Three capabilities the iroh-blobs 0.103 line makes possible:

- Push (new API surface, protocol v2): `Push { hash, node_id }` hands
  fully-present content to a trusted peer, unprompted. Consent is
  mutual — the sender pushes only to peers it trusts, and the receiver
  (which now accepts connections unconditionally and gates per request)
  admits pushes only from peers *it* trusts, deferring with RateLimited
  while metered. An accepted push is pinned by the receiver (default
  policy, format inferred from the request ranges) once its transfer
  completes, and surfaces as a PushReceived event. Because QUIC writes
  are fire-and-forget, the sender confirms delivery by observing the
  receiver's bitfields (root + last hashseq child) before replying —
  Pushed { bytes } means verified received, not merely sent. New
  varde-ctl `push` command.

- Truthful partial presence: Status/List report bytes actually present
  and verified for partial blobs, from the store's bitfields via
  observe, instead of the old "0 until complete".

- Split downloads: multi-provider fetches stripe one request across
  providers (SplitStrategy::Split) instead of trying them serially.

Trusted peers may observe bitfields even when serving is disabled or
metered — bitfields are metadata, and push confirmation rides on them.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
This commit is contained in:
2026-08-16 14:52:27 +02:00
parent ad69f7d884
commit 4033c0ffab
9 changed files with 474 additions and 33 deletions
+134
View File
@@ -280,3 +280,137 @@ fn overlapping_pins_auto_sync_via_lan_discovery() {
assert!(reply.ok, "materialize failed: {:?}", reply.error);
assert_eq!(std::fs::read(&dest).unwrap(), payload);
}
/// Wait until `client` sees at least one connected trusted peer; false
/// if the deadline passes (multicast unavailable).
fn wait_peer_connected(client: &mut support::Client, timeout: Duration) -> bool {
let deadline = Instant::now() + timeout;
while Instant::now() < deadline {
let reply = client.request(&Request::PeerList {});
if let Some(ResponseData::Peers { peers }) = reply.data {
if peers.iter().any(|p| p.connected) {
return true;
}
}
std::thread::sleep(Duration::from_millis(250));
}
false
}
/// Push: A hands pinned content to B unprompted. Requires mutual trust
/// and mdns (skips without multicast). The receiver pins what it
/// accepted, so the content survives gc and lists as a pin.
#[test]
fn push_hands_content_to_trusted_peer() {
let dir_a = tempfile::tempdir().unwrap();
let dir_b = tempfile::tempdir().unwrap();
let a = support::spawn_daemon_with_env(dir_a.path(), &[("VARDE_DISCOVERY", "true")]);
let b = support::spawn_daemon_with_env(dir_b.path(), &[("VARDE_DISCOVERY", "true")]);
let mut client_a = support::Client::connect(&a.socket);
let mut client_b = support::Client::connect(&b.socket);
let id_a = node_id(&mut client_a);
let id_b = node_id(&mut client_b);
let dir = tempfile::tempdir().unwrap();
let src = dir.path().join("gift.bin");
let payload = vec![3u8; 300_000];
std::fs::write(&src, &payload).unwrap();
let hash = add_file(&mut client_a, &src);
pin(&mut client_a, &hash, false);
// Sender-side consent: pushing to a peer we don't trust is refused
// locally, before any connection is made.
let reply = client_a.request(&Request::Push {
hash: hash.clone(),
node_id: id_b.clone(),
});
assert!(!reply.ok, "push to untrusted peer must be refused");
// Mutual trust, established before any blob connection exists.
assert!(client_a.request(&Request::PeerTrust { node_id: id_b }).ok);
assert!(client_b.request(&Request::PeerTrust { node_id: id_a }).ok);
if !wait_peer_connected(&mut client_a, Duration::from_secs(30)) {
eprintln!("SKIP: mdns discovery saw no peers (multicast unavailable in this environment)");
return;
}
let reply = client_a.request(&Request::Push {
hash: hash.clone(),
node_id: node_id(&mut client_b),
});
assert!(reply.ok, "push failed: {:?}", reply.error);
match reply.data {
Some(ResponseData::Pushed { bytes, .. }) => {
assert_eq!(bytes, payload.len() as u64, "push moved the payload")
}
other => panic!("unexpected push reply: {other:?}"),
}
// The receiver has the bytes, and pinned them.
if !wait_complete(&b.socket, &hash, Duration::from_secs(30)) {
let reply = client_b.request(&Request::Status {
hash: Some(hash.clone()),
});
panic!("pushed content not complete on receiver; status: {reply:?}");
}
let deadline = Instant::now() + Duration::from_secs(10);
loop {
let reply = client_b.request(&Request::List {});
if let Some(ResponseData::Pins { pins }) = &reply.data {
if pins.iter().any(|p| p.hash == hash && p.complete) {
break;
}
}
assert!(
Instant::now() < deadline,
"receiver did not pin pushed content"
);
std::thread::sleep(Duration::from_millis(250));
}
let dest = dir.path().join("gift-out.bin");
let reply = client_b.request(&Request::Materialize {
hash,
dest: dest.display().to_string(),
mode: MaterializeMode::Copy,
});
assert!(reply.ok, "materialize failed: {:?}", reply.error);
assert_eq!(std::fs::read(&dest).unwrap(), payload);
}
/// Consent is mutual: a receiver that does not trust the sender rejects
/// the push, even though the sender trusts the receiver.
#[test]
fn push_rejected_without_receiver_trust() {
let dir_a = tempfile::tempdir().unwrap();
let dir_b = tempfile::tempdir().unwrap();
let a = support::spawn_daemon_with_env(dir_a.path(), &[("VARDE_DISCOVERY", "true")]);
let b = support::spawn_daemon_with_env(dir_b.path(), &[("VARDE_DISCOVERY", "true")]);
let mut client_a = support::Client::connect(&a.socket);
let mut client_b = support::Client::connect(&b.socket);
let id_b = node_id(&mut client_b);
let dir = tempfile::tempdir().unwrap();
let src = dir.path().join("unwanted.bin");
std::fs::write(&src, vec![5u8; 100_000]).unwrap();
let hash = add_file(&mut client_a, &src);
pin(&mut client_a, &hash, false);
// One-way trust only: A trusts B, B has never heard of A.
assert!(client_a.request(&Request::PeerTrust { node_id: id_b }).ok);
if !wait_peer_connected(&mut client_a, Duration::from_secs(30)) {
eprintln!("SKIP: mdns discovery saw no peers (multicast unavailable in this environment)");
return;
}
let reply = client_a.request(&Request::Push {
hash: hash.clone(),
node_id: node_id(&mut client_b),
});
assert!(!reply.ok, "receiver without trust must reject the push");
// And the receiver holds none of it.
assert!(!is_complete(&b.socket, &hash));
}