1use p2panda_core::prune::validate_prunable_backlink;
5use p2panda_core::{
6 AnyHeader, AnyOperation, Extensions, Hash, LogId, Operation, SeqNum, VerifyingKey,
7};
8use p2panda_store::Transaction;
9use p2panda_store::logs::LogStore;
10use p2panda_store::operations::OperationStore;
11use p2panda_store::topics::TopicStore;
12use thiserror::Error;
13
14use crate::ingest::ooo::{OooBuffer, OooResult};
15
16#[derive(Clone, Debug, PartialEq, Eq)]
19pub enum IngestResult<E> {
20 Inserted,
22
23 AlreadyExists,
25
26 Ordered(Vec<Operation<E>>),
30
31 OutOfOrder,
33
34 Outdated,
36}
37
38pub async fn ingest_operation<S, L, E, TP>(
48 store: &S,
49 ooo: Option<&OooBuffer<L, E>>,
50 operation: &Operation<E>,
53 log_id: &L,
54 topic: &TP,
55 prune_flag: bool,
56) -> Result<IngestResult<E>, IngestError>
57where
58 S: Transaction
59 + OperationStore<Operation<E>, Hash>
60 + LogStore<AnyOperation, VerifyingKey, L, SeqNum, Hash>
61 + TopicStore<TP, VerifyingKey, L>,
62 L: LogId,
63 E: Extensions,
64{
65 if operation.hash != operation.header.hash() {
70 return Err(IngestError::HashMismatch);
71 }
72
73 p2panda_core::validate_operation(operation).map_err(IngestError::InvalidOperation)?;
75
76 let permit = store
77 .begin()
78 .await
79 .map_err(|err| IngestError::StoreError(err.to_string()))?;
80
81 let already_exists = store
86 .has_operation_tx(&operation.hash)
87 .await
88 .map_err(|err| IngestError::StoreError(err.to_string()))?;
89
90 if already_exists {
91 let verifying_key = operation.header.verifying_key;
94 <S as TopicStore<TP, VerifyingKey, L>>::associate_tx(store, topic, &verifying_key, log_id)
95 .await
96 .map_err(|err| IngestError::StoreError(err.to_string()))?;
97
98 store
99 .commit(permit)
100 .await
101 .map_err(|err| IngestError::StoreError(err.to_string()))?;
102
103 return Ok(IngestResult::AlreadyExists);
104 }
105
106 let result = if let Some(ooo) = ooo {
110 let latest_header = store
112 .get_latest_entry_tx(&operation.header.verifying_key, log_id)
113 .await
114 .map_err(|err| IngestError::StoreError(err.to_string()))?
115 .map(|operation| operation.header);
116
117 match ooo
120 .process(operation, latest_header.as_ref(), log_id, prune_flag)
121 .await
122 {
123 OooResult::InOrder(operation) => {
125 check_log_and_insert(store, operation, log_id, topic, prune_flag).await?;
126 IngestResult::Inserted
127 }
128
129 OooResult::Ordered(operations) => {
131 for operation in &operations {
132 check_log_and_insert(store, operation, log_id, topic, prune_flag).await?;
133 }
134
135 IngestResult::Ordered(operations)
136 }
137
138 OooResult::OutOfOrder => return Ok(IngestResult::OutOfOrder),
139 OooResult::Outdated => return Ok(IngestResult::Outdated),
140 }
141 } else {
142 check_log_and_insert(store, operation, log_id, topic, prune_flag).await?;
145 IngestResult::Inserted
146 };
147
148 store
149 .commit(permit)
150 .await
151 .map_err(|err| IngestError::StoreError(err.to_string()))?;
152
153 Ok(result)
154}
155
156async fn check_log_and_insert<S, L, E, TP>(
157 store: &S,
158 operation: &Operation<E>,
159 log_id: &L,
160 topic: &TP,
161 prune_flag: bool,
162) -> Result<(), IngestError>
163where
164 S: Transaction
165 + OperationStore<Operation<E>, Hash>
166 + LogStore<AnyOperation, VerifyingKey, L, SeqNum, Hash>
167 + TopicStore<TP, VerifyingKey, L>,
168 L: LogId,
169 E: Extensions,
170{
171 let latest_header = store
175 .get_latest_entry_tx(&operation.header.verifying_key, log_id)
176 .await
177 .map_err(|err| IngestError::StoreError(err.to_string()))?
178 .map(|operation| operation.header);
179
180 validate_prunable_backlink(
186 latest_header.as_ref(),
187 &operation.header.clone().into(),
188 prune_flag,
189 )
190 .map_err(IngestError::InvalidOperation)?;
191
192 let verifying_key = operation.header.verifying_key;
197
198 store
199 .insert_operation_tx(&operation.hash, operation, log_id)
200 .await
201 .map_err(|err| IngestError::StoreError(err.to_string()))?;
202
203 <S as TopicStore<TP, VerifyingKey, L>>::associate_tx(store, topic, &verifying_key, log_id)
204 .await
205 .map_err(|err| IngestError::StoreError(err.to_string()))?;
206
207 Ok(())
208}
209
210pub async fn validate_operation<S, L, E, TP>(
212 store: &S,
213 operation: &Operation<E>,
214 log_id: &L,
215 prune_flag: bool,
216) -> Result<(), IngestError>
217where
218 S: Transaction
219 + OperationStore<Operation<E>, Hash>
220 + LogStore<AnyOperation, VerifyingKey, L, SeqNum, Hash>
221 + TopicStore<TP, VerifyingKey, L>,
222 L: LogId,
223 E: Extensions,
224{
225 if operation.hash != operation.header.hash() {
227 return Err(IngestError::HashMismatch);
228 }
229
230 p2panda_core::validate_operation(operation).map_err(IngestError::InvalidOperation)?;
232
233 let permit = store
234 .begin()
235 .await
236 .map_err(|err| IngestError::StoreError(err.to_string()))?;
237
238 let latest_header = store
239 .get_latest_entry_tx(&operation.header.verifying_key, log_id)
240 .await
241 .map_err(|err| IngestError::StoreError(err.to_string()))?
242 .map(|operation| operation.header);
243
244 let header: AnyHeader = operation.header.clone().into();
250 validate_prunable_backlink(latest_header.as_ref(), &header, prune_flag)
251 .map_err(IngestError::InvalidOperation)?;
252
253 store
254 .commit(permit)
255 .await
256 .map_err(|err| IngestError::StoreError(err.to_string()))?;
257
258 Ok(())
259}
260
261#[derive(Clone, Debug, PartialEq, Error)]
263pub enum IngestError {
264 #[error("invalid operation: {0}")]
267 InvalidOperation(#[from] p2panda_core::OperationError),
268
269 #[error("hash associated with operation does not match header digest")]
271 HashMismatch,
272
273 #[error("critical storage failure: {0}")]
275 StoreError(String),
276}
277
278#[cfg(test)]
279mod tests {
280 use std::assert_matches;
281
282 use p2panda_core::test_utils::TestLog;
283 use p2panda_core::{Hash, Header, Operation, SigningKey, Topic, VerifyingKey};
284 use p2panda_store::logs::LogStore;
285 use p2panda_store::topics::TopicStore;
286 use p2panda_store::{SqliteStore, tx_unwrap};
287
288 use crate::ingest::ooo::OooBuffer;
289
290 use super::{IngestResult, ingest_operation};
291
292 #[tokio::test]
293 async fn valid_log() {
294 let store = SqliteStore::temporary().await;
295 let log = TestLog::new();
296
297 for i in 0..128 {
298 let operation = log.operation(format!("{i}").as_bytes(), ());
299 let result = ingest_operation(&store, None, &operation, &1, &1, false).await;
300 assert!(result.is_ok());
301 }
302 }
303
304 #[tokio::test]
305 async fn deduplicate_operations() {
306 let store = SqliteStore::temporary().await;
307 let log = TestLog::new();
308 let operation = log.operation(b"same same", ());
309
310 let result = ingest_operation(&store, None, &operation, &1, &1, false)
311 .await
312 .unwrap();
313 std::assert_matches!(result, IngestResult::<()>::Inserted);
314
315 let result = ingest_operation(&store, None, &operation, &1, &1, false)
317 .await
318 .unwrap();
319 std::assert_matches!(result, IngestResult::AlreadyExists);
320 }
321
322 #[tokio::test]
323 async fn topic_association() {
324 let store = SqliteStore::temporary().await;
325
326 let log_0 = TestLog::new();
327 let log_1 = TestLog::new();
328 let log_2 = TestLog::new();
329
330 let dogs = [2; 32];
331 let cats = [3; 32];
332
333 ingest_operation(&store, None, &log_0.operation(b"Do", ()), &0, &dogs, false)
334 .await
335 .unwrap();
336
337 ingest_operation(&store, None, &log_0.operation(b"Re", ()), &0, &dogs, false)
338 .await
339 .unwrap();
340
341 ingest_operation(&store, None, &log_1.operation(b"Mi", ()), &1, &dogs, false)
342 .await
343 .unwrap();
344
345 ingest_operation(&store, None, &log_2.operation(b"Fa", ()), &2, &cats, false)
346 .await
347 .unwrap();
348
349 ingest_operation(&store, None, &log_2.operation(b"So", ()), &2, &cats, false)
350 .await
351 .unwrap();
352
353 ingest_operation(&store, None, &log_2.operation(b"La", ()), &2, &cats, false)
354 .await
355 .unwrap();
356
357 let authors =
359 <SqliteStore as TopicStore<[u8; 32], VerifyingKey, usize>>::resolve(&store, &dogs)
360 .await
361 .unwrap();
362 assert_eq!(*authors.get(&log_0.author()).unwrap(), [0]);
363 assert_eq!(*authors.get(&log_1.author()).unwrap(), [1]);
364
365 let operation = tx_unwrap!(
366 store,
367 store
368 .get_latest_entry_tx(&log_0.author(), &0)
369 .await
370 .unwrap()
371 .unwrap()
372 );
373 assert_eq!(operation.header.seq_num, 1);
374
375 let authors =
377 <SqliteStore as TopicStore<[u8; 32], VerifyingKey, usize>>::resolve(&store, &cats)
378 .await
379 .unwrap();
380 assert_eq!(*authors.get(&log_2.author()).unwrap(), [2]);
381
382 let operation = tx_unwrap!(
383 store,
384 store
385 .get_latest_entry_tx(&log_2.author(), &2)
386 .await
387 .unwrap()
388 .unwrap()
389 );
390 assert_eq!(operation.header.seq_num, 2);
391 }
392
393 #[tokio::test]
394 async fn missing_prefix() {
395 let store = SqliteStore::temporary().await;
396 let signing_key = SigningKey::generate();
397
398 let header = Header::builder()
401 .chain(12, Hash::digest(b"mock operation"))
403 .build(&signing_key, ());
404
405 let operation = Operation::from_parts(header, None);
406 let result = ingest_operation(&store, None, &operation, &1, &1, false).await;
407
408 assert!(result.is_err());
409 }
410
411 #[tokio::test]
412 async fn ignore_outdated_pruned_operations() {
413 let store = SqliteStore::temporary().await;
414 let signing_key = SigningKey::generate();
415
416 let header = Header::builder()
419 .chain(1, Hash::digest(b"mock operation"))
420 .build(&signing_key, ());
421 let operation = Operation::from_parts(header, None);
422
423 let prune_flag = true; let result = ingest_operation(&store, None, &operation, &1, &1, prune_flag).await;
425 assert!(result.is_ok());
426
427 let header = Header::builder().build(&signing_key, ());
429 let operation = Operation::from_parts(header, None);
430
431 let result = ingest_operation(&store, None, &operation, &1, &1, false).await;
432 assert!(result.is_err());
433 }
434
435 #[tokio::test]
436 async fn ooo_operation_before_pruning_point() {
437 let log = TestLog::new();
438
439 let store = SqliteStore::temporary().await;
440 let ooo = OooBuffer::with_capacity(32);
441
442 let operation_0 = log.operation(b"This is a poopy message.", ());
443 let operation_1 = log.operation(b"There's nothing to see.", ());
444
445 let log_id = 0;
446 let topic = Topic::random();
447
448 let result =
451 ingest_operation(&store, Some(&ooo), &operation_1, &log_id, &topic, true).await;
452 assert_matches!(result, Ok(IngestResult::<()>::Inserted));
453
454 let result =
457 ingest_operation(&store, Some(&ooo), &operation_0, &log_id, &topic, false).await;
458 assert_matches!(result, Ok(IngestResult::Outdated));
459 }
460
461 #[tokio::test]
462 async fn ooo_operations() {
463 let log = TestLog::new();
464
465 let store = SqliteStore::temporary().await;
466 let ooo = OooBuffer::with_capacity(32);
467
468 let operation_0 = log.operation(b"Order", ());
469 let operation_1 = log.operation(b"Please", ());
470 let operation_2 = log.operation(b"!", ());
471
472 let log_id = 0;
473 let topic = Topic::random();
474
475 let result =
476 ingest_operation(&store, Some(&ooo), &operation_1, &log_id, &topic, false).await;
477 assert_matches!(result, Ok(IngestResult::OutOfOrder));
478
479 let result =
480 ingest_operation(&store, Some(&ooo), &operation_2, &log_id, &topic, false).await;
481 assert_matches!(result, Ok(IngestResult::OutOfOrder));
482
483 let result =
484 ingest_operation(&store, Some(&ooo), &operation_0, &log_id, &topic, false).await;
485 assert_eq!(
486 result,
487 Ok(IngestResult::Ordered(vec![
488 operation_0,
489 operation_1,
490 operation_2
491 ]))
492 );
493 }
494}