feat(wallet,content,seed): Fedimint dual-ecash, paid content streaming, seed ceremony

- Fedimint ecash alongside Cashu: fedimint-clientd (fmcd) HTTP bridge,
  fedimint_client, fedimint RPC, wallet wiring
- Paid peer content: content invoices + streaming content server + content RPCs
- Seed-phrase ceremony/reveal RPCs and CLI ceremony tool
- LND wallet, mesh status/messaging, app-stack (netbird HTTPS), and
  decoupled-update wiring; Fedimint Client core app in catalog

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
This commit is contained in:
archipelago
2026-06-17 19:21:07 -04:00
co-authored by Claude Opus 4.8
parent c10f2ac22e
commit bd567cd165
34 changed files with 2677 additions and 68 deletions
+125 -4
View File
@@ -202,6 +202,106 @@ pub async fn save_mirrors(data_dir: &Path, mirrors: &[UpdateMirror]) -> Result<(
Ok(())
}
// ─── Update/app fetch source (origin vs DHT swarm) ──────────────────────────
//
// User-selectable per node, persisted in `data_dir/update-source.json`. This is
// the live-testing switch: keep `Origin` (default) to pull releases/app blobs
// purely over HTTP from the configured mirrors — the known-good path — or flip
// to `Swarm` on a test node to exercise the DHT (iroh swarm-assist), knowing the
// origin still always wins as fallback. Independent of the compile-time
// `iroh-swarm` feature and the `swarm_enabled` config: if the swarm engine isn't
// present, `Swarm` simply has no peers to consult and behaves like `Origin`.
const UPDATE_SOURCE_FILE: &str = "update-source.json";
#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize, Default)]
#[serde(rename_all = "lowercase")]
pub enum UpdateSource {
/// HTTP origin/mirrors only. The safe default and the universal fallback.
#[default]
Origin,
/// Try DHT swarm peers first for content-addressed blobs, origin always wins.
Swarm,
}
fn default_true() -> bool {
true
}
/// Node-level swarm preferences, persisted together in `update-source.json`.
/// Two independent switches:
/// - `source`: where THIS node fetches (origin vs swarm). Default origin.
/// - `provide_dht`: whether this node SEEDS/serves blobs to peers. Default on
/// (opt-out) so the swarm has providers; nodes that don't want to serve can
/// turn it off without affecting how they fetch.
#[derive(Debug, Clone, Copy, Serialize, Deserialize)]
struct SwarmPrefs {
#[serde(default)]
source: UpdateSource,
#[serde(default = "default_true")]
provide_dht: bool,
}
impl Default for SwarmPrefs {
fn default() -> Self {
Self {
source: UpdateSource::default(),
provide_dht: true,
}
}
}
fn update_source_path(data_dir: &Path) -> std::path::PathBuf {
data_dir.join(UPDATE_SOURCE_FILE)
}
async fn load_swarm_prefs(data_dir: &Path) -> SwarmPrefs {
match fs::read_to_string(update_source_path(data_dir)).await {
Ok(s) => serde_json::from_str::<SwarmPrefs>(&s).unwrap_or_default(),
Err(_) => SwarmPrefs::default(),
}
}
async fn save_swarm_prefs(data_dir: &Path, prefs: &SwarmPrefs) -> Result<()> {
fs::create_dir_all(data_dir)
.await
.with_context(|| format!("mkdir {}", data_dir.display()))?;
let path = update_source_path(data_dir);
let tmp = path.with_extension("json.tmp");
let json = serde_json::to_vec_pretty(prefs).context("serialize swarm prefs")?;
fs::write(&tmp, json)
.await
.with_context(|| format!("write {}", tmp.display()))?;
fs::rename(&tmp, &path)
.await
.with_context(|| format!("rename {} -> {}", tmp.display(), path.display()))?;
Ok(())
}
/// Load the node's selected fetch source. Missing/corrupt file → `Origin`.
pub async fn load_update_source(data_dir: &Path) -> UpdateSource {
load_swarm_prefs(data_dir).await.source
}
/// Persist the node's selected fetch source (preserving `provide_dht`).
pub async fn save_update_source(data_dir: &Path, source: UpdateSource) -> Result<()> {
let mut prefs = load_swarm_prefs(data_dir).await;
prefs.source = source;
save_swarm_prefs(data_dir, &prefs).await
}
/// Whether this node seeds/serves blobs to peers. Default true (opt-out).
pub async fn load_provide_dht(data_dir: &Path) -> bool {
load_swarm_prefs(data_dir).await.provide_dht
}
/// Persist whether this node provides to the swarm (preserving `source`).
pub async fn save_provide_dht(data_dir: &Path, provide: bool) -> Result<()> {
let mut prefs = load_swarm_prefs(data_dir).await;
prefs.provide_dht = provide;
save_swarm_prefs(data_dir, &prefs).await
}
/// Parse a manifest URL and return its `scheme://host[:port]` prefix.
/// Used by `rewrite_manifest_origins` so a manifest fetched from a
/// mirror points component downloads back at the same mirror rather
@@ -795,6 +895,23 @@ pub async fn download_update(data_dir: &Path) -> Result<DownloadProgress> {
DOWNLOAD_BYTES.store(0, Ordering::Relaxed);
DOWNLOAD_PROGRESS_AT.store(now_ms(), Ordering::Relaxed);
// Consult swarm peers only when the node has opted into DHT mode. In Origin
// mode (default) this stays empty so every component goes straight to the
// HTTP origin — instant, no-rebuild fallback while live-testing the swarm.
let update_source = load_update_source(data_dir).await;
let provide_dht = load_provide_dht(data_dir).await;
let swarm_providers = if update_source == UpdateSource::Swarm {
crate::swarm::providers()
} else {
Vec::new()
};
if update_source == UpdateSource::Swarm {
info!(
providers = swarm_providers.len(),
"Update source = DHT swarm (origin still wins as fallback)"
);
}
for component in &manifest.components {
if is_canceled() {
DOWNLOAD_TOTAL.store(0, Ordering::Relaxed);
@@ -825,7 +942,7 @@ pub async fn download_update(data_dir: &Path) -> Result<DownloadProgress> {
let dest_ref = &dest;
let source = crate::swarm::fetch_content_addressed(
&digest,
&crate::swarm::providers(),
&swarm_providers,
&dest,
move || async move {
download_component_resumable(client_ref, component, dest_ref, downloaded).await
@@ -844,9 +961,13 @@ pub async fn download_update(data_dir: &Path) -> Result<DownloadProgress> {
}
}
// 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;
// SHA-256 gates — announce that we can now seed it to peers. Gated on
// the node's "provide to swarm" preference (default on); best-effort,
// inert unless the iroh swarm is active, and never blocks the install.
// Independent of fetch source: an origin-fetching node can still seed.
if provide_dht {
crate::swarm::announce_held_blob(&digest.hex, &dest).await;
}
} else {
download_component_resumable(&client, component, &dest, downloaded).await?;
}