derec_library/protocol/traits.rs
1// SPDX-License-Identifier: Apache-2.0
2// Copyright (c) 2026 DeRec Alliance. All rights reserved.
3
4use super::error::{ChannelStoreError, SecretStoreError, ShareStoreError, StateStoreError};
5use crate::Result;
6use crate::protocol::types::{
7 Channel, MissingPolicy, SecretKind, SecretValue, Share, StateItem, StateKey, StateKind,
8 UserSecrets,
9};
10use crate::types::ChannelId;
11use derec_proto::TransportProtocol;
12use std::{future::Future, pin::Pin};
13
14/// Type-erased future returned by [`DeRecSecretStore`] methods.
15///
16/// `Send` on native targets so multi-threaded executors (e.g. `tokio::spawn`)
17/// can take it; under the `ffi` feature or `wasm32` the `Send` bound is
18/// dropped because callbacks cross an FFI boundary or run in a
19/// single-threaded host. Sync backends can return
20/// `Box::pin(std::future::ready(...))` at zero cost.
21#[cfg(any(feature = "ffi", target_arch = "wasm32"))]
22pub type SecretStoreFuture<'a, T> =
23 Pin<Box<dyn Future<Output = std::result::Result<T, SecretStoreError>> + 'a>>;
24/// Type-erased future returned by [`DeRecSecretStore`] methods.
25///
26/// `Send` on native targets so multi-threaded executors (e.g. `tokio::spawn`)
27/// can take it; under the `ffi` feature or `wasm32` the `Send` bound is
28/// dropped because callbacks cross an FFI boundary or run in a
29/// single-threaded host. Sync backends can return
30/// `Box::pin(std::future::ready(...))` at zero cost.
31#[cfg(not(any(feature = "ffi", target_arch = "wasm32")))]
32pub type SecretStoreFuture<'a, T> =
33 Pin<Box<dyn Future<Output = std::result::Result<T, SecretStoreError>> + Send + 'a>>;
34
35/// Type-erased future returned by [`DeRecChannelStore`] methods. See
36/// [`SecretStoreFuture`] for the `Send`/non-`Send` rules.
37#[cfg(any(feature = "ffi", target_arch = "wasm32"))]
38pub type ChannelStoreFuture<'a, T> =
39 Pin<Box<dyn Future<Output = std::result::Result<T, ChannelStoreError>> + 'a>>;
40/// Type-erased future returned by [`DeRecChannelStore`] methods. See
41/// [`SecretStoreFuture`] for the `Send`/non-`Send` rules.
42#[cfg(not(any(feature = "ffi", target_arch = "wasm32")))]
43pub type ChannelStoreFuture<'a, T> =
44 Pin<Box<dyn Future<Output = std::result::Result<T, ChannelStoreError>> + Send + 'a>>;
45
46/// Type-erased future returned by [`DeRecShareStore`] methods. See
47/// [`SecretStoreFuture`] for the `Send`/non-`Send` rules.
48#[cfg(any(feature = "ffi", target_arch = "wasm32"))]
49pub type ShareStoreFuture<'a, T> =
50 Pin<Box<dyn Future<Output = std::result::Result<T, ShareStoreError>> + 'a>>;
51/// Type-erased future returned by [`DeRecShareStore`] methods. See
52/// [`SecretStoreFuture`] for the `Send`/non-`Send` rules.
53#[cfg(not(any(feature = "ffi", target_arch = "wasm32")))]
54pub type ShareStoreFuture<'a, T> =
55 Pin<Box<dyn Future<Output = std::result::Result<T, ShareStoreError>> + Send + 'a>>;
56
57/// Type-erased future returned by [`DeRecTransport::send`]. See
58/// [`SecretStoreFuture`] for the `Send`/non-`Send` rules.
59#[cfg(any(feature = "ffi", target_arch = "wasm32"))]
60pub type TransportFuture<'a> = Pin<Box<dyn Future<Output = Result<()>> + 'a>>;
61/// Type-erased future returned by [`DeRecTransport::send`]. See
62/// [`SecretStoreFuture`] for the `Send`/non-`Send` rules.
63#[cfg(not(any(feature = "ffi", target_arch = "wasm32")))]
64pub type TransportFuture<'a> = Pin<Box<dyn Future<Output = Result<()>> + Send + 'a>>;
65
66/// Type-erased future returned by [`DeRecStateStore`] methods. See
67/// [`SecretStoreFuture`] for the `Send`/non-`Send` rules.
68#[cfg(any(feature = "ffi", target_arch = "wasm32"))]
69pub type StateStoreFuture<'a, T> =
70 Pin<Box<dyn Future<Output = std::result::Result<T, StateStoreError>> + 'a>>;
71/// Type-erased future returned by [`DeRecStateStore`] methods. See
72/// [`SecretStoreFuture`] for the `Send`/non-`Send` rules.
73#[cfg(not(any(feature = "ffi", target_arch = "wasm32")))]
74pub type StateStoreFuture<'a, T> =
75 Pin<Box<dyn Future<Output = std::result::Result<T, StateStoreError>> + Send + 'a>>;
76
77/// Keychain-grade storage for the protocol's per-channel cryptographic state.
78///
79/// Holds three kinds of material (see [`SecretKind`]):
80/// [`SecretKind::SharedKey`] and [`SecretKind::PairingSecret`] are
81/// sensitive — implementations should persist them with keychain-grade
82/// protection. [`SecretKind::PairingContact`] is a transient public-key blob
83/// that only needs durable storage.
84///
85/// # VSS guarantee
86///
87/// Individual Verifiable Secret Sharing shares reveal **zero** information
88/// about the original secret (information-theoretic security), so share
89/// storage does **not** require this trait.
90///
91/// # Executor independence
92///
93/// Methods return [`SecretStoreFuture`] — a type-erased [`std::future::Future`]
94/// that any executor can poll. Sync implementations return
95/// `Box::pin(std::future::ready(...))` at zero cost; async implementations
96/// return `Box::pin(async move { ... })`. No runtime is prescribed; see
97/// [`SecretStoreFuture`] for the per-target `Send` rules.
98///
99/// # Concurrency
100///
101/// The protocol holds each store by `&mut Self`, so implementations never
102/// see overlapping calls and need no internal synchronization.
103pub trait DeRecSecretStore {
104 /// Load a secret for the given `(secret_id, channel_id)` pair.
105 ///
106 /// `secret_id` partitions storage so a single backend can serve many
107 /// secrets on the same device (Owner of N secrets, or Helper for N
108 /// Owners). Returns `Ok(None)` when no entry of the requested
109 /// [`SecretKind`] exists for this partition key.
110 fn load(
111 &self,
112 secret_id: u64,
113 channel_id: ChannelId,
114 kind: SecretKind,
115 ) -> SecretStoreFuture<'_, Option<SecretValue>>;
116
117 /// Load secrets of the same [`SecretKind`] for several channels in
118 /// one call, scoped to `secret_id`.
119 ///
120 /// Used by the [`crate::protocol`] orchestrator when broadcasting a
121 /// request (discovery, recovery, verification, sharing, unpairing) to
122 /// keep the per-broadcast roundtrip count constant instead of linear
123 /// in the number of paired channels.
124 fn load_many(
125 &self,
126 secret_id: u64,
127 channel_ids: &[ChannelId],
128 kind: SecretKind,
129 missing_policy: MissingPolicy,
130 ) -> SecretStoreFuture<'_, Vec<(ChannelId, SecretValue)>>;
131
132 /// Persist a secret for the given `(secret_id, channel_id)` pair.
133 ///
134 /// The [`SecretKind`] is derived from the [`SecretValue`] variant.
135 /// An existing entry of the same kind under the same partition is
136 /// silently overwritten.
137 fn save(
138 &mut self,
139 secret_id: u64,
140 channel_id: ChannelId,
141 value: SecretValue,
142 ) -> SecretStoreFuture<'_, ()>;
143
144 /// Remove a secret for the given `(secret_id, channel_id)` pair.
145 /// Idempotent: removing a non-existent entry is `Ok(())`.
146 fn remove(
147 &mut self,
148 secret_id: u64,
149 channel_id: ChannelId,
150 kind: SecretKind,
151 ) -> SecretStoreFuture<'_, ()>;
152}
153
154/// Storage backend for paired channels.
155///
156/// A [`Channel`] is the post-pairing representation of a peer relationship,
157/// retaining only the fields needed for ongoing protocol operations — see
158/// [`Channel`] for the per-field documentation. The full
159/// [`derec_proto::ContactMessage`] — which carries ephemeral cryptographic
160/// material — is discarded after pairing.
161///
162/// # Implementor notes
163///
164/// - [`load`](DeRecChannelStore::load) returns `Ok(None)` when no channel
165/// exists for the given ID.
166/// - [`save`](DeRecChannelStore::save) silently replaces any previously stored
167/// channel with the same ID.
168///
169/// # Channel linking
170///
171/// The channel store also owns the **channel-link graph**: a record that two
172/// channels belong to the same Owner identity (e.g. after a recovery
173/// re-pairing). [`link_channel`](DeRecChannelStore::link_channel) records one
174/// undirected, idempotent, transitive edge;
175/// [`linked_channels`](DeRecChannelStore::linked_channels) returns a channel's
176/// whole connected component. Linking moves no share data — it is pure
177/// relationship metadata. Recovery/discovery resolves the linked set here, then
178/// loads the corresponding shares via [`DeRecShareStore::load_many`].
179///
180/// # Executor independence
181///
182/// Same as [`DeRecSecretStore`]; methods return [`ChannelStoreFuture`].
183pub trait DeRecChannelStore {
184 /// Load the [`Channel`] for `(secret_id, channel_id)`.
185 ///
186 /// `secret_id` partitions storage so one backend can serve many
187 /// secrets on the same device. Returns `Ok(None)` when no channel
188 /// exists for this partition key.
189 fn load(
190 &self,
191 secret_id: u64,
192 channel_id: ChannelId,
193 ) -> ChannelStoreFuture<'_, Option<Channel>>;
194
195 /// Persist a [`Channel`] under `secret_id`. The channel ID is taken
196 /// from [`Channel::id`]. Replaces any previously stored channel for
197 /// the same `(secret_id, channel_id)`.
198 fn save(&mut self, secret_id: u64, channel: Channel) -> ChannelStoreFuture<'_, ()>;
199
200 /// Remove the channel for `(secret_id, channel_id)`. Returns `true`
201 /// when an entry was removed, `false` when none existed.
202 fn remove(
203 &mut self,
204 secret_id: u64,
205 channel_id: ChannelId,
206 ) -> ChannelStoreFuture<'_, bool>;
207
208 /// Return every channel stored under `secret_id`. Used by the
209 /// protocol to enumerate paired peers when building the secret bag
210 /// and when fanning out broadcast flows.
211 fn channels(&self, secret_id: u64) -> ChannelStoreFuture<'_, Vec<Channel>>;
212
213 /// Link two channels under `secret_id` as belonging to the same
214 /// Owner identity. The relation is **undirected, idempotent, and
215 /// transitive** within the partition.
216 fn link_channel(
217 &mut self,
218 secret_id: u64,
219 a: ChannelId,
220 b: ChannelId,
221 ) -> ChannelStoreFuture<'_, ()>;
222
223 /// Return the full set of channels linked to `channel_id` under
224 /// `secret_id`, **including `channel_id` itself**. An unlinked
225 /// channel returns `[channel_id]`.
226 fn linked_channels(
227 &self,
228 secret_id: u64,
229 channel_id: ChannelId,
230 ) -> ChannelStoreFuture<'_, Vec<ChannelId>>;
231}
232
233/// Storage backend for secret shares.
234///
235/// Each entry is opaque protobuf bytes keyed by `(channel_id, secret_id, version)`.
236/// The byte format depends on which side stored it; the store itself never
237/// decodes:
238///
239/// - **Helper** stores the encoded [`derec_proto::StoreShareRequestMessage`]
240/// received from the Owner. Recovery returns this whole message to the
241/// library, and verification derives the share content from
242/// `StoreShareRequestMessage.share`.
243/// - **Owner** stores the encoded [`derec_proto::CommittedDeRecShare`] it
244/// sent to each helper, so that the verification handler can replay the
245/// commitment when validating each helper's response.
246///
247/// # Relation to channel linking
248///
249/// This store is a **pure keyed store** — it never sees the channel-link
250/// graph. Linking lives in [`DeRecChannelStore`]. Callers that need shares
251/// across linked channels resolve the channel set via
252/// [`DeRecChannelStore::linked_channels`] first, then pass it to
253/// [`load_many`](DeRecShareStore::load_many).
254///
255/// # Why `secret_id` is required on filtered loads
256///
257/// Versions are namespaced by `secret_id`: the same `version` number can
258/// legitimately exist for two different secrets (e.g. a helper holds v1 from
259/// owner A and v1 from owner B). A version-only query would conflate them, so
260/// [`load`](DeRecShareStore::load) and
261/// [`load_many`](DeRecShareStore::load_many) both require `secret_id`.
262/// [`load_all`](DeRecShareStore::load_all) — the lone exception — exists
263/// for **discovery**, which by definition enumerates what's stored before any
264/// `secret_id` is known.
265///
266/// # Executor independence
267///
268/// Same as [`DeRecSecretStore`]; methods return [`ShareStoreFuture`].
269pub trait DeRecShareStore {
270 /// Load shares stored for `(secret_id, channel_id)`.
271 ///
272 /// - **Specific versions**: pass the versions you need in `versions`.
273 /// Missing versions are silently skipped.
274 /// - **All versions**: pass an empty slice.
275 fn load(
276 &self,
277 secret_id: u64,
278 channel_id: ChannelId,
279 versions: &[u32],
280 ) -> ShareStoreFuture<'_, Vec<Share>>;
281
282 /// Load shares for several channels in one call, scoped to
283 /// `secret_id`. Recovery uses this with the set returned by
284 /// [`DeRecChannelStore::linked_channels`], so it is a single
285 /// round-trip regardless of how many channels are linked.
286 fn load_many(
287 &self,
288 secret_id: u64,
289 channel_ids: &[ChannelId],
290 versions: &[u32],
291 ) -> ShareStoreFuture<'_, Vec<Share>>;
292
293 /// Load every share stored under `secret_id` across the given
294 /// channels and every version. Used by Discovery to enumerate the
295 /// helper's holdings for the active secret.
296 fn load_all(
297 &self,
298 secret_id: u64,
299 channel_ids: &[ChannelId],
300 ) -> ShareStoreFuture<'_, Vec<Share>>;
301
302 /// Return the highest version number stored for `secret_id` across
303 /// all channels, or `None` if no shares exist yet for this secret.
304 fn latest_version(&self, secret_id: u64) -> ShareStoreFuture<'_, Option<u32>>;
305
306 /// Persist a share for `(secret_id, channel_id)`.
307 ///
308 /// # Conceptual storage key
309 ///
310 /// The protocol considers the full storage key to be
311 /// `(secret_id, channel_id, share.version, share.replica_id)`.
312 /// Replica destinations reuse the source's channel shared key with
313 /// helpers (the key travels in the `ReplicaSecretPayload`), so
314 /// two replicas writing the same `(secret_id, channel_id, version)`
315 /// look cryptographically identical at the wire layer — only
316 /// `share.replica_id` separates them. A naive helper that ignored
317 /// `replica_id` and overwrote on the three-tuple key would silently
318 /// lose one of the two writes.
319 ///
320 /// # Implementation freedom
321 ///
322 /// The trait does not dictate how implementations represent the
323 /// `replica_id` discriminator (separate column, composite primary
324 /// key, write-time conflict log, etc.). The contract is:
325 ///
326 /// - Writes from distinct `replica_id`s for the same
327 /// `(secret_id, channel_id, version)` MUST both survive — neither
328 /// may silently overwrite the other.
329 /// - A write that matches an existing entry on all four fields
330 /// replaces it (idempotent re-send).
331 /// - `load`, `load_many`, and `load_all` return every distinct
332 /// `(version, replica_id)` entry matching the requested filter;
333 /// the application performs any per-application coalescing.
334 ///
335 /// `share.secret_id` is denormalized metadata and must match the
336 /// partition key `secret_id` — implementations may assert this.
337 fn save(
338 &mut self,
339 secret_id: u64,
340 channel_id: ChannelId,
341 share: Share,
342 ) -> ShareStoreFuture<'_, ()>;
343
344 /// Drop every share stored under `(secret_id, channel_id)`. Used
345 /// when an unpair flow tears down a channel. Idempotent.
346 fn remove_channel(
347 &mut self,
348 secret_id: u64,
349 channel_id: ChannelId,
350 ) -> ShareStoreFuture<'_, ()>;
351}
352
353/// Storage for the user-facing secret contents, keyed by `secret_id`.
354///
355/// One `secret_id` maps to at most one [`UserSecrets`] entry — the most
356/// recent snapshot the application handed off via
357/// `start(FlowKind::ProtectSecret)`. The pair-completion auto-publish
358/// hook reads from here so a freshly-paired Helper or Replica receives
359/// the current secret without an explicit re-publish from the app.
360///
361/// # Executor independence
362///
363/// Methods return [`ShareStoreFuture`] — same `Send` rules as the other
364/// store traits. The error type is reused from [`ShareStoreError`]
365/// because the persistence concerns overlap (latest-version bookkeeping,
366/// IO failures); no separate error category was warranted.
367///
368/// # Concurrency
369///
370/// The protocol holds the store by `&mut Self`, so implementations never
371/// see overlapping calls and need no internal synchronization.
372pub trait DeRecUserSecretStore {
373 /// Return the latest [`UserSecrets`] entry for `secret_id`, or
374 /// `Ok(None)` if the application has never published for this
375 /// `secret_id` on this instance.
376 fn load_latest(&self, secret_id: u64) -> ShareStoreFuture<'_, Option<UserSecrets>>;
377
378 /// Persist `value` as the latest entry for `secret_id`, overwriting
379 /// any prior entry. The store keeps only the latest snapshot — older
380 /// versions are recoverable via the helper share quorum if needed.
381 fn save_latest(
382 &mut self,
383 secret_id: u64,
384 value: UserSecrets,
385 ) -> ShareStoreFuture<'_, ()>;
386
387 /// Drop the entry for `secret_id`. Idempotent: removing a
388 /// non-existent entry is `Ok(())`.
389 fn remove(&mut self, secret_id: u64) -> ShareStoreFuture<'_, ()>;
390}
391
392/// Outbound transport abstraction.
393///
394/// The library calls `send` whenever it needs to deliver bytes to a peer.
395/// The `endpoint` value comes from the `TransportProtocol` stored during pairing.
396///
397/// # Executor independence
398///
399/// Same as [`DeRecSecretStore`]; `send` returns [`TransportFuture`].
400pub trait DeRecTransport {
401 /// Deliver `message` to `endpoint`.
402 ///
403 /// `endpoint` is the [`TransportProtocol`] the peer advertised during
404 /// pairing. The library calls this from protocol handlers whenever an
405 /// outbound envelope needs to reach a peer.
406 fn send(&self, endpoint: &TransportProtocol, message: Vec<u8>) -> TransportFuture<'_>;
407}
408
409/// Durable storage for the orchestrator's in-flight protocol state.
410///
411/// The `DeRecProtocol` orchestrator produces short-lived state during
412/// every flow — outstanding verification challenges, in-progress recovery
413/// accumulators, and pending unpair acknowledgements. In long-running
414/// processes this state can live in memory; in stateless deployments
415/// (serverless functions, load-balanced services with instance churn)
416/// the state must survive across process boundaries or replies will
417/// arrive to a live channel with nothing to bind them to.
418///
419/// Every backend chooses its own persistence layer — in-memory `HashMap`
420/// for local development and tests, SQLite for edge or single-process
421/// deployments, Redis / Postgres / DynamoDB for load-balanced or
422/// serverless deployments.
423///
424/// # Contract
425///
426/// - [`save`](DeRecStateStore::save) is a **full-replacement upsert**.
427/// No per-item merge or append semantic. Accumulator-style state
428/// ([`StateItem::PendingRecovery`] and [`StateItem::SharingRound`])
429/// grows via load-modify-save from the library.
430/// - [`load`](DeRecStateStore::load) is a **pure read**. No side effects.
431/// Returns `Ok(None)` when the row does not exist.
432/// - [`remove`](DeRecStateStore::remove) is idempotent: removing a
433/// missing entry is `Ok(false)`, and returning `Ok(true)` iff a row
434/// was actually removed.
435/// - [`load_all`](DeRecStateStore::load_all) returns every item of the
436/// given kind under this `secret_id`, in no guaranteed order.
437///
438/// # Concurrency
439///
440/// The library guarantees at-most-once processing of any given inbound
441/// response only in **single-instance deployments**. In multi-instance /
442/// load-balanced deployments where two instances may hold a
443/// [`DeRecProtocol`](super::DeRecProtocol) against the same
444/// `secret_id` at once:
445///
446/// - `load` + `remove` is not atomic across calls (two round-trips).
447/// - Two instances processing the same inbound response can each `load`
448/// the entry, each `remove` it, and each proceed with response
449/// handling — producing **duplicate events** to the application.
450/// - All library-emitted events (`ShareVerified`, `Unpaired`, etc.) are
451/// idempotent from the application's perspective: on-wire state has
452/// already settled, and a duplicate event does not corrupt anything.
453/// - Concurrent inbound shares racing to modify a
454/// [`StateItem::PendingRecovery`] accumulator, or concurrent inbound
455/// store-share responses racing to update a
456/// [`StateItem::SharingRound`] tally, can clobber each other via naive
457/// load-modify-save. **The application layer is responsible for
458/// serializing concurrent `process()` calls that touch the same
459/// `(secret_id, version)`** if this matters.
460///
461/// # Executor independence
462///
463/// Same as [`DeRecSecretStore`]; methods return [`StateStoreFuture`].
464///
465/// # Concurrency (single-instance)
466///
467/// The protocol holds the store by `&mut Self`, so a single-instance
468/// implementation never sees overlapping calls and needs no internal
469/// synchronization. Multi-instance backends must provide their own
470/// consistency guarantees.
471pub trait DeRecStateStore {
472 /// Insert or full-replace by `(secret_id, item.key())`. Idempotent —
473 /// if the row already exists, the existing entry is replaced in place
474 /// with the caller-supplied `item`.
475 fn save(&mut self, secret_id: u64, item: StateItem) -> StateStoreFuture<'_, ()>;
476
477 /// Read the item at `(secret_id, key)`. Returns `Ok(None)` when no
478 /// row exists. No side effects.
479 fn load(&self, secret_id: u64, key: StateKey) -> StateStoreFuture<'_, Option<StateItem>>;
480
481 /// Remove the item at `(secret_id, key)`. Returns `Ok(true)` iff a
482 /// row was removed. Idempotent — removing a missing entry is
483 /// `Ok(false)`, not an error.
484 fn remove(&mut self, secret_id: u64, key: StateKey) -> StateStoreFuture<'_, bool>;
485
486 /// Return every item of the given `kind` under this `secret_id`, in
487 /// no guaranteed order.
488 ///
489 /// Used by the library to sweep timeouts (walk
490 /// [`StateKind::PendingUnpair`], filter by
491 /// [`StateItem::PendingUnpair::started_at`]) and for
492 /// recovery-accumulator introspection. Data volume per kind is
493 /// bounded by the number of channels or active reconstruction
494 /// targets and is expected to be small.
495 fn load_all(&self, secret_id: u64, kind: StateKind) -> StateStoreFuture<'_, Vec<StateItem>>;
496}