Fetch-side auto-pay decision layer (payment.rs), Shape-A paid-blobs negotiation ALPN (paid_alpn.rs), cross-mint ecash swap + payer auto-swap builder + idempotent resume/liquidity cache (ecash.rs), and the streaming.prepare-payment RPC. All gated behind the iroh-swarm feature (off by default). 91/91 tests pass, both build configs clean. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
14 KiB
DHT work — RESUME HERE
Last updated: 2026-06-16 · Branch: agent-trust-wip · Worktree: ~/Projects/archy-dht
This file is the single source of truth for resuming the DHT / peer-distribution work after a restart. Read it top to bottom, run the Verify state block, then continue at Next step.
⚠️ CRITICAL — where to work (do not skip)
- Work ONLY in the worktree
~/Projects/archy-dhton branchagent-trust-wip. - NEVER run git checkout / branch-switch / commit in the shared tree
~/Projects/archy. Another agent cuts releases onmainthere. Git branch state is global to one working tree, so a checkout in the shared tree drags every session onto that branch and can clobber uncommitted work. That already happened once — the worktree exists specifically to prevent it. See memoryfeedback_concurrent_agent_tree. - The shared tree stays on
mainfor the release agent. Leave it alone.
Build facts (so you don't get surprised)
- It's a binary crate: test with
cargo test --bin archipelago -- <filter>(there is no lib target). - The test profile is opt-level=3 → every incremental test rebuild of the
archipelagocrate is ~5 min; a cold build of the iroh feature tree is ~19 min. Budget for it. Run builds in the background and poll. - Default build = no iroh. The iroh swarm engine is behind the
iroh-swarmCargo feature (off by default):cargo build --features iroh-swarm. - Plain
cargo build(no feature) is the fleet build and is unaffected by any DHT work.
Verify state (run these first on resume)
cd ~/Projects/archy-dht
git branch --show-current # → agent-trust-wip
git log --oneline -7 # see the commit list below
git status --short # should be clean (or your in-progress edits)
git worktree list # archy-dht → agent-trust-wip; archy → main
# sanity compile (default, fast-ish):
cargo build --bin archipelago 2>&1 | tail -3
What is DONE (committed on agent-trust-wip)
Design doc: docs/dht-distribution-design.md (the full plan).
| Commit | Phase | Summary |
|---|---|---|
0fef8086 |
base | parked trust module + seed::derive_release_root_ed25519 (pre-existing) |
27f11bf8 |
0 | signed-catalog authenticity wired: trust/ module verifies the release-root detached signature in app_catalog::fetch_one; release-root KAT pinned |
f0cb91ed |
1 | BLAKE3 alongside SHA-256: content_hash.rs, ComponentUpdate.blake3, BlobMeta.blake3 |
2523c9e3 |
2 seam | swarm/mod.rs — BlobProvider + fetch_content_addressed (verify peer bytes, origin-always-wins); iroh-swarm flag; wired into update.rs |
082946aa |
2 engine | real swarm/iroh_provider.rs over iroh 1.0 + iroh-blobs 0.103 (optional deps). Dep tree proven to resolve+compile against the pinned stack |
9fa56a82 |
3 core | swarm/seed_advert.rs — signed Nostr seed-advertisement protocol (NIP-33 kind 30081, d-tag=blake3) |
All tests green at each step. Total new modules: trust/, content_hash.rs, swarm/.
task #12 — Phase 3 glue + wiring — DONE (2026-06-17, NOT yet committed)
Implemented in the worktree, uncommitted (release in flight — do not commit/merge
until the user says so). Verified: default cargo build clean, cargo build --features iroh-swarm clean, cargo test --bin archipelago -- swarm:: → 8/8 pass.
NostrSeedDiscovery(swarm/iroh_provider.rs) —ProviderDiscoverymade async (#[async_trait]); impl queries relays via the newseed_advert::fetch_seed_endpoint_idsand parses each string withEndpointId::from_str(EndpointId = PublicKey, hasFromStr/Display), skipping unparseable.try_fetchnow.awaits discovery.- Publish path — dep-free
seed_advert::fetch_seed_endpoint_ids+publish_seed_advert(reuse now-pub(crate)build_nostr_client/load_or_create_nostr_keys);IrohProvider::seed_and_advertiseimports the blob into the FsStore (blobs().add_path→TagInfo) with a defensive hash-match, then publishes. Scope: releases/catalog only. - Wiring —
swarm::init()builds theIrohProvideronce at startup into aOnceLock<SwarmRuntime>(keeps endpoint/router alive → keeps seeding);providers()returns the registered provider;announce_held_blob()is called fromupdate.rsafter each release component passes both hash gates. New configswarm_enabled(ARCHIPELAGO_SWARM_ENABLED, default false);server.rscallsswarm::init. All iroh code stays behindiroh-swarm; default build inert.
iroh-blobs paid-serving spike (open Q#1) — RESOLVED: BlobsProtocol::new(&store, Some(EventSender)) + EventMask intercept gives native per-request allow/deny
(RequestMode::Intercept → Result<(), AbortReason>), connection-level reject
(ConnectMode::Intercept), and per-request throttle/meter (ThrottleMode::Intercept).
NEW: Phase 4+ plan (paid streaming / relay / IndeeHub) — docs/phase4-streaming-ecash-plan.md
Design for: (1) ecash-paid swarm transport, (2) networking through nodes / relay,
(3) IndeeHub "Archipelago" content source (signed Nostr film catalog, kind 30082).
Headline: ~80% already exists (Cashu wallet, streaming/ payment gate + metering,
4-tier transport, the swarm above). Also shipped this session: a Networking Profits
→ Settings UI in neode-ui (new views/web5/Web5NetworkingProfitsSettings.vue +
route + button in Web5QuickActions.vue + common.settings i18n) that drives the
existing streaming.list-services/configure-service RPCs; free-everything is the
default (all services ship enabled:false). Frontend typechecks clean (pre-existing
Web5ConnectedNodes.vue .did errors are NOT ours). neode-ui deps were
npm installed to complete a partial install.
F2 step 1 — cross-mint ecash swap — DONE (2026-06-17, NOT yet committed)
Plan §2a / phasing F2 step 1. Implemented in wallet/ecash.rs, uncommitted
(release in flight). Verified: cargo test --bin archipelago -- wallet::ecash →
25/25 pass (6 new), default build clean, --features iroh-swarm build clean.
is_mint_trusted(data_dir, url)— swap-into allow-list. Home Fedimint always trusted; any other mint must be onaccepted_mints(normalized, trailing-slash tolerant). Reuses the list the streaming gate already advertises to payers.mint_quote_at/melt_quote_at/send_token_at(data_dir, mint_url, amount)— the home-mint-hardcoded helpers parameterized by target mint.send_tokennow delegates tosend_token_atwith the home mint.swap_between_mints(data_dir, from, to, amount, max_fee_sats) -> u64— mint-quote on B → melt-quote on A → fee-cap check (swap_fee= total_paid − delivered; bail if > cap so caller falls back to free origin) → select+melt A proofs → persist the spend BEFORE claiming (crash can't double-spend) → poll B invoice until PAID/ISSUED (wait_for_mint_quote_paid, 60s/2s) → mint+claim on B. Both legs recorded in the tx log (peer field carries the counterpart mint).
F2 step 2 — payer-side auto-swap payment builder — DONE (2026-06-17, NOT yet committed)
Plan §2a step 2. Implemented in wallet/ecash.rs, uncommitted. Verified:
cargo test --bin archipelago -- wallet::ecash → 34/34 pass (9 new). All on the
default path (no feature gating) so the iroh-swarm tree is unaffected.
WalletState::spendable_by_mint() -> Vec<(mint_url, balance)>— per-mint holdings.PaymentPlan { Direct{mint}, Swap{from,to}, Insufficient }+ pureplan_payment(holdings, accepted: &[(mint, trusted)], amount)— the policy: Direct beats Swap (already-held mint, no fee, no trust needed); a Swap target must be trusted (is_mint_trusted); home mint is the tie-break for both legs;Insufficient→ caller uses free origin. Pure/sync, unit-tested without a mint.build_payment_token(data_dir, accepted_mints, amount_sats, max_fee_sats) -> token— annotates the seeder'saccepted_mintswith trust, runsplan_paymentagainstspendable_by_mint(), thensend_token_at(direct) orswap_between_mints+send_token_at(swap, honoring the fee cap). Bails (→ origin) when nothing covers the amount within balance/trust/fee. This is the builder the fetch side calls.
Fetch-side auto-pay + F2 step 3 hardening — DONE (2026-06-17, NOT yet committed)
Implemented; uncommitted. Verified: cargo test --bin archipelago -- wallet:: swarm:: → 85/85 pass (18 new across these + earlier steps), 0 warnings,
default build clean. --features iroh-swarm build = (see below; re-run after these
edits).
swarm/payment.rs(un-gated — builds withoutiroh-swarm):PaymentPolicy { budget_sats, max_fee_sats }+auto_pay_token(data_dir, policy, accepted_mints, price)→Ok(Some(token))to pay /Ok(None)to use origin. Degrades any wallet/mint error toOk(None)so payment can never block content (origin always wins). The on-wire token→peer exchange (in-band paid-blobs ALPN, "shape A") is the remaining gap — deferred in the plan; this is the decision/builder brain it'll call.streaming.prepare-paymentRPC (dispatcher +handle_streaming_prepare_payment): the live, user-invokable entry to the payer-side builder. Params{accepted_mints, price_sats, budget_sats?, max_fee_sats?}→{status:"ready", token}or{status:"declined"}. This is what makes the whole payment chain reachable (no dead code).- Idempotent swap resume (
wallet/pending_swaps.json):swap_between_mintsjournals the in-flight swap (melt + mint quote ids) right after the source spend is persisted, removes it on claim.resume_pending_swaps(data_dir)reclaimsPAIDquotes, skipsISSUED(never double-claims), leaves unsettled — wired at server startup (server.rs, afterswarm::init). - Liquidity cache (
wallet/swap_liquidity.json): per-route success/failure;build_payment_tokenorders swap targets bytarget_liquidity_score(proven routes first, home still first).swap_between_mintsrecords success/failure. - Removed the unused
mint_quote_at/melt_quote_atthin wrappers (swap callsMintClientdirectly; nothing else used them).
Shape-A paid-blobs negotiation ALPN — DONE (2026-06-17, NOT yet committed)
Plan §1 "shape A" — the on-wire exchange that lets a downloader pay a seeder before
fetching a gated blob. Implemented behind iroh-swarm; uncommitted. Compiles
clean (cargo build --features iroh-swarm → only the 2 pre-existing trust/ warns).
Caveat: the request/grant wire path can only be fully verified with a live
two-node iroh test (serde + types are unit-tested; the QUIC round-trip is not).
swarm/paid_alpn.rs(gated): ALPNarchy/paid-blobs/1on a second handler on the same endpoint/router.PaidRequest { want, token? }↔PaidResponse { Granted | PaymentRequired{price_sats, accepted_mints} | Denied{reason} }.- Serve side
PaidBlobsProtocol(ProtocolHandler): per bi-stream, keys the peer byconnection.remote_id(), runsstreaming::gate::check_gate(content-download, peer, token, blob_size), maps to a verdict. Free when service disabled (default), fail-OPEN (Granted) on gate error — mirrorsswarm/paid.rs. A paid retry's token opens the session the blob-GET gate then sees (same endpoint id → same session). - Fetch side
negotiate_access(endpoint, data_dir, peer, hex, policy) -> bool: best-effort + additive. Asks with no token; onPaymentRequiredcallspayment::auto_pay_token(cross-mint aware), retries with the token. Connect/ protocol failure ⇒ proceed (the GET gate is the real enforcement); explicitPaymentRequiredwe won't/can't pay ⇒ skip peer → origin.
- Serve side
- Wired into
iroh_provider.rs: registers the 2nd ALPN on theRouter;try_fetchnegotiates with each discovered peer beforedownloader.download.IrohProvidercarriesdata_dir+pay_policy(defaults toPaymentPolicy::free→ releases/ catalog never pay; a future film fetch passes a real budget).
Remaining to make paid FILM fetch real (small, on top of shape A)
- Pass a non-free
PaymentPolicyfor the film scope (releases stay free) + surface an auto-pay cap in Settings. The plumbing is all here; only the policy source is free. - Live two-node integration test (tests/multinode/) to exercise the actual QUIC request→pay→grant→GET path end to end.
Remaining Phase 4 roadmap (NOT started — gated)
- Relay protocol (§2b) — single-hop paid
relay.fetch. Needs design sign-off. - IndeeHub "Archipelago" source (steps A–E) — signed kind-30082 film catalog +
film.catalog/GET /api/film/:blake3+ frontend source. Gated on user decisions (publisher trust anchor, MinIO origin) + the external IndeeHub frontend repo. Shipping directive (user 2026-06-17): ship the IndeeHub app change as a decoupled app-catalog update (bumpreleases/app-catalog.json), not a binary OTA. Seedocs/phase4-streaming-ecash-plan.md§4 note.
After Phase 3
- Phase 4 — IndeeHub films on the same blob layer (Blossom catalog + iroh swarm;
MinIO origin). Each HLS
.tssegment = a content-addressed blob. - Phase 0 GO-LIVE (needs the user) — the catalog/manifest signature anchor
trust::anchor::RELEASE_ROOT_PUBKEY_HEXis stillNone; the pinned KAT is the TEST mnemonic, not the real key. Going live = signing ceremony with the real release master seed (only the user has it) → derive release-root → bake its pubkey intoanchor.rs→ sign the realreleases/app-catalog.json. Until then verification is advisory (verify-if-present, anchor not enforced).
Mergeability
As of last check we were only ~4 commits diverged from main; the only shared-file
overlap is seed.rs + update.rs. Do NOT merge to main while the release is in
flight — that's the user's call. Sync (merge main → agent-trust-wip) once the
release lands and main is clean.
Background build logs from the last session (may be stale)
/tmp/dht-*.log — phase test/build outputs. Safe to ignore/delete on resume.