use std::sync::Arc;
use tokio::task::JoinHandle;
use crate::artifact_store::{ArtifactStore, ArtifactType};
use crate::chain::ChainManager;
use crate::mesh_artifact::{ArtifactExchange, ExchangeConfig};
use crate::chain::{EVENT_KIND_ARTIFACT_FETCH, EVENT_KIND_ARTIFACT_SERVE};
use crate::mesh::MeshStream;
use crate::mesh_artifact_tests::{
Node, anchors_for, connect, events, key, link, node, signed_package,
};
use crate::mesh_artifact_transfer::{FetchError, PeerLink, PeerSet};
use crate::mesh_artifact_wire::{ArtifactDescriptor, ArtifactKey, ArtifactMsg, Bitfield};
use crate::mesh_test_support::{InMemoryStream, connected_pair};
use crate::workload_pkg::codec::{hex_decode_exact, hex_encode};
const JUNK: [u8; 1024] = [0xAA; 1024];
async fn spawn_rogue(
d: ArtifactDescriptor,
pieces: Vec<Vec<u8>>,
) -> (InMemoryStream, JoinHandle<()>) {
let (client, mut srv) = connected_pair().await.unwrap();
let task = tokio::spawn(async move {
let id = d.id();
let tx = |m: ArtifactMsg| m.to_wire().unwrap();
while let Ok(raw) = srv.recv().await {
let reply = match ArtifactMsg::from_wire(&raw) {
Ok(ArtifactMsg::MetaRequest { .. }) => {
let mut have = Bitfield::new(d.piece_count());
(0..d.piece_count()).for_each(|i| have.set(i, true));
vec![
tx(ArtifactMsg::Meta {
descriptor: d.clone(),
}),
tx(ArtifactMsg::Announce { id, have }),
]
}
Ok(ArtifactMsg::Request { pieces: want, .. }) => want
.into_iter()
.map(|index| {
tx(ArtifactMsg::Piece {
id,
index,
offset: 0,
data: pieces[index as usize].clone(),
})
})
.collect(),
_ => vec![],
};
for frame in reply {
if srv.send(&frame).await.is_err() {
return;
}
}
}
});
(client, task)
}
async fn spawn_liar(claimed: [u8; 32]) -> (InMemoryStream, JoinHandle<()>) {
let d = ArtifactDescriptor {
piece_size: 1024,
total_size: JUNK.len() as u64,
content_hash: claimed,
pieces: vec![*blake3::hash(&JUNK).as_bytes()],
};
spawn_rogue(d, vec![JUNK.to_vec()]).await
}
fn exchange_over(id: &str, store: &Arc<ArtifactStore>) -> ArtifactExchange {
let cfg = ExchangeConfig {
piece_size: 1024 * 1024,
block_size: 256 * 1024,
..Default::default()
};
let mut ex = ArtifactExchange::new(id, store.clone(), cfg).unwrap();
ex.set_chain_manager(Arc::new(ChainManager::new(0, 1000)));
ex
}
fn seeded(a: &Node, tmp: &std::path::Path) -> (String, [u8; 32]) {
let k = key(1);
let pkg = signed_package(tmp, 4096, &k);
let seeded = a.ex.seed_package_dir(&pkg, &anchors_for(&k)).unwrap();
let bin = &seeded.verified.body.binaries["aarch64"];
let h = hex_decode_exact::<32>(&bin.blake3).unwrap();
(seeded.manifest_hash, h)
}
#[tokio::test]
async fn lying_descriptor_is_discarded_never_served_and_does_not_block_recovery() {
let tmp = tempfile::tempdir().unwrap();
let (a, b, c) = (node("node-a"), node("node-b"), node("node-c"));
let (manifest_hash, h) = seeded(&a, tmp.path());
let anchors = anchors_for(&key(1));
b.ex.grant(h, "pkg");
let (s, _liar) = spawn_liar(h).await;
let err =
b.ex.fetch(&mut link("rogue", s), ArtifactKey::Content(h))
.await
.unwrap_err();
assert!(matches!(err, FetchError::Verification(_)), "{err:?}");
assert!(b.ex.resolve(&ArtifactKey::Content(h)).is_none());
let junk_key = hex_encode(blake3::hash(&JUNK).as_bytes());
assert!(!b.ex.store().contains(&junk_key), "junk piece discarded");
let failed = events(&b.chain, EVENT_KIND_ARTIFACT_FETCH);
assert_eq!(failed.len(), 1);
assert_eq!(failed[0]["result"], "failed");
assert_eq!(failed[0]["descriptors_rejected"][0]["peer"], "rogue");
let (s, b_serves_c) = connect(&b, "node-c").await;
let mut peers = link("node-b", s);
let err =
c.ex.fetch(&mut peers, ArtifactKey::Content(h))
.await
.unwrap_err();
drop(peers);
assert!(err.to_string().contains("not servable"), "{err}");
assert!(b_serves_c.await.unwrap().unwrap().pieces_served.is_empty());
assert!(events(&b.chain, EVENT_KIND_ARTIFACT_SERVE).is_empty());
let (s, _a) = connect(&a, "node-b").await;
b.ex.fetch_package(&mut link("node-a", s), &manifest_hash, &anchors)
.await
.expect("honest origin still works after the liar");
let (s, _b) = connect(&b, "node-c").await;
let got =
c.ex.fetch(&mut link("node-b", s), ArtifactKey::Content(h))
.await
.unwrap();
assert_eq!(
c.ex.read_all(&got.id).unwrap(),
a.ex.read_all(&got.id).unwrap()
);
}
#[tokio::test]
async fn liar_then_honest_peer_in_one_fetch_recovers() {
let tmp = tempfile::tempdir().unwrap();
let (a, b) = (node("node-a"), node("node-b"));
let (_, h) = seeded(&a, tmp.path());
let (s_liar, _liar) = spawn_liar(h).await;
let (s_a, _a) = connect(&a, "node-b").await;
let mut peers = PeerSet::new();
peers.push(PeerLink::new("rogue", Box::new(s_liar)));
peers.push(PeerLink::new("node-a", Box::new(s_a)));
let out =
b.ex.fetch(&mut peers, ArtifactKey::Content(h))
.await
.unwrap();
assert!(peers.links_mut()[0].is_dead(), "liar dropped");
assert_eq!(out.sources, ["node-a"]);
assert_eq!(out.descriptor.content_hash, h);
assert_eq!(b.ex.resolve(&ArtifactKey::Content(h)).unwrap().id(), out.id);
let fetches = events(&b.chain, EVENT_KIND_ARTIFACT_FETCH);
assert_eq!(fetches.len(), 1);
assert_eq!(fetches[0]["result"], "verified");
assert_eq!(fetches[0]["source_peer"], "node-a");
assert_eq!(fetches[0]["descriptors_rejected"][0]["peer"], "rogue");
assert_eq!(fetches[0]["bytes"], 4096);
}
#[tokio::test]
async fn liar_cannot_delete_an_unrelated_store_blob() {
let store = Arc::new(ArtifactStore::new_memory());
let b = exchange_over("node-b", &store);
let blob = vec![0x5A; 700];
let k = store.store(&blob, ArtifactType::Generic).unwrap();
let d = ArtifactDescriptor {
piece_size: 1024,
total_size: 700,
content_hash: [7; 32],
pieces: vec![*blake3::hash(&blob).as_bytes()],
};
let (s, _rogue) = spawn_rogue(d, vec![blob.clone()]).await;
let err = b
.fetch(&mut link("rogue", s), ArtifactKey::Content([7; 32]))
.await
.unwrap_err();
assert!(matches!(err, FetchError::Verification(_)), "{err:?}");
assert!(
store.contains(&k),
"liar deleted an unrelated blob it never sent"
);
assert_eq!(store.load(&k).unwrap(), blob);
}
#[tokio::test]
async fn liar_cannot_delete_pieces_held_before_a_restart() {
let tmp = tempfile::tempdir().unwrap();
let a = node("node-a");
let (_, h) = seeded(&a, tmp.path());
let store = Arc::new(ArtifactStore::new_memory());
let b1 = exchange_over("node-b", &store);
let (s, _a) = connect(&a, "node-b").await;
let real = b1
.fetch(&mut link("node-a", s), ArtifactKey::Content(h))
.await
.unwrap()
.descriptor;
drop(b1);
let b2 = exchange_over("node-b", &store);
let forged = ArtifactDescriptor {
content_hash: [9; 32],
..real.clone()
};
let (s, _rogue) = spawn_rogue(forged, vec![]).await;
let err = b2
.fetch(&mut link("rogue", s), ArtifactKey::Content([9; 32]))
.await
.unwrap_err();
assert!(matches!(err, FetchError::Verification(_)), "{err:?}");
for p in &real.pieces {
assert!(
store.contains(&hex_encode(p)),
"liar deleted an honest piece"
);
}
assert!(store.contains(&real.content_hex()), "whole blob kept");
}
#[tokio::test]
async fn root_key_liar_cannot_reuse_the_honest_id_or_wipe_resume_state() {
let tmp = tempfile::tempdir().unwrap();
let (a, b) = (node("node-a"), node("node-b"));
let (_, h) = seeded(&a, tmp.path());
let real = a.ex.resolve(&ArtifactKey::Content(h)).unwrap();
b.ex.note_pending(&real).unwrap();
for i in 0..real.piece_count() {
let data = a.ex.load_piece(&real, i).unwrap();
b.ex.store_piece(&data, &real.pieces[i as usize]).unwrap();
}
let forged = ArtifactDescriptor {
content_hash: [3; 32],
..real.clone()
};
assert_ne!(forged.id(), real.id(), "the id binds the content hash");
let (s, _rogue) = spawn_rogue(forged, vec![]).await;
let err =
b.ex.fetch(&mut link("rogue", s), ArtifactKey::Root(real.id()))
.await
.unwrap_err();
assert!(err.to_string().contains("does not match"), "{err}");
for p in &real.pieces {
assert!(b.ex.store().contains(&hex_encode(p)), "resume state wiped");
}
let (s, _a) = connect(&a, "node-b").await;
let out =
b.ex.fetch(&mut link("node-a", s), ArtifactKey::Root(real.id()))
.await
.unwrap();
assert_eq!(out.pieces_fetched, 0);
assert_eq!(
b.ex.read_all(&out.id).unwrap(),
a.ex.read_all(&out.id).unwrap()
);
}
#[tokio::test]
async fn an_idle_fetcher_does_not_hold_a_serve_session_open() {
let cfg = ExchangeConfig {
serve_idle_timeout: std::time::Duration::from_millis(100),
..Default::default()
};
let a = ArtifactExchange::new("node-a", Arc::new(ArtifactStore::new_memory()), cfg).unwrap();
let (_client, mut srv) = connected_pair().await.unwrap();
let served = tokio::time::timeout(
std::time::Duration::from_secs(5),
a.serve(&mut srv, "idle-peer"),
)
.await
.expect("serve returned on its own");
let err = served.unwrap_err();
assert!(err.to_string().contains("idle"), "{err}");
}