Skip to main content

aranya_runtime/
client.rs

1use core::{fmt, iter::DoubleEndedIterator};
2
3use buggy::Bug;
4use tracing::error;
5
6use crate::{
7    Address, CmdId, Command, GraphId, LocatedAddress, PeerCache, Perspective as _, Policy,
8    PolicyError, PolicyStore, Segment as _, Sink, Storage as _, StorageError, StorageProvider,
9    TraversalBuffer,
10    policy::ActionPlacement,
11    storage::{HeadSet, Spill},
12};
13
14pub(crate) mod braiding; // exposed for `buffers.rs` (StrandHeap)
15mod buffers;
16pub(crate) mod convergence_map; // exposed for `buffers.rs` (ConvergenceStorage)
17mod session;
18mod transaction;
19
20pub(crate) use buffers::BraidBuffer;
21pub use buffers::RuntimeBuffers;
22
23pub use self::{session::Session, transaction::Transaction};
24
25/// An error returned by the runtime client.
26#[derive(Debug, thiserror::Error)]
27#[non_exhaustive]
28pub enum ClientError {
29    #[error("no such parent: {0}")]
30    NoSuchParent(CmdId),
31    #[error("policy error: {0}")]
32    PolicyError(#[from] PolicyError),
33    #[error("storage error: {0}")]
34    StorageError(#[from] StorageError),
35    #[error("init error")]
36    InitError,
37    #[error("could not deserialize session command")]
38    SessionDeserialize,
39    /// Attempted to braid two parallel finalize commands together.
40    ///
41    /// Policy must be designed such that two parallel finalize commands are never produced.
42    ///
43    /// Currently, this is practically an unrecoverable error. You must wipe all graphs containing
44    /// the "bad" finalize command and resync from the "good" clients. Otherwise, your network will
45    /// split into two separate graph states which can never successfully sync.
46    #[error("found parallel finalize commands during braid")]
47    ParallelFinalize,
48    #[error("concurrent transaction usage")]
49    ConcurrentTransaction,
50    #[error(transparent)]
51    Bug(#[from] Bug),
52}
53
54/// Keeps track of client graph state.
55///
56/// - `PS` should be an implementation of [`PolicyStore`].
57/// - `SP` should be an implementation of [`StorageProvider`].
58pub struct ClientState<PS, SP> {
59    policy_store: PS,
60    provider: SP,
61}
62
63// Manual Debug impl to exclude `buffers` (large, not useful in debug output).
64impl<PS: fmt::Debug, SP: fmt::Debug> fmt::Debug for ClientState<PS, SP> {
65    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
66        f.debug_struct("ClientState")
67            .field("policy_store", &self.policy_store)
68            .field("provider", &self.provider)
69            .finish_non_exhaustive()
70    }
71}
72
73impl<PS, SP> ClientState<PS, SP> {
74    /// Creates a `ClientState`.
75    pub const fn new(policy_store: PS, provider: SP) -> Self {
76        Self {
77            policy_store,
78            provider,
79        }
80    }
81
82    /// Provide access to the [`StorageProvider`].
83    pub fn provider(&mut self) -> &mut SP {
84        &mut self.provider
85    }
86}
87
88impl<PS, SP> ClientState<PS, SP>
89where
90    PS: PolicyStore,
91    SP: StorageProvider,
92{
93    /// Create a new graph (AKA Team). This graph will start with the initial policy
94    /// provided which must be compatible with the policy store PS. The `payload` is the initial
95    /// init message that will bootstrap the graph facts. Effects produced when processing
96    /// the payload are emitted to the sink.
97    pub fn new_graph(
98        &mut self,
99        policy_data: &[u8],
100        action: <PS::Policy as Policy>::Action<'_>,
101        sink: &mut impl Sink<PS::Effect>,
102    ) -> Result<GraphId, ClientError> {
103        let policy_id = self.policy_store.add_policy(policy_data)?;
104        let policy = self.policy_store.get_policy(policy_id)?;
105
106        let mut perspective = self.provider.new_perspective(policy_id);
107        sink.begin();
108        policy
109            .call_action(action, &mut perspective, sink, ActionPlacement::OnGraph)
110            .inspect_err(|_| sink.rollback())?;
111        sink.commit();
112
113        let (graph_id, _) = self.provider.new_storage(perspective)?;
114
115        Ok(graph_id)
116    }
117
118    /// Remove a graph (AKA Team). The graph commands will be removed from storage.
119    pub fn remove_graph(&mut self, graph_id: GraphId) -> Result<(), ClientError> {
120        self.provider.remove_storage(graph_id)?;
121
122        Ok(())
123    }
124
125    /// Commit the [`Transaction`] to storage, after merging all temporary heads.
126    ///
127    /// `make_spill` is called whenever an internal braid or convergence map
128    /// needs byte-addressable overflow storage. It is typically
129    /// `|| LibcSpill::new(&dir)` for production callers or `MemSpill::new`
130    /// for tests.
131    ///
132    /// Returns whether any new commands were added.
133    pub fn commit<F, MS>(
134        &mut self,
135        trx: Transaction<SP, PS>,
136        sink: &mut impl Sink<PS::Effect>,
137        buffers: &mut RuntimeBuffers<SP::Segment>,
138        make_spill: MS,
139    ) -> Result<bool, ClientError>
140    where
141        F: Spill,
142        MS: Fn() -> Result<F, StorageError>,
143    {
144        trx.commit::<F, MS>(
145            &mut self.provider,
146            &mut self.policy_store,
147            sink,
148            buffers,
149            &make_spill,
150        )
151    }
152
153    /// Add commands to the transaction, writing the results to
154    /// `sink`.
155    ///
156    /// `make_spill` is called whenever an internal braid or convergence map
157    /// needs byte-addressable overflow storage. See [`Self::commit`].
158    ///
159    /// Returns the number of commands that were added.
160    pub fn add_commands<F, MS>(
161        &mut self,
162        trx: &mut Transaction<SP, PS>,
163        sink: &mut impl Sink<PS::Effect>,
164        commands: &[impl Command],
165        buffers: &mut RuntimeBuffers<SP::Segment>,
166        make_spill: MS,
167    ) -> Result<usize, ClientError>
168    where
169        F: Spill,
170        MS: Fn() -> Result<F, StorageError>,
171    {
172        trx.add_commands::<F, MS>(
173            commands,
174            &mut self.provider,
175            &mut self.policy_store,
176            sink,
177            buffers,
178            &make_spill,
179        )
180    }
181
182    pub fn update_heads<I>(
183        &mut self,
184        graph_id: GraphId,
185        addrs: I,
186        request_heads: &mut PeerCache,
187        buffer: &mut TraversalBuffer,
188    ) -> Result<(), ClientError>
189    where
190        I: IntoIterator<Item = Address>,
191        I::IntoIter: DoubleEndedIterator,
192    {
193        let storage = self.provider.get_storage(graph_id)?;
194
195        // Commands in sync messages are always ancestor-first (lower max_cut to higher max_cut).
196        // Reverse the iterator to process highest max_cut first, which allows us to skip ancestors
197        // since if a command is an ancestor of one we've already added, we don't need to add it.
198        for address in addrs.into_iter().rev() {
199            request_heads.add_command(storage, address, buffer)?;
200        }
201
202        Ok(())
203    }
204
205    /// Returns the address of the head of the graph.
206    pub fn head_address(&mut self, graph_id: GraphId) -> Result<Address, ClientError> {
207        let storage = self.provider.get_storage(graph_id)?;
208        let address = storage.get_head_address()?;
209        Ok(address)
210    }
211
212    /// Returns the address to advertise in a sync hello notification.
213    ///
214    /// A single-head graph advertises its head. A multi-head graph advertises
215    /// the deterministic merge its head set would collapse to, computed
216    /// without writing anything to storage: a peer holding the same head set
217    /// computes the same address, and a peer that already collapsed these
218    /// heads (by committing a command on top) has this exact command in its
219    /// graph. See [`Self::should_sync_on_hello`] for the receiving side.
220    pub fn hello_head(&mut self, graph_id: GraphId) -> Result<Address, ClientError> {
221        let storage = &*self.provider.get_storage(graph_id)?;
222        let heads = storage.get_heads()?;
223        transaction::synthetic_head(storage, &self.policy_store, heads)
224    }
225
226    /// Returns whether a hello notification advertising `head` warrants
227    /// syncing from that peer.
228    ///
229    /// No sync is needed if the advertised address matches this graph's own
230    /// [`Self::hello_head`] (both sides hold the same head set) or if the
231    /// command is already in the graph (this side is ahead). A peer that has
232    /// extended one of our heads advertises an address that fails both
233    /// checks, costing one sync that transfers nothing; that false positive
234    /// is accepted. A missing graph always warrants a sync.
235    pub fn should_sync_on_hello(
236        &mut self,
237        graph_id: GraphId,
238        head: Address,
239        buffer: &mut TraversalBuffer,
240    ) -> Result<bool, ClientError> {
241        match self.provider.get_storage(graph_id) {
242            Err(StorageError::NoSuchStorage) => return Ok(true),
243            Err(e) => return Err(e.into()),
244            Ok(_) => {}
245        }
246        if self.hello_head(graph_id)? == head {
247            return Ok(false);
248        }
249        let storage = self.provider.get_storage(graph_id)?;
250        Ok(storage.get_location(head, buffer)?.is_none())
251    }
252
253    /// Performs an `action`, writing the results to `sink`.
254    ///
255    /// `make_spill` is called whenever collapsing a multi-head graph needs
256    /// byte-addressable overflow storage for an internal braid. See
257    /// [`Self::commit`].
258    pub fn action<F, MS>(
259        &mut self,
260        graph_id: GraphId,
261        sink: &mut impl Sink<PS::Effect>,
262        action: <PS::Policy as Policy>::Action<'_>,
263        buffers: &mut RuntimeBuffers<SP::Segment>,
264        make_spill: MS,
265    ) -> Result<(), ClientError>
266    where
267        F: Spill,
268        MS: Fn() -> Result<F, StorageError>,
269    {
270        let storage = self.provider.get_storage(graph_id)?;
271
272        // A new command needs a single parent: collapse the head set, which may
273        // run a braid (hence the buffers and spill).
274        let heads = storage.get_heads()?.clone();
275        let head = transaction::collapse_heads::<_, PS, F, _>(
276            storage,
277            &mut self.policy_store,
278            heads,
279            buffers,
280            &make_spill,
281        )?;
282
283        let mut perspective = storage.get_linear_perspective(head)?;
284
285        let policy_id = perspective.policy();
286        let policy = self.policy_store.get_policy(policy_id)?;
287
288        // No need to checkpoint the perspective since it is only for this action.
289        // Must checkpoint once we add action transactions.
290
291        sink.begin();
292        match policy.call_action(action, &mut perspective, sink, ActionPlacement::OnGraph) {
293            Ok(()) => {
294                let segment = storage.write(perspective)?;
295                let new_head = LocatedAddress {
296                    id: segment.head_id(),
297                    segment: segment.index(),
298                    max_cut: segment.longest_max_cut()?,
299                };
300                let fact_cache = segment.facts()?;
301                storage.commit_heads(HeadSet::single(new_head), fact_cache)?;
302                sink.commit();
303                Ok(())
304            }
305            Err(e) => {
306                sink.rollback();
307                Err(e.into())
308            }
309        }
310    }
311}
312
313impl<PS, SP> ClientState<PS, SP>
314where
315    SP: StorageProvider,
316{
317    /// Create a new [`Transaction`], used to receive [`Command`]s when syncing.
318    pub fn transaction(&mut self, graph_id: GraphId) -> Transaction<SP, PS> {
319        Transaction::new(graph_id)
320    }
321
322    /// Create an ephemeral [`Session`] associated with this client.
323    pub fn session(&mut self, graph_id: GraphId) -> Result<Session<SP, PS>, ClientError> {
324        Session::new(&mut self.provider, graph_id)
325    }
326
327    /// Checks if a command with the given address exists in the specified graph.
328    ///
329    /// Returns `true` if the command exists, `false` if it doesn't exist or the graph doesn't exist.
330    /// This method is used to determine if we need to sync when a hello message is received.
331    pub fn command_exists(
332        &mut self,
333        graph_id: GraphId,
334        address: Address,
335        buffer: &mut TraversalBuffer,
336    ) -> bool {
337        let Ok(storage) = self.provider.get_storage(graph_id) else {
338            // Graph doesn't exist
339            return false;
340        };
341        storage
342            .get_location(address, buffer)
343            .unwrap_or(None)
344            .is_some()
345    }
346}