use std::borrow::Borrow;
use p2panda_core::prune::validate_prunable_backlink;
use p2panda_core::{
Extensions, Hash, LogId, Operation, OperationError, SeqNum, VerifyingKey, validate_operation,
};
use p2panda_store::Transaction;
use p2panda_store::logs::LogStore;
use p2panda_store::operations::OperationStore;
use p2panda_store::topics::TopicStore;
use thiserror::Error;
pub async fn ingest_operation<S, T, L, E, TP>(
store: &S,
operation: &T,
log_id: &L,
topic: &TP,
prune_flag: bool,
) -> Result<bool, IngestError>
where
S: Transaction
+ OperationStore<Operation<E>, Hash, L>
+ LogStore<Operation<E>, VerifyingKey, L, SeqNum, Hash>
+ TopicStore<TP, VerifyingKey, L>,
T: Borrow<Operation<E>>,
L: LogId,
E: Extensions,
{
let operation: &Operation<E> = operation.borrow();
validate_operation(operation)?;
let permit = store
.begin()
.await
.map_err(|err| IngestError::StoreError(err.to_string()))?;
let already_exists = store
.has_operation_tx(&operation.hash)
.await
.map_err(|err| IngestError::StoreError(err.to_string()))?;
if already_exists {
store
.rollback(permit)
.await
.map_err(|err| IngestError::StoreError(err.to_string()))?;
return Ok(false);
}
let past_header = store
.get_latest_entry_tx(&operation.header.verifying_key, log_id)
.await
.map_err(|err| IngestError::StoreError(err.to_string()))?
.map(|operation| operation.header);
validate_prunable_backlink(past_header.as_ref(), &operation.header, prune_flag)?;
let id = operation.hash;
let verifying_key = operation.header.verifying_key;
store
.insert_operation(&id, operation, log_id)
.await
.map_err(|err| IngestError::StoreError(err.to_string()))?;
<S as TopicStore<TP, VerifyingKey, L>>::associate(store, topic, &verifying_key, log_id)
.await
.map_err(|err| IngestError::StoreError(err.to_string()))?;
store
.commit(permit)
.await
.map_err(|err| IngestError::StoreError(err.to_string()))?;
Ok(true)
}
#[derive(Clone, Debug, Error)]
pub enum IngestError {
#[error(transparent)]
InvalidOperation(#[from] OperationError),
#[error("critical storage failure: {0}")]
StoreError(String),
}
#[cfg(test)]
mod tests {
use p2panda_core::test_utils::TestLog;
use p2panda_core::{Hash, Header, Operation, SeqNum, SigningKey, Timestamp, VerifyingKey};
use p2panda_store::SqliteStore;
use p2panda_store::logs::LogStore;
use p2panda_store::topics::TopicStore;
use super::ingest_operation;
#[tokio::test]
async fn valid_log() {
let store = SqliteStore::temporary().await;
let log = TestLog::new();
for i in 0..128 {
let operation = log.operation(format!("{i}").as_bytes(), ());
let result = ingest_operation(&store, &operation, &1, &1, false).await;
assert!(result.is_ok());
}
}
#[tokio::test]
async fn deduplicate_operations() {
let store = SqliteStore::temporary().await;
let log = TestLog::new();
let operation = log.operation(b"same same", ());
let result = ingest_operation(&store, &operation, &1, &1, false)
.await
.unwrap();
assert!(result);
let result = ingest_operation(&store, &operation, &1, &1, false)
.await
.unwrap();
assert!(!result);
}
#[tokio::test]
async fn topic_association() {
let store = SqliteStore::temporary().await;
let log_0 = TestLog::new();
let log_1 = TestLog::new();
let log_2 = TestLog::new();
let dogs = [2; 32];
let cats = [3; 32];
ingest_operation(&store, &log_0.operation(b"Do", ()), &0, &dogs, false)
.await
.unwrap();
ingest_operation(&store, &log_0.operation(b"Re", ()), &0, &dogs, false)
.await
.unwrap();
ingest_operation(&store, &log_1.operation(b"Mi", ()), &1, &dogs, false)
.await
.unwrap();
ingest_operation(&store, &log_2.operation(b"Fa", ()), &2, &cats, false)
.await
.unwrap();
ingest_operation(&store, &log_2.operation(b"So", ()), &2, &cats, false)
.await
.unwrap();
ingest_operation(&store, &log_2.operation(b"La", ()), &2, &cats, false)
.await
.unwrap();
let authors =
<SqliteStore as TopicStore<[u8; 32], VerifyingKey, usize>>::resolve(&store, &dogs)
.await
.unwrap();
assert_eq!(*authors.get(&log_0.author()).unwrap(), [0]);
assert_eq!(*authors.get(&log_1.author()).unwrap(), [1]);
let operation = <SqliteStore as LogStore<
Operation<()>,
VerifyingKey,
usize,
SeqNum,
Hash,
>>::get_latest_entry(&store, &log_0.author(), &0)
.await
.unwrap()
.unwrap();
assert_eq!(operation.header.seq_num, 1);
let authors =
<SqliteStore as TopicStore<[u8; 32], VerifyingKey, usize>>::resolve(&store, &cats)
.await
.unwrap();
assert_eq!(*authors.get(&log_2.author()).unwrap(), [2]);
let operation = <SqliteStore as LogStore<
Operation<()>,
VerifyingKey,
usize,
SeqNum,
Hash,
>>::get_latest_entry(&store, &log_2.author(), &2)
.await
.unwrap()
.unwrap();
assert_eq!(operation.header.seq_num, 2);
}
#[tokio::test]
async fn missing_prefix() {
let store = SqliteStore::temporary().await;
let signing_key = SigningKey::generate();
let mut header = Header {
verifying_key: signing_key.verifying_key(),
version: 1,
signature: None,
payload_size: 0,
payload_hash: None,
timestamp: Timestamp::now(),
seq_num: 12, backlink: Some(Hash::digest(b"mock operation")),
extensions: (),
};
header.sign(&signing_key);
let operation = Operation {
hash: header.hash(),
header,
body: None,
};
let result = ingest_operation(&store, &operation, &1, &1, false).await;
assert!(result.is_err());
}
#[tokio::test]
async fn ignore_outdated_pruned_operations() {
let store = SqliteStore::temporary().await;
let signing_key = SigningKey::generate();
let mut header = Header {
verifying_key: signing_key.verifying_key(),
version: 1,
signature: None,
payload_size: 0,
payload_hash: None,
timestamp: Timestamp::now(),
seq_num: 1,
backlink: Some(Hash::digest(b"mock operation")),
extensions: (),
};
header.sign(&signing_key);
let operation = Operation {
hash: header.hash(),
header,
body: None,
};
let prune_flag = true; let result = ingest_operation(&store, &operation, &1, &1, prune_flag).await;
assert!(result.is_ok());
let mut header = Header {
verifying_key: signing_key.verifying_key(),
version: 1,
signature: None,
payload_size: 0,
payload_hash: None,
timestamp: Timestamp::now(),
seq_num: 0,
backlink: None,
extensions: (),
};
header.sign(&signing_key);
let operation = Operation {
hash: header.hash(),
header,
body: None,
};
let result = ingest_operation(&store, &operation, &1, &1, false).await;
assert!(result.is_err());
}
}