Skip to main content

openrtc/client/
sparse_fanout_impl.rs

1use anyhow::{Context, Result};
2
3use super::Client;
4
5impl Client {
6    /// Reduce one authoritative avenue roster through the shared Rust policy.
7    /// The returned JSON is input to the existing desired-peer actor; this
8    /// method never dials or schedules lifecycle work.
9    pub async fn configure_sparse_fanout(
10        &self,
11        capability: &str,
12        revision: u64,
13        local_device_id: &str,
14        local_device_key_x: &str,
15        peers_json: &str,
16        sparse: bool,
17    ) -> Result<(String, crate::sparse_fanout::SparseFanoutProjection)> {
18        anyhow::ensure!(
19            peers_json.len() <= 256 * 1024,
20            "sparse fanout roster exceeds 256 KiB"
21        );
22        let peers = serde_json::from_str::<Vec<serde_json::Value>>(peers_json)
23            .context("decode sparse fanout roster")?;
24        anyhow::ensure!(peers.len() <= 100, "sparse fanout roster exceeds 100 peers");
25        let local_node_id = self.sparse_fanout_node_id().await?;
26        let (projected, diagnostics) = self.sparse_fanout.lock().await.configure(
27            capability,
28            revision,
29            local_device_id,
30            &local_node_id,
31            local_device_key_x,
32            peers,
33            sparse,
34        )?;
35        Ok((serde_json::to_string(&projected)?, diagnostics))
36    }
37
38    /// Canonicalize one bounded overlay payload for the existing platform
39    /// device signer. This never exposes or imports private key bytes.
40    pub async fn prepare_sparse_fanout_message(
41        &self,
42        capability: &str,
43        payload: &[u8],
44    ) -> Result<crate::sparse_fanout::SparseFanoutSigningRequest> {
45        self.sparse_fanout
46            .lock()
47            .await
48            .prepare(capability, payload, sparse_fanout_now_ms())
49    }
50
51    /// Finalize exactly one canonical draft after the platform device signer
52    /// returns an Ed25519 signature. Rust verifies the signature before bytes
53    /// become eligible for transport.
54    pub async fn finalize_sparse_fanout_message(
55        &self,
56        request_id: &str,
57        signature: &str,
58    ) -> Result<Vec<u8>> {
59        self.sparse_fanout
60            .lock()
61            .await
62            .finalize(request_id, signature, sparse_fanout_now_ms())
63    }
64
65    /// Validate and deduplicate one sparse overlay frame. `source_peer_id` is
66    /// the already-admitted immediate Iroh peer that delivered the frame.
67    pub async fn accept_sparse_fanout_message(
68        &self,
69        capability: &str,
70        source_peer_id: &str,
71        encoded: &[u8],
72    ) -> Result<crate::sparse_fanout::SparseFanoutDecision> {
73        self.sparse_fanout.lock().await.accept(
74            capability,
75            source_peer_id,
76            encoded,
77            sparse_fanout_now_ms(),
78        )
79    }
80
81    pub async fn sparse_fanout_diagnostics(
82        &self,
83        capability: &str,
84    ) -> crate::sparse_fanout::SparseFanoutDiagnostics {
85        self.sparse_fanout.lock().await.diagnostics(capability)
86    }
87
88    pub async fn record_sparse_fanout_forward_queue_drop(
89        &self,
90        capability: &str,
91        count: u64,
92    ) -> Result<()> {
93        self.sparse_fanout
94            .lock()
95            .await
96            .record_forward_queue_drop(capability, count)
97    }
98
99    async fn sparse_fanout_node_id(&self) -> Result<String> {
100        let node = self.iroh_node.read().await;
101        let node = node
102            .as_ref()
103            .ok_or_else(|| anyhow::anyhow!("Iroh node is not initialized"))?;
104        Ok(node.endpoint().id().to_string())
105    }
106}
107
108fn sparse_fanout_now_ms() -> u64 {
109    std::time::SystemTime::now()
110        .duration_since(std::time::UNIX_EPOCH)
111        .unwrap_or_default()
112        .as_millis()
113        .min(u128::from(u64::MAX)) as u64
114}