1use std::collections::BTreeSet;
4use std::fmt;
5use std::sync::{Mutex, MutexGuard};
6
7use serde::{Deserialize, Serialize};
8use softrelay_negentropy::{Id, Negentropy, NegentropyStorageVector};
9
10use crate::{NostrEventId, SoftchatError};
11
12pub const MAX_NEGENTROPY_ITEMS: usize = 100_000;
14pub const MIN_NEGENTROPY_FRAME_BYTES: u64 = 4_096;
16pub const MAX_NEGENTROPY_FRAME_BYTES: u64 = 512 * 1_024;
18
19#[derive(Clone, Debug, Deserialize, Eq, PartialEq, Serialize)]
21#[serde(rename_all = "camelCase", deny_unknown_fields)]
22#[cfg_attr(feature = "native-bindings", derive(uniffi::Record))]
23pub struct NegentropyItem {
24 pub created_at: i64,
26 pub event_id: String,
28}
29
30#[derive(Clone, Debug, Eq, PartialEq, Serialize)]
32#[serde(rename_all = "camelCase")]
33#[cfg_attr(feature = "native-bindings", derive(uniffi::Record))]
34pub struct NegentropyStep {
35 pub outgoing: Option<Vec<u8>>,
37 pub have_event_ids: Vec<String>,
39 pub need_event_ids: Vec<String>,
41 pub complete: bool,
43}
44
45#[cfg_attr(feature = "native-bindings", derive(uniffi::Object))]
47pub struct NegentropyClient {
48 engine: Mutex<Negentropy<'static, NegentropyStorageVector>>,
49 initiated: Mutex<bool>,
50}
51
52impl fmt::Debug for NegentropyClient {
53 fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
54 formatter
55 .debug_struct("NegentropyClient")
56 .finish_non_exhaustive()
57 }
58}
59
60#[cfg_attr(feature = "native-bindings", uniffi::export)]
61impl NegentropyClient {
62 #[cfg_attr(feature = "native-bindings", uniffi::constructor)]
69 pub fn new(items: Vec<NegentropyItem>, frame_size_limit: u64) -> Result<Self, SoftchatError> {
70 if items.len() > MAX_NEGENTROPY_ITEMS
71 || !(MIN_NEGENTROPY_FRAME_BYTES..=MAX_NEGENTROPY_FRAME_BYTES)
72 .contains(&frame_size_limit)
73 {
74 return Err(SoftchatError::InvalidNegentropy);
75 }
76
77 let mut storage = NegentropyStorageVector::with_capacity(items.len());
78 let mut unique_ids = BTreeSet::new();
79 for item in items {
80 let created_at =
81 u64::try_from(item.created_at).map_err(|_| SoftchatError::InvalidNegentropy)?;
82 let event_id = NostrEventId::from_hex(&item.event_id)
83 .map_err(|_| SoftchatError::InvalidNegentropy)?;
84 if !unique_ids.insert(event_id.to_hex()) {
85 return Err(SoftchatError::InvalidNegentropy);
86 }
87 storage
88 .insert(
89 created_at,
90 Id::from_byte_array(*event_id.as_inner().as_bytes()),
91 )
92 .map_err(|_| SoftchatError::InvalidNegentropy)?;
93 }
94 storage
95 .seal()
96 .map_err(|_| SoftchatError::InvalidNegentropy)?;
97 let engine = Negentropy::owned(storage, frame_size_limit)
98 .map_err(|_| SoftchatError::InvalidNegentropy)?;
99 Ok(Self {
100 engine: Mutex::new(engine),
101 initiated: Mutex::new(false),
102 })
103 }
104
105 pub fn initiate(&self) -> Result<Vec<u8>, SoftchatError> {
111 let mut initiated = lock(&self.initiated);
112 if *initiated {
113 return Err(SoftchatError::InvalidNegentropy);
114 }
115 let output = lock(&self.engine)
116 .initiate()
117 .map_err(|_| SoftchatError::InvalidNegentropy)?;
118 *initiated = true;
119 Ok(output)
120 }
121
122 pub fn reconcile(&self, response: Vec<u8>) -> Result<NegentropyStep, SoftchatError> {
129 if !*lock(&self.initiated) || response.len() > MAX_NEGENTROPY_FRAME_BYTES as usize {
130 return Err(SoftchatError::InvalidNegentropy);
131 }
132 let mut have_ids = Vec::new();
133 let mut need_ids = Vec::new();
134 let outgoing = lock(&self.engine)
135 .reconcile_with_ids(&response, &mut have_ids, &mut need_ids)
136 .map_err(|_| SoftchatError::InvalidNegentropy)?;
137 let have_event_ids = have_ids
138 .into_iter()
139 .map(|id| hex::encode(id.to_bytes()))
140 .collect();
141 let need_event_ids = need_ids
142 .into_iter()
143 .map(|id| hex::encode(id.to_bytes()))
144 .collect();
145 Ok(NegentropyStep {
146 complete: outgoing.is_none(),
147 outgoing,
148 have_event_ids,
149 need_event_ids,
150 })
151 }
152}
153
154fn lock<T>(mutex: &Mutex<T>) -> MutexGuard<'_, T> {
155 match mutex.lock() {
156 Ok(guard) => guard,
157 Err(poisoned) => poisoned.into_inner(),
158 }
159}
160
161#[cfg(test)]
162#[allow(clippy::unwrap_used)]
163mod tests {
164 use softrelay_negentropy::{NegentropyStorageVector, Storage};
165
166 use super::*;
167
168 fn item(created_at: i64, byte: u8) -> NegentropyItem {
169 NegentropyItem {
170 created_at,
171 event_id: hex::encode([byte; 32]),
172 }
173 }
174
175 #[test]
176 fn reconciles_against_reviewed_softrelay_engine() {
177 let client = NegentropyClient::new(vec![item(1, 0x11), item(3, 0x33)], 4_096).unwrap();
178 let initial = client.initiate().unwrap();
179
180 let mut relay_storage = NegentropyStorageVector::new();
181 relay_storage
182 .insert(2, Id::from_byte_array([0x22; 32]))
183 .unwrap();
184 relay_storage
185 .insert(3, Id::from_byte_array([0x33; 32]))
186 .unwrap();
187 relay_storage.seal().unwrap();
188 let mut relay = Negentropy::new(Storage::Borrowed(&relay_storage), 4_096).unwrap();
189
190 let response = relay.reconcile(&initial).unwrap();
191 let step = client.reconcile(response).unwrap();
192 assert!(step.complete);
193 assert_eq!(step.have_event_ids, vec![hex::encode([0x11; 32])]);
194 assert_eq!(step.need_event_ids, vec![hex::encode([0x22; 32])]);
195 }
196
197 #[test]
198 fn bounds_ids_timestamps_frames_and_state_order() {
199 assert!(matches!(
200 NegentropyClient::new(vec![item(-1, 0x11)], 4_096),
201 Err(SoftchatError::InvalidNegentropy)
202 ));
203 assert!(matches!(
204 NegentropyClient::new(vec![item(1, 0x11), item(2, 0x11)], 4_096),
205 Err(SoftchatError::InvalidNegentropy)
206 ));
207 assert!(matches!(
208 NegentropyClient::new(vec![], 4_095),
209 Err(SoftchatError::InvalidNegentropy)
210 ));
211 let client = NegentropyClient::new(vec![], 4_096).unwrap();
212 assert!(matches!(
213 client.reconcile(vec![0x61]),
214 Err(SoftchatError::InvalidNegentropy)
215 ));
216 client.initiate().unwrap();
217 assert!(matches!(
218 client.initiate(),
219 Err(SoftchatError::InvalidNegentropy)
220 ));
221 }
222}