use crate::api::helpers::get_skiplink_for_entry;
use crate::api::validation::{
ensure_document_not_deleted, get_checked_document_id_for_view_id, increment_seq_num,
next_log_id,
};
use crate::api::DomainError;
use crate::document::DocumentViewId;
use crate::entry::traits::{AsEncodedEntry, AsEntry};
use crate::entry::{LogId, SeqNum};
use crate::hash::Hash;
use crate::identity::PublicKey;
use crate::storage_provider::traits::{EntryStore, LogStore, OperationStore};
type Backlink = Hash;
type Skiplink = Hash;
pub async fn next_args<S: EntryStore + OperationStore + LogStore>(
store: &S,
public_key: &PublicKey,
document_view_id: Option<&DocumentViewId>,
) -> Result<(Option<Backlink>, Option<Skiplink>, SeqNum, LogId), DomainError> {
let document_view_id = match document_view_id {
Some(id) => id,
None => return calculate_next_args_new_log(store, public_key).await,
};
let document_id = get_checked_document_id_for_view_id(store, document_view_id).await?;
ensure_document_not_deleted(store, &document_id).await?;
let log_id = store.get_log_id(public_key, &document_id).await?;
match log_id {
None => calculate_next_args_new_log(store, public_key).await,
Some(log_id) => calculate_next_args_existing_log(store, &log_id, public_key).await,
}
}
async fn calculate_next_args_new_log<S: LogStore>(
store: &S,
public_key: &PublicKey,
) -> Result<(Option<Backlink>, Option<Skiplink>, SeqNum, LogId), DomainError> {
let next_log_id = next_log_id(store, public_key).await?;
Ok((None, None, SeqNum::default(), next_log_id))
}
async fn calculate_next_args_existing_log<S: EntryStore>(
store: &S,
log_id: &LogId,
public_key: &PublicKey,
) -> Result<(Option<Backlink>, Option<Skiplink>, SeqNum, LogId), DomainError> {
let latest_entry = store.get_latest_entry(public_key, &log_id).await?;
let seq_num = match latest_entry {
Some(ref latest_entry) => {
let mut latest_seq_num = latest_entry.seq_num().to_owned();
increment_seq_num(&mut latest_seq_num)
.map_err(|_| DomainError::MaxSeqNumReached(public_key.to_string(), log_id.as_u64()))
}
None => Err(DomainError::ExpectedLogIdNotFound(
log_id.as_u64().to_owned(),
)),
}?;
let skiplink = get_skiplink_for_entry(store, &seq_num, &log_id, public_key).await?;
let backlink = latest_entry.map(|entry| entry.hash());
Ok((backlink, skiplink, seq_num, log_id.clone()))
}
#[cfg(test)]
mod tests {
use rstest::rstest;
use crate::api::next_args;
use crate::api::next_args::{calculate_next_args_existing_log, calculate_next_args_new_log};
use crate::document::DocumentViewId;
use crate::entry::encode::sign_and_encode_entry;
use crate::entry::traits::{AsEncodedEntry, AsEntry};
use crate::entry::{LogId, SeqNum};
use crate::identity::KeyPair;
use crate::operation::OperationId;
use crate::storage_provider::traits::EntryStore;
use crate::test_utils::constants::PRIVATE_KEY;
use crate::test_utils::fixtures::{
key_pair, populate_store_config, random_document_view_id, random_hash,
};
use crate::test_utils::memory_store::helpers::{
populate_store, remove_entries, remove_operations, PopulateStoreConfig,
};
use crate::test_utils::memory_store::{MemoryStore, StorageEntry};
type LogIdAndSeqNum = (u64, u64);
#[rstest]
#[tokio::test]
async fn calculates_next_args(
#[from(populate_store_config)]
#[with(8, 1, 1)]
config: PopulateStoreConfig,
) {
let store = MemoryStore::default();
let (key_pairs, _) = populate_store(&store, &config).await;
let public_key = key_pairs[0].public_key();
let (backlink, skiplink, seq_num, log_id) =
calculate_next_args_new_log(&store, &public_key)
.await
.unwrap();
assert_eq!(backlink, None);
assert_eq!(skiplink, None);
assert_eq!(seq_num, SeqNum::default());
assert_eq!(log_id, LogId::new(1));
let (backlink, skiplink, seq_num, log_id) =
calculate_next_args_existing_log(&store, &LogId::new(0), &public_key)
.await
.unwrap();
let expected_backlink = store
.get_entry_at_seq_num(&public_key, &LogId::new(0), &SeqNum::new(8).unwrap())
.await
.unwrap()
.unwrap();
assert_eq!(backlink, Some(expected_backlink.hash()));
assert_eq!(skiplink, None);
assert_eq!(seq_num, SeqNum::new(9).unwrap());
assert_eq!(log_id, LogId::new(0));
let result = calculate_next_args_existing_log(&store, &LogId::new(1), &public_key).await;
assert!(result.is_err())
}
#[rstest]
#[case::ok_single_writer(
&[],
&[(0, 8)],
KeyPair::from_private_key_str(PRIVATE_KEY).unwrap()
)]
#[case::ok_many_previous(
&[],
&[(0, 8), (0, 7), (0, 6)],
KeyPair::from_private_key_str(PRIVATE_KEY).unwrap()
)]
#[case::ok_not_the_most_recent_document_view_id(
&[],
&[(0, 1)],
KeyPair::from_private_key_str(PRIVATE_KEY).unwrap()
)]
#[case::ok_multi_writer(
&[],
&[(0, 8)],
KeyPair::new()
)]
#[should_panic(
expected = "Previous operation 00209038901221ce1002f023461f1530adf632081d9fcd2da1082c7c91fdcb534d03 not found in store"
)]
#[case::previous_operation_missing(
&[(0, 8)],
&[(0, 8)],
KeyPair::from_private_key_str(PRIVATE_KEY).unwrap()
)]
#[should_panic(
expected = "Previous operation 00201971f1257645a2f6d3465f8713991d269709f81a5c6c458168b9461d68af5ecf not found in store"
)]
#[case::one_of_some_previous_missing(
&[(0, 7)],
&[(0, 7), (0, 8)],
KeyPair::from_private_key_str(PRIVATE_KEY).unwrap()
)]
#[should_panic(
expected = "Previous operation 00209038901221ce1002f023461f1530adf632081d9fcd2da1082c7c91fdcb534d03 not found in store"
)]
#[case::one_of_some_previous_missing(
&[(0, 8)],
&[(0, 7), (0, 8)],
KeyPair::from_private_key_str(PRIVATE_KEY).unwrap()
)]
#[should_panic(
expected = "Previous operation 00209038901221ce1002f023461f1530adf632081d9fcd2da1082c7c91fdcb534d03 not found in store"
)]
#[case::missing_previous_operation_multi_writer(
&[(0, 8)],
&[(0, 8)],
KeyPair::new()
)]
#[should_panic(
expected = "Operations in passed document view id originate from different documents"
)]
#[case::previous_invalid_multiple_document_id(
&[],
&[(0, 8), (1, 8)],
KeyPair::from_private_key_str(PRIVATE_KEY).unwrap()
)]
#[tokio::test]
async fn next_args_with_missing_operations(
#[case] operations_to_remove: &[LogIdAndSeqNum],
#[case] document_view_id: &[LogIdAndSeqNum],
#[case] key_pair: KeyPair,
#[from(populate_store_config)]
#[with(8, 2, 1)]
config: PopulateStoreConfig,
) {
let store = MemoryStore::default();
let (key_pairs, _) = populate_store(&store, &config).await;
let public_key_with_removed_operations = key_pairs[0].public_key();
let public_key_making_request = key_pair.public_key();
let document_view_id: Vec<OperationId> = document_view_id
.iter()
.filter_map(|(log_id, seq_num)| {
store
.entries
.lock()
.unwrap()
.values()
.find(|entry| {
entry.seq_num().as_u64() == *seq_num
&& entry.log_id().as_u64() == *log_id
&& *entry.public_key() == public_key_with_removed_operations
})
.map(|entry| entry.hash().into())
})
.collect();
let document_view_id = DocumentViewId::new(&document_view_id);
remove_operations(
&store,
&public_key_with_removed_operations,
operations_to_remove,
);
let result = next_args(&store, &public_key_making_request, Some(&document_view_id)).await;
result.map_err(|err| err.to_string()).unwrap();
}
type SeqNumU64 = u64;
type LogIdU64 = u64;
type Backlink = Option<u64>;
type Skiplink = Option<u64>;
#[rstest]
#[case(0, 0, None, (1, 0, None, None))]
#[case(1, 1, Some((1, 0)), (2, 0, Some(1), None))]
#[case(2, 1, Some((2, 0)), (3, 0, Some(2), None))]
#[case(3, 1, Some((3, 0)), (4, 0, Some(3), Some(1)))]
#[case(4, 1, Some((4, 0)), (5, 0, Some(4), None))]
#[case(5, 1, Some((5, 0)), (6, 0, Some(5), None))]
#[case(6, 1, Some((6, 0)), (7, 0, Some(6), None))]
#[case(7, 1, Some((7, 0)), (8, 0, Some(7), Some(4)))]
#[case(2, 1, Some((1, 0)), (3, 0, Some(2), None))]
#[case(3, 1, Some((1, 0)), (4, 0, Some(3), Some(1)))]
#[case(4, 1, Some((1, 0)), (5, 0, Some(4), None))]
#[case(5, 1, Some((1, 0)), (6, 0, Some(5), None))]
#[case(6, 1, Some((1, 0)), (7, 0, Some(6), None))]
#[case(7, 1, Some((1, 0)), (8, 0, Some(7), Some(4)))]
#[case(1, 2, None, (1, 2, None, None))]
#[case(1, 10, None, (1, 10, None, None))]
#[case(1, 100, None, (1, 100, None, None))]
#[case(1, 100, Some((1, 9)), (2, 9, Some(1), None))]
#[case(1, 100, Some((1, 99)), (2, 99, Some(1), None))]
#[tokio::test]
async fn next_args_with_expected_results(
#[case] no_of_entries: usize,
#[case] no_of_logs: usize,
#[case] document_view_id: Option<(SeqNumU64, LogIdU64)>,
#[case] expected_next_args: (SeqNumU64, LogIdU64, Backlink, Skiplink),
) {
let store = MemoryStore::default();
let config = PopulateStoreConfig {
no_of_entries,
no_of_logs,
no_of_public_keys: 1,
..PopulateStoreConfig::default()
};
let (key_pairs, _) = populate_store(&store, &config).await;
let public_key = key_pairs[0].public_key();
let document_view_id: Option<DocumentViewId> = document_view_id.map(|(seq_num, log_id)| {
store
.entries
.lock()
.unwrap()
.values()
.find(|entry| {
entry.seq_num().as_u64() == seq_num && entry.log_id().as_u64() == log_id
})
.map(|entry| DocumentViewId::new(&[entry.hash().into()]))
.unwrap()
});
let expected_seq_num = SeqNum::new(expected_next_args.0).unwrap();
let expected_log_id = LogId::new(expected_next_args.1);
let expected_backlink = match expected_next_args.2 {
Some(backlink) => store
.get_entry_at_seq_num(
&public_key,
&expected_log_id,
&SeqNum::new(backlink).unwrap(),
)
.await
.unwrap()
.map(|entry| entry.hash()),
None => None,
};
let expected_skiplink = match expected_next_args.3 {
Some(skiplink) => store
.get_entry_at_seq_num(
&public_key,
&expected_log_id,
&SeqNum::new(skiplink).unwrap(),
)
.await
.unwrap()
.map(|entry| entry.hash()),
None => None,
};
let (backlink, skiplink, seq_num, log_id) =
next_args(&store, &public_key, document_view_id.as_ref())
.await
.unwrap();
assert_eq!(backlink, expected_backlink);
assert_eq!(skiplink, expected_skiplink);
assert_eq!(seq_num, expected_seq_num);
assert_eq!(log_id, expected_log_id);
}
#[rstest]
#[tokio::test]
async fn gets_next_args_other_cases(
#[from(populate_store_config)]
#[with(7, 1, 1)]
config: PopulateStoreConfig,
) {
let store = MemoryStore::default();
let (key_pairs, documents) = populate_store(&store, &config).await;
let public_key = key_pairs[0].public_key();
let (backlink, skiplink, seq_num, log_id) =
next_args(&store, &public_key, None).await.unwrap();
assert_eq!(backlink, None);
assert_eq!(skiplink, None);
assert_eq!(seq_num, SeqNum::new(1).unwrap());
assert_eq!(log_id, LogId::new(1));
let result = next_args(&store, &public_key, Some(&random_document_view_id())).await;
assert!(result.is_err());
assert!(
result
.unwrap_err()
.to_string()
.contains("not found in store") );
remove_entries(&store, &public_key, &[(0, 4)]);
let document_id = documents.get(0).unwrap();
let document_view_id = DocumentViewId::new(&[document_id.as_str().parse().unwrap()]);
let result = next_args(&store, &public_key, Some(&document_view_id)).await;
assert_eq!(
result.unwrap_err().to_string(),
"Expected skiplink entry not found in store: public key 2f8e50c2ede6d936ecc3144187ff1c273808185cfbc5ff3d3748d1ff7353fc96, log id 0, seq num 4"
);
}
#[rstest]
#[should_panic(expected = "Document is deleted")]
#[case(KeyPair::from_private_key_str(PRIVATE_KEY).unwrap())]
#[should_panic(expected = "Document is deleted")]
#[case(KeyPair::new())]
#[tokio::test]
async fn next_args_deleted_documents(
#[case] key_pair: KeyPair,
#[from(populate_store_config)]
#[with(3, 1, 1, true)]
config: PopulateStoreConfig,
) {
let store = MemoryStore::default();
let (_, documents) = populate_store(&store, &config).await;
let document_id = documents.first().unwrap();
let document_view_id: DocumentViewId = document_id.as_str().parse().unwrap();
let public_key = key_pair.public_key();
let result = next_args(&store, &public_key, Some(&document_view_id)).await;
result.map_err(|err| err.to_string()).unwrap();
}
#[rstest]
#[should_panic(
expected = "Max sequence number reached for public key 2f8e50c2ede6d936ecc3144187ff1c273808185cfbc5ff3d3748d1ff7353fc96 log 0"
)]
#[tokio::test]
async fn next_args_max_seq_num_reached(
key_pair: KeyPair,
#[from(populate_store_config)]
#[with(2, 1, 1, false)]
config: PopulateStoreConfig,
) {
let store = MemoryStore::default();
let _ = populate_store(&store, &config).await;
let public_key = key_pair.public_key();
let entry_two = store
.get_entry_at_seq_num(&public_key, &LogId::default(), &SeqNum::new(2).unwrap())
.await
.unwrap()
.unwrap();
let encoded_entry = sign_and_encode_entry(
&LogId::default(),
&SeqNum::new(u64::MAX).unwrap(),
Some(&random_hash()),
Some(&random_hash()),
entry_two.payload.as_ref().unwrap(),
&key_pair,
)
.unwrap();
let entry = StorageEntry::new(&encoded_entry, entry_two.payload.as_ref());
store
.entries
.lock()
.unwrap()
.insert(entry.hash(), entry.clone());
let result = next_args(&store, &public_key, Some(&entry_two.hash().into())).await;
result.map_err(|err| err.to_string()).unwrap();
}
}