Merge agent-trust-wip (DHT Phases 0–4) into main
Integrates the DHT/peer-distribution line with the v1.7.98-alpha release fixes: - Phase 0 signed-catalog trust + release-root key (KAT-pinned) - Phase 1 BLAKE3 content addressing alongside SHA-256 - Phase 2 swarm-assist fetch seam (origin always wins) + iroh-blobs provider — heavy iroh deps stay behind the off-by-default `iroh-swarm` feature, so the default build/deploy is unaffected - Phase 3 signed Nostr seed-advertisement + discovery glue + paid swarm serving + "Networking Profits" Settings page - Phase 4 paid swarm streaming (cross-mint ecash, Shape-A paid ALPN, streaming.prepare-payment), also iroh-swarm-gated Conflicts resolved: seed.rs (kept release-root KAT tests), update.rs (comment-only, OTA logic identical), Cargo.lock (regenerated against the merged Cargo.toml). Default-feature build is clean. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
This commit is contained in:
@@ -263,6 +263,11 @@ pub struct ComponentUpdate {
|
||||
pub download_url: String,
|
||||
pub sha256: String,
|
||||
pub size_bytes: u64,
|
||||
/// DHT Phase 1: BLAKE3 content address (bare hex or `"blake3:<hex>"`), the
|
||||
/// iroh-native, range-verifiable hash. Optional during the migration
|
||||
/// window — when present it is verified ALONGSIDE the mandatory SHA-256.
|
||||
#[serde(default, skip_serializing_if = "Option::is_none")]
|
||||
pub blake3: Option<String>,
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
|
||||
@@ -798,7 +803,53 @@ pub async fn download_update(data_dir: &Path) -> Result<DownloadProgress> {
|
||||
}
|
||||
info!(name = %component.name, url = %component.download_url, "Downloading component");
|
||||
let dest = staging_dir.join(&component.name);
|
||||
download_component_resumable(&client, component, &dest, downloaded).await?;
|
||||
|
||||
// DHT Phase 2: when the manifest pins a BLAKE3 digest, route the fetch
|
||||
// through the swarm seam (swarm-assist, origin always wins). With no
|
||||
// providers registered (iroh-swarm feature off) this is identical to
|
||||
// calling the resumable HTTP origin directly — same bytes, now
|
||||
// content-addressed. A swarm hit is BLAKE3-verified inside the seam;
|
||||
// we still enforce the mandatory SHA-256 gate on peer bytes here and
|
||||
// re-fetch from origin if a (consistency-broken) peer slips through.
|
||||
let digest = component.blake3.as_deref().and_then(|b| {
|
||||
let s = b.trim();
|
||||
let normalized = if s.contains(':') {
|
||||
s.to_string()
|
||||
} else {
|
||||
format!("blake3:{s}")
|
||||
};
|
||||
crate::content_hash::ContentDigest::parse(&normalized).ok()
|
||||
});
|
||||
if let Some(digest) = digest {
|
||||
let client_ref = &client;
|
||||
let dest_ref = &dest;
|
||||
let source = crate::swarm::fetch_content_addressed(
|
||||
&digest,
|
||||
&crate::swarm::providers(),
|
||||
&dest,
|
||||
move || async move {
|
||||
download_component_resumable(client_ref, component, dest_ref, downloaded).await
|
||||
},
|
||||
)
|
||||
.await?;
|
||||
if source == crate::swarm::FetchSource::Swarm {
|
||||
let bytes = tokio::fs::read(&dest).await?;
|
||||
if crate::content_hash::sha256_hex(&bytes) != component.sha256 {
|
||||
warn!(
|
||||
name = %component.name,
|
||||
"swarm bytes passed BLAKE3 but failed the SHA-256 manifest gate — re-fetching from origin"
|
||||
);
|
||||
let _ = tokio::fs::remove_file(&dest).await;
|
||||
download_component_resumable(&client, component, &dest, downloaded).await?;
|
||||
}
|
||||
}
|
||||
// This is a PUBLIC release blob and it just passed both the BLAKE3 and
|
||||
// SHA-256 gates — announce that we can now seed it to peers. Best-effort
|
||||
// and inert unless the iroh swarm is active; never blocks the install.
|
||||
crate::swarm::announce_held_blob(&digest.hex, &dest).await;
|
||||
} else {
|
||||
download_component_resumable(&client, component, &dest, downloaded).await?;
|
||||
}
|
||||
downloaded += component.size_bytes;
|
||||
DOWNLOAD_BYTES.store(downloaded, Ordering::Relaxed);
|
||||
info!(
|
||||
@@ -993,6 +1044,25 @@ async fn download_component_resumable(
|
||||
.context("read staging file for hash check")?;
|
||||
let hash = hex::encode(Sha256::digest(&bytes));
|
||||
if hash == component.sha256 {
|
||||
// DHT Phase 1: if the manifest also pins a BLAKE3 digest, it must
|
||||
// match too. SHA-256 stays the mandatory gate during migration;
|
||||
// BLAKE3 is the hash the iroh swarm will fetch/verify by, so a
|
||||
// present-but-wrong BLAKE3 means the bytes aren't swarm-consistent
|
||||
// — treat it like a SHA mismatch and re-download.
|
||||
if let Some(b3) = component.blake3.as_deref() {
|
||||
let expected = b3.trim().strip_prefix("blake3:").unwrap_or(b3.trim());
|
||||
let actual = crate::content_hash::blake3_hex(&bytes);
|
||||
if !actual.eq_ignore_ascii_case(expected) {
|
||||
let _ = tokio::fs::remove_file(dest).await;
|
||||
last_err = Some(anyhow::anyhow!(
|
||||
"BLAKE3 mismatch for {}: expected {}, got {}",
|
||||
component.name,
|
||||
expected,
|
||||
actual
|
||||
));
|
||||
continue;
|
||||
}
|
||||
}
|
||||
return Ok(());
|
||||
}
|
||||
// SHA mismatch — the file on disk is garbage. Nuke it and
|
||||
@@ -1675,6 +1745,7 @@ mod tests {
|
||||
download_url: "https://git.tx1138.com/lfg2025/archy/raw/branch/main/releases/v1.7.26-alpha/archipelago".into(),
|
||||
sha256: "x".into(),
|
||||
size_bytes: 1,
|
||||
blake3: None,
|
||||
},
|
||||
ComponentUpdate {
|
||||
name: "frontend".into(),
|
||||
@@ -1683,6 +1754,7 @@ mod tests {
|
||||
download_url: "https://git.tx1138.com/lfg2025/archy/raw/branch/main/releases/v1.7.26-alpha/frontend.tar.gz".into(),
|
||||
sha256: "y".into(),
|
||||
size_bytes: 2,
|
||||
blake3: None,
|
||||
},
|
||||
],
|
||||
};
|
||||
@@ -1882,9 +1954,10 @@ mod tests {
|
||||
tokio::fs::write(staging.join("archipelago"), b"staged")
|
||||
.await
|
||||
.unwrap();
|
||||
// A *complete* staged update carries the marker; without it the state
|
||||
// self-heal correctly treats this as a partial download and clears
|
||||
// update_in_progress (see has_staged_update / #26).
|
||||
// A *complete* staged update carries the .download-complete marker;
|
||||
// without it has_staged_update() reads the staging as partial and the
|
||||
// load_state self-heal clears update_in_progress (see #26). This test
|
||||
// simulates a complete staging, so write the marker.
|
||||
tokio::fs::write(staging.join(STAGED_COMPLETE_MARKER), b"1")
|
||||
.await
|
||||
.unwrap();
|
||||
@@ -1902,6 +1975,7 @@ mod tests {
|
||||
download_url: "https://example.com/binary".to_string(),
|
||||
sha256: "abc123".to_string(),
|
||||
size_bytes: 5000,
|
||||
blake3: None,
|
||||
}],
|
||||
}),
|
||||
update_in_progress: true,
|
||||
|
||||
Reference in New Issue
Block a user