1use std::{collections::BTreeSet, future::Future, path::PathBuf, sync::Arc, time::Duration};
6
7use api::{
8 framing,
9 v2::client::{MessageReader, MessageWriter, RpcTransport},
10};
11use iroh::endpoint::Connection;
12use objects::{
13 object::thread_replication::{OPERATION_FORMAT, ThreadFacet},
14 store::ObjectStore,
15};
16use prost::Message;
17use repo::thread_replication::ThreadReplica;
18
19use crate::{
20 Rpc,
21 authority::RootAuthority,
22 contract::*,
23 live_replication::{self, Feed, Side},
24 replication::{
25 self, Session,
26 native::LocalReplica,
27 opening::{self, FRAME_LIMIT, validate_endpoint},
28 },
29 rpc,
30 transport::{self, Authorize, IrohTransport, Reader, Writer},
31};
32
33const TIMEOUT: Duration = Duration::from_secs(10);
34
35#[derive(Clone)]
38pub struct Peer {
39 replica: ThreadReplica,
40 endpoint: EndpointRef,
41 facets: BTreeSet<ThreadFacet>,
42 genesis: ThreadGenesisRecord,
43 authority_home: Option<PathBuf>,
44}
45impl Peer {
46 pub fn new(
47 replica: ThreadReplica,
48 endpoint: EndpointRef,
49 facets: BTreeSet<ThreadFacet>,
50 ) -> Result<Self, transport::Error> {
51 validate_endpoint(&endpoint)?;
52 if facets.is_empty() {
53 return Err(transport::Error::Protocol(
54 "replication requires an admission facet",
55 ));
56 }
57 let genesis = replica.genesis_record().map_err(io_error)?;
58 Ok(Self {
59 replica,
60 endpoint,
61 facets,
62 genesis,
63 authority_home: None,
64 })
65 }
66
67 pub fn with_device_authority(mut self, home: PathBuf) -> Self {
70 self.authority_home = Some(home);
71 self
72 }
73 fn local_replica<S: ObjectStore + Send + Sync + 'static>(
74 &self,
75 store: Arc<S>,
76 ) -> LocalReplica<S> {
77 let local = LocalReplica::new(self.replica.clone(), store);
78 match &self.authority_home {
79 Some(home) => local.with_device_authority(home.clone()),
80 None => local,
81 }
82 }
83 fn reference(&self) -> Result<ThreadRef, transport::Error> {
84 let genesis = self.replica.genesis().map_err(store_error)?;
85 Ok(ThreadRef {
86 spool: Some(SpoolRef { id: genesis.spool }),
87 id: Some(ThreadId {
88 value: self.replica.thread_id().as_bytes().to_vec(),
89 }),
90 })
91 }
92
93 pub async fn connect<S, A, G, F>(
96 &self,
97 connection: Connection,
98 destination: EndpointKind,
99 signer: A,
100 store: Arc<S>,
101 feed: &Feed,
102 authorize: G,
103 ) -> live_replication::Result<(), replication::native::Error>
104 where
105 S: ObjectStore + Send + Sync + 'static,
106 A: Authorize,
107 G: Fn() -> F + Clone + Send + Sync + 'static,
108 F: Future<Output = Result<(), transport::Error>> + Send,
109 {
110 let remote_key = *connection.remote_id().as_bytes();
111 let destination = EndpointRef {
112 public_key: remote_key.to_vec(),
113 kind: destination as i32,
114 };
115 validate_endpoint(&destination)?;
116 let thread = self.reference()?;
117 let (_, version) = self.replica.sharing(&remote_key).map_err(store_error)?;
118 let opening = ReplicationOpen {
119 native_authority: None,
120 thread: Some(thread.clone()),
121 facets: self
122 .facets
123 .iter()
124 .copied()
125 .map(replication::wire_facet)
126 .collect(),
127 sharing_policy_version: version.map(|v| v.as_bytes().to_vec()).unwrap_or_default(),
128 thread_genesis: Some(self.genesis.clone()),
129 budget: Some(ReadBudget {
130 max_items: 64,
131 max_frame_bytes: FRAME_LIMIT as u32,
132 max_snapshot_bytes: 0,
133 }),
134 record_formats: vec![OPERATION_FORMAT.into()],
135 session_nonce: uuid::Uuid::new_v4().as_bytes().to_vec(),
136 source: Some(self.endpoint.clone()),
137 destination: Some(destination.clone()),
138 protocol: crate::hybrid::sync_protocol(),
139 import_authority: None,
140 };
141 let transport = IrohTransport::new(connection, signer, FRAME_LIMIT, TIMEOUT)?;
142 let requested_protocol = opening.protocol.clone();
143 let (writer, mut reader) = transport
144 .exchange(
145 rpc::SyncServiceReplicateThread::METHOD,
146 ReplicateThreadRequest {
147 body: Some(replicate_thread_request::Body::Open(opening)),
148 }
149 .encode_to_vec(),
150 )
151 .await?;
152 let bytes = reader.next().await?.ok_or(transport::Error::Protocol(
153 "replication closed before Ready",
154 ))?;
155 let response =
156 ReplicateThreadResponse::decode(bytes.as_slice()).map_err(transport::Error::from)?;
157 let Some(replicate_thread_response::Body::Ready(ready)) = response.body else {
158 return Err(transport::Error::Protocol("replication requires Ready").into());
159 };
160 let (facets, max_items) =
161 opening::validate_ready(&ready, &thread, &destination, &self.facets, 64)?;
162 let session = Session::new(self.local_replica(store), remote_key, facets, max_items)?
163 .with_protocol(requested_protocol.as_ref(), ready.protocol.as_ref())?;
164 live_replication::run(
165 session,
166 reader,
167 writer,
168 Side::Initiator,
169 feed,
170 move |activity| {
171 let authorize = authorize.clone();
172 async move {
173 if matches!(activity, live_replication::Activity::InputConsumed { .. }) {
174 Ok(())
175 } else {
176 authorize().await
177 }
178 }
179 },
180 )
181 .await
182 }
183
184 pub async fn accept<S>(
187 &self,
188 connection: Connection,
189 authority: Arc<RootAuthority>,
190 store: Arc<S>,
191 feed: &Feed,
192 ) -> live_replication::Result<(), replication::native::Error>
193 where
194 S: ObjectStore + Send + Sync + 'static,
195 {
196 let remote_key = *connection.remote_id().as_bytes();
197 let (send, mut recv) = tokio::time::timeout(TIMEOUT, connection.accept_bi())
198 .await
199 .map_err(|_| transport::Error::Timeout)?
200 .map_err(io_error)?;
201 let mut writer = Writer::new(send, FRAME_LIMIT, TIMEOUT);
202 let opening = async {
203 let mut prelude = vec![0; 6];
204 recv.read_exact(&mut prelude).await.map_err(io_error)?;
205 let method_size = u16::from_be_bytes([prelude[0], prelude[1]]) as usize;
206 let context_size =
207 u32::from_be_bytes([prelude[2], prelude[3], prelude[4], prelude[5]]) as usize;
208 if method_size == 0
209 || method_size > framing::MAX_METHOD_PATH
210 || context_size > framing::MAX_CALL_CONTEXT
211 {
212 return Err(transport::Error::Protocol(
213 "opening exceeds metadata limits",
214 ));
215 }
216 prelude.resize(6 + method_size + context_size, 0);
217 recv.read_exact(&mut prelude[6..]).await.map_err(io_error)?;
218 let (request, _) = framing::decode_request_prelude(&prelude)?
219 .ok_or(transport::Error::Protocol("incomplete opening metadata"))?;
220 if request.method != rpc::SyncServiceReplicateThread::METHOD.path {
221 return Err(transport::Error::Protocol(
222 "unsupported RPC on replication boundary",
223 ));
224 }
225 Ok(request.context)
226 };
227 let context = tokio::time::timeout(TIMEOUT, opening)
228 .await
229 .map_err(|_| transport::Error::Timeout)??;
230 let mut reader = Reader::for_method(
231 recv,
232 FRAME_LIMIT,
233 TIMEOUT,
234 rpc::SyncServiceReplicateThread::METHOD,
235 );
236 let bytes = reader
237 .next()
238 .await?
239 .ok_or(transport::Error::Protocol("missing replication opening"))?;
240 let peer = self.clone();
241 let verifier = authority.clone();
242 let checked = tokio::task::spawn_blocking(move || -> Result<_, transport::Error> {
243 let request = ReplicateThreadRequest::decode(bytes.as_slice())?;
244 let Some(replicate_thread_request::Body::Open(open)) = request.body else {
245 return Err(transport::Error::Protocol("replication requires Open"));
246 };
247 let (_, version) = peer.replica.sharing(&remote_key).map_err(store_error)?;
248 let accepted = opening::accept(
249 &open,
250 &peer.reference()?,
251 &peer.endpoint,
252 remote_key,
253 &peer.facets,
254 version.map(|v| v.as_bytes().to_vec()).unwrap_or_default(),
255 )?;
256 if accepted.import_authority.is_some() {
257 return Err(transport::Error::Protocol(
258 "HYBRID replication requires independently selected hosted trust",
259 ));
260 }
261 let ready = accepted.ready;
262 let (facets, max_items) = opening::validate_ready(
263 &ready,
264 &peer.reference()?,
265 &peer.endpoint,
266 &peer.facets,
267 64,
268 )?;
269 let verified = verifier.verify(
270 &context,
271 rpc::SyncServiceReplicateThread::METHOD,
272 &bytes,
273 "write",
274 &peer.replica,
275 )?;
276 Ok((verified, facets, max_items, ready))
277 })
278 .await
279 .map_err(|e| transport::Error::Io(e.to_string()))?;
280 let (verified, facets, max_items, ready) = match checked {
281 Ok(value) => value,
282 Err(error) => {
283 writer
284 .fail(&api::heddle::api::common::CallFailure {
285 code: 7,
286 message: error.to_string(),
287 ..Default::default()
288 })
289 .await?;
290 return Err(error.into());
291 }
292 };
293 let negotiated_protocol = ready.protocol.clone();
294 writer
295 .send(
296 ReplicateThreadResponse {
297 body: Some(replicate_thread_response::Body::Ready(ready)),
298 }
299 .encode_to_vec(),
300 )
301 .await?;
302 let session = Session::new(self.local_replica(store), remote_key, facets, max_items)?
303 .with_protocol(negotiated_protocol.as_ref(), negotiated_protocol.as_ref())?;
304 live_replication::run(
305 session,
306 reader,
307 writer,
308 Side::Acceptor,
309 feed,
310 move |activity| {
311 std::future::ready(
312 if matches!(activity, live_replication::Activity::InputConsumed { .. }) {
313 Ok(())
314 } else {
315 authority.recheck(&verified)
316 },
317 )
318 },
319 )
320 .await
321 }
322}
323
324fn io_error(error: impl std::fmt::Display) -> transport::Error {
325 transport::Error::Io(error.to_string())
326}
327fn store_error(error: repo::thread_replication::Error) -> transport::Error {
328 io_error(error)
329}
330
331#[cfg(test)]
332#[path = "replication_rpc_tests.rs"]
333mod tests;