feat(dht): Phase 1 — BLAKE3 content addressing alongside SHA-256

Adds the iroh-native, range-verifiable hash next to the incumbent SHA-256
so the swarm can later fetch/verify by BLAKE3 with the registry/origin as
fallback. Non-breaking: SHA-256 stays the mandatory gate; BLAKE3 is verified
only when present.

- content_hash.rs: HashAlg + ContentDigest (parse/verify '<alg>:<hex>'
  multihash strings), blake3_hex/sha256_hex; BLAKE3 known-answer test
- update.rs: ComponentUpdate.blake3 (serde-default); verified ALONGSIDE
  SHA-256 in the resumable download loop, re-download on mismatch
- blobs.rs: BlobMeta.blake3 computed on put (on-disk path stays
  SHA-256-keyed for back-compat; advertises the future swarm address)

Drive-by: fix a pre-existing stale test (test_save_and_load_state_roundtrip)
that never wrote the .download-complete marker #26 requires, so load_state's
self-heal cleared update_in_progress. Unrelated to BLAKE3 — surfaced by
running the full update:: suite.

40/40 content_hash/update/blobs tests pass.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
This commit is contained in:
archipelago
2026-06-16 13:05:27 -04:00
co-authored by Claude Opus 4.8
parent 27f11bf85a
commit f0cb91ed76
6 changed files with 238 additions and 10 deletions
+7
View File
@@ -25,6 +25,12 @@ pub const MAX_BLOB_SIZE: u64 = 64 * 1024 * 1024;
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct BlobMeta {
pub cid: String,
/// DHT Phase 1: BLAKE3 hash of the content (iroh-native swarm address).
/// The on-disk path stays SHA-256-keyed (`cid`) for back-compat; this
/// advertises the hash a peer swarm can fetch/range-verify by. Absent in
/// legacy metadata written before Phase 1.
#[serde(default, skip_serializing_if = "Option::is_none")]
pub blake3: Option<String>,
pub size: u64,
pub mime: String,
#[serde(default, skip_serializing_if = "Option::is_none")]
@@ -88,6 +94,7 @@ impl BlobStore {
let cid = hex::encode(hasher.finalize());
let meta = BlobMeta {
cid: cid.clone(),
blake3: Some(crate::content_hash::blake3_hex(bytes)),
size: bytes.len() as u64,
mime: mime.to_string(),
filename,