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:
co-authored by
Claude Opus 4.8
parent
c10f2ac22e
commit
bd567cd165
@@ -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?;
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user