use crate::{
db::{
QueryError,
codec::{finalize_hash_sha256, new_hash_sha256_prefixed, write_hash_str_u32},
integrity::DatabaseIncarnationId,
schema::AcceptedSchemaRuntimeRootIdentity,
},
error::InternalError,
};
use candid::CandidType;
use serde::Deserialize;
use std::{error::Error as StdError, fmt};
const READ_SET_STORE_IDENTITY_DOMAIN: &[u8] = b"icydb.read-set.store.v1";
const READ_SET_PROOF_FIXED_BYTES: usize = 16 + 8 + 1 + 32 + 4;
const READ_SET_STORE_ENTRY_BYTES: usize = 32 + 8 + 8;
pub const MAX_READ_SET_PROOF_STORES: usize = 64;
const MAX_READ_SET_PROOF_STORES_U32: u32 = 64;
pub const MAX_READ_SET_PROOF_BYTES: usize = 8 * 1024;
const MAX_READ_SET_PROOF_BYTES_U32: u32 = 8 * 1024;
#[derive(CandidType, Clone, Copy, Debug, Deserialize, Eq, Hash, Ord, PartialEq, PartialOrd)]
pub struct ReadSetStoreIdentity([u8; 32]);
impl ReadSetStoreIdentity {
pub(in crate::db) fn for_store_path(store_path: &str) -> Self {
let mut hasher = new_hash_sha256_prefixed(READ_SET_STORE_IDENTITY_DOMAIN);
write_hash_str_u32(&mut hasher, store_path);
Self(finalize_hash_sha256(hasher))
}
pub(in crate::db) const fn from_bytes(bytes: [u8; 32]) -> Self {
Self(bytes)
}
#[must_use]
pub const fn to_bytes(self) -> [u8; 32] {
self.0
}
}
#[derive(CandidType, Clone, Copy, Debug, Deserialize, Eq, PartialEq)]
pub struct ReadSetStoreRevision {
store: ReadSetStoreIdentity,
data_revision: u64,
access_state_revision: u64,
}
impl ReadSetStoreRevision {
pub(in crate::db) const fn new(
store: ReadSetStoreIdentity,
data_revision: u64,
access_state_revision: u64,
) -> Self {
Self {
store,
data_revision,
access_state_revision,
}
}
#[must_use]
pub const fn store(&self) -> ReadSetStoreIdentity {
self.store
}
#[must_use]
pub const fn data_revision(&self) -> u64 {
self.data_revision
}
#[must_use]
pub const fn access_state_revision(&self) -> u64 {
self.access_state_revision
}
}
#[derive(CandidType, Clone, Debug, Deserialize, Eq, PartialEq)]
pub struct ReadSetRevisionProof {
database_incarnation: [u8; 16],
accepted_root_revision: u64,
accepted_root_fingerprint_method: u8,
accepted_root_fingerprint: [u8; 32],
stores: Vec<ReadSetStoreRevision>,
}
impl ReadSetRevisionProof {
pub(in crate::db) fn new(
root: AcceptedSchemaRuntimeRootIdentity,
stores: Vec<ReadSetStoreRevision>,
) -> Result<Self, ReadSetRevisionError> {
let (accepted_root_fingerprint_method, accepted_root_fingerprint) = root.fingerprint();
let proof = Self {
database_incarnation: root.database_incarnation().to_bytes(),
accepted_root_revision: root.accepted_root_revision().get(),
accepted_root_fingerprint_method,
accepted_root_fingerprint,
stores,
};
proof.validate()?;
Ok(proof)
}
pub(in crate::db) fn from_parts(
database_incarnation: [u8; 16],
accepted_root_revision: u64,
accepted_root_fingerprint_method: u8,
accepted_root_fingerprint: [u8; 32],
stores: Vec<ReadSetStoreRevision>,
) -> Result<Self, ReadSetRevisionError> {
let proof = Self {
database_incarnation,
accepted_root_revision,
accepted_root_fingerprint_method,
accepted_root_fingerprint,
stores,
};
proof.validate()?;
Ok(proof)
}
#[must_use]
pub const fn database_incarnation(&self) -> [u8; 16] {
self.database_incarnation
}
#[must_use]
pub const fn accepted_root_revision(&self) -> u64 {
self.accepted_root_revision
}
#[must_use]
pub const fn accepted_root_fingerprint_method(&self) -> u8 {
self.accepted_root_fingerprint_method
}
#[must_use]
pub const fn accepted_root_fingerprint(&self) -> [u8; 32] {
self.accepted_root_fingerprint
}
#[must_use]
pub const fn stores(&self) -> &[ReadSetStoreRevision] {
self.stores.as_slice()
}
#[must_use]
pub const fn encoded_len(&self) -> usize {
READ_SET_PROOF_FIXED_BYTES
.saturating_add(self.stores.len().saturating_mul(READ_SET_STORE_ENTRY_BYTES))
}
pub fn validate(&self) -> Result<(), ReadSetRevisionError> {
if self.stores.is_empty() {
return Err(ReadSetRevisionError::Empty);
}
if self.stores.len() > MAX_READ_SET_PROOF_STORES {
return Err(ReadSetRevisionError::TooManyStores {
limit: MAX_READ_SET_PROOF_STORES_U32,
actual: u32::try_from(self.stores.len()).unwrap_or(u32::MAX),
});
}
let encoded_len = self.encoded_len();
if encoded_len > MAX_READ_SET_PROOF_BYTES {
return Err(ReadSetRevisionError::EncodedBytesExceeded {
limit: MAX_READ_SET_PROOF_BYTES_U32,
actual: u32::try_from(encoded_len).unwrap_or(u32::MAX),
});
}
if self.database_incarnation == [0; 16]
|| self.accepted_root_revision == 0
|| self.accepted_root_fingerprint_method == 0
|| self.accepted_root_fingerprint == [0; 32]
|| self.stores.iter().any(|store| {
store.store.to_bytes() == [0; 32]
|| store.data_revision == 0
|| store.access_state_revision == 0
})
|| self
.stores
.windows(2)
.any(|pair| pair[0].store >= pair[1].store)
{
return Err(ReadSetRevisionError::NonCanonical);
}
Ok(())
}
pub(in crate::db) fn contains_store(&self, store: ReadSetStoreIdentity) -> bool {
self.stores
.binary_search_by_key(&store, ReadSetStoreRevision::store)
.is_ok()
}
pub(in crate::db) fn signature_bytes(&self) -> Vec<u8> {
let mut bytes = Vec::with_capacity(self.encoded_len());
bytes.extend_from_slice(&self.database_incarnation);
bytes.extend_from_slice(&self.accepted_root_revision.to_be_bytes());
bytes.push(self.accepted_root_fingerprint_method);
bytes.extend_from_slice(&self.accepted_root_fingerprint);
let store_count = u32::try_from(self.stores.len()).unwrap_or(u32::MAX);
bytes.extend_from_slice(&store_count.to_be_bytes());
for store in &self.stores {
bytes.extend_from_slice(&store.store.to_bytes());
bytes.extend_from_slice(&store.data_revision.to_be_bytes());
bytes.extend_from_slice(&store.access_state_revision.to_be_bytes());
}
bytes
}
pub(in crate::db) fn root_matches(
&self,
incarnation: DatabaseIncarnationId,
root: AcceptedSchemaRuntimeRootIdentity,
) -> bool {
let (method, fingerprint) = root.fingerprint();
self.database_incarnation == incarnation.to_bytes()
&& self.accepted_root_revision == root.accepted_root_revision().get()
&& self.accepted_root_fingerprint_method == method
&& self.accepted_root_fingerprint == fingerprint
}
}
#[derive(CandidType, Clone, Debug, Deserialize, Eq, PartialEq)]
pub enum ReadSetRevisionError {
Empty,
TooManyStores { limit: u32, actual: u32 },
EncodedBytesExceeded { limit: u32, actual: u32 },
NonCanonical,
UnknownEntity,
StoreMissingFromProof { store: ReadSetStoreIdentity },
ResumeProofRequired,
DatabaseIncarnationChanged,
AcceptedRootChanged,
StoreDataChanged { store: ReadSetStoreIdentity },
StoreAccessChanged { store: ReadSetStoreIdentity },
DurableStoreRequired { store: ReadSetStoreIdentity },
}
impl fmt::Display for ReadSetRevisionError {
fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
formatter.write_str("exhaustive read source proof is invalid")
}
}
impl StdError for ReadSetRevisionError {}
impl ReadSetRevisionError {
pub(in crate::db) const fn is_source_change(&self) -> bool {
matches!(
self,
Self::DatabaseIncarnationChanged
| Self::AcceptedRootChanged
| Self::StoreDataChanged { .. }
| Self::StoreAccessChanged { .. }
)
}
}
#[derive(Debug)]
pub enum ExhaustiveReadError {
Query(QueryError),
Revision(ReadSetRevisionError),
}
impl fmt::Display for ExhaustiveReadError {
fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
match self {
Self::Query(error) => error.fmt(formatter),
Self::Revision(error) => error.fmt(formatter),
}
}
}
impl StdError for ExhaustiveReadError {}
impl From<QueryError> for ExhaustiveReadError {
fn from(error: QueryError) -> Self {
Self::Query(error)
}
}
impl From<InternalError> for ExhaustiveReadError {
fn from(error: InternalError) -> Self {
Self::Query(QueryError::execute(error))
}
}
impl From<ReadSetRevisionError> for ExhaustiveReadError {
fn from(error: ReadSetRevisionError) -> Self {
Self::Revision(error)
}
}
#[cfg(test)]
mod tests {
use super::*;
fn store(byte: u8, data_revision: u64) -> ReadSetStoreRevision {
ReadSetStoreRevision::new(
ReadSetStoreIdentity::from_bytes([byte; 32]),
data_revision,
1,
)
}
fn proof(
stores: Vec<ReadSetStoreRevision>,
) -> Result<ReadSetRevisionProof, ReadSetRevisionError> {
ReadSetRevisionProof::from_parts([1; 16], 1, 1, [2; 32], stores)
}
#[test]
fn read_set_proof_requires_bounded_canonical_distinct_store_order() {
assert_eq!(proof(Vec::new()), Err(ReadSetRevisionError::Empty));
assert_eq!(
proof(vec![store(2, 1), store(1, 1)]),
Err(ReadSetRevisionError::NonCanonical),
);
assert_eq!(
proof(vec![store(1, 1), store(1, 2)]),
Err(ReadSetRevisionError::NonCanonical),
);
let too_many = (1..=MAX_READ_SET_PROOF_STORES + 1)
.map(|index| {
let mut identity = [0; 32];
identity[24..].copy_from_slice(
&u64::try_from(index)
.expect("bounded test store count should fit u64")
.to_be_bytes(),
);
ReadSetStoreRevision::new(ReadSetStoreIdentity::from_bytes(identity), 1, 1)
})
.collect();
assert_eq!(
proof(too_many),
Err(ReadSetRevisionError::TooManyStores {
limit: MAX_READ_SET_PROOF_STORES_U32,
actual: u32::try_from(MAX_READ_SET_PROOF_STORES + 1)
.expect("bounded test store count should fit u32"),
}),
);
}
#[test]
fn read_set_proof_rejects_zero_authority_or_revision_components() {
let valid = proof(vec![store(1, 1)]).expect("nonzero canonical proof should admit");
assert_eq!(
valid.encoded_len(),
READ_SET_PROOF_FIXED_BYTES + READ_SET_STORE_ENTRY_BYTES
);
assert_eq!(
ReadSetRevisionProof::from_parts([1; 16], 0, 1, [2; 32], vec![store(1, 1)]),
Err(ReadSetRevisionError::NonCanonical),
);
assert_eq!(
proof(vec![store(1, 0)]),
Err(ReadSetRevisionError::NonCanonical),
);
}
}