pub mod attachments;
pub mod batch;
mod client;
pub use client::Preflight;
mod codec;
pub(crate) mod dependencies;
mod domain;
#[cfg(feature = "test-support")]
pub(crate) mod fuzz;
pub(crate) mod graphs;
mod labels;
mod moves;
mod notes;
mod recurrence;
mod server;
use super::{LocalSharedStatePackageKey, seed_claim::peer};
use anyhow::{Result, ensure};
use serde::{Deserialize, Serialize};
use sha2::Sha256;
pub const RECORD_LIMIT: usize = 135640;
pub const CONTROL_LIMIT: usize = 16384;
pub const APPEND_LIMIT: usize = super::base64_bytes::encoded_len(RECORD_LIMIT) + CONTROL_LIMIT;
pub const PAGE_BYTES: usize = 2 * 1048576;
pub const PAGE_COUNT: usize = 256;
pub const RESPONSE_LIMIT: usize = PAGE_BYTES + CONTROL_LIMIT;
pub const BATCH_COUNT: usize = 128;
pub const BATCH_BYTES: usize = 1048576;
pub const BATCH_APPEND_LIMIT: usize = super::base64_bytes::encoded_len(BATCH_BYTES) + CONTROL_LIMIT;
pub const BATCH_CONTROL_LIMIT: usize = 256 * 1024;
#[derive(Debug)]
pub struct PrefixIdentityCollision;
impl std::fmt::Display for PrefixIdentityCollision {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
f.write_str("error encrypted-tail-prefix-identity-collision")
}
}
impl std::error::Error for PrefixIdentityCollision {}
#[derive(Clone, Serialize, Deserialize, PartialEq, Eq)]
#[serde(deny_unknown_fields)]
pub struct Context {
pub vault: [u8; 32],
pub genesis: [u8; 32],
pub device: [u8; 32],
pub head: [u8; 32],
pub stream: [u8; 32],
pub descriptor: [u8; 32],
}
impl Context {
pub fn authentication<'a>(
&self,
bearer: &'a super::seed_claim::Secret,
) -> peer::Authentication<'a> {
peer::Authentication {
vault: self.vault,
genesis: self.genesis,
device: self.device,
head: self.head,
bearer,
}
}
}
pub struct Authority {
pub context: Context,
pub membership: super::seed_claim::membership::Membership,
pub keys: super::seed_claim::membership::VerifiedKeys,
pub prefix: i64,
pub association: String,
pub sync_generation: i64,
}
pub struct AbsentOperation {
context: Context,
record: Vec<u8>,
}
pub struct Push {
pub record: Vec<u8>,
pub upload: Option<attachments::Upload>,
}
pub struct RoundState {
pub cursor: i64,
pub initial_watermark: Option<i64>,
pub idle: bool,
pub upload_pending: bool,
pub downloads: Option<Downloads>,
}
#[derive(Clone, Copy)]
pub struct Downloads {
pub pending: bool,
pub unavailable: bool,
}
impl Authority {
pub fn generation(&self) -> [u8; 32] {
self.membership.current_generation().id
}
pub fn rotation_pending(&self) -> bool {
self.membership.rotation_pending()
}
pub fn key(&self, generation: [u8; 32]) -> Result<&LocalSharedStatePackageKey> {
self.keys.key(generation)
}
pub fn record_is_closed(&self, record: &[u8]) -> Result<bool> {
let e = codec::parse(record)?;
self.key(e.generation)?;
valid(e.vault == self.context.vault && e.stream == self.context.stream)?;
Ok(e.generation != self.generation())
}
pub fn confirm_absent(&self, record: &[u8], response: &Reply) -> Result<AbsentOperation> {
self.validate()?;
valid(matches!(response, Reply::Absent))?;
codec::open(self, record)?;
Ok(AbsentOperation {
context: self.context.clone(),
record: record.to_vec(),
})
}
fn validate(&self) -> Result<()> {
self.keys.validate(&self.membership)?;
let b = self.membership.publication().binding();
valid(
self.context.vault == b.vault_id
&& self.context.genesis == self.membership.genesis().commitment()
&& self.context.head == self.membership.head()
&& self.context.stream == b.stream_id
&& self.context.descriptor == b.descriptor_commitment
&& self.prefix == i64::try_from(b.prefix_count)?,
)
}
}
#[derive(Clone, Serialize, Deserialize, PartialEq, Eq)]
#[serde(deny_unknown_fields)]
pub struct Mapping {
pub operation_id: String,
pub sequence: i64,
pub commitment: [u8; 32],
}
#[derive(Clone, Serialize, Deserialize)]
#[serde(deny_unknown_fields)]
pub struct Accepted {
pub mapping: Mapping,
#[serde(with = "crate::sync::base64_bytes")]
pub record: Vec<u8>,
}
#[derive(Clone, Serialize, Deserialize)]
#[serde(deny_unknown_fields)]
pub struct BatchFeatures {
pub count: usize,
pub bytes: usize,
}
#[derive(Clone, Serialize, Deserialize)]
#[serde(deny_unknown_fields)]
pub enum Operation {
Features,
Append {
ticket: Option<attachments::Ticket>,
#[serde(with = "crate::sync::base64_bytes")]
record: Vec<u8>,
},
Lookup {
operation_id: String,
expected: Option<Mapping>,
},
Pull {
after: i64,
limit: usize,
watermark: Option<i64>,
},
}
#[derive(Clone, Serialize, Deserialize)]
#[serde(deny_unknown_fields)]
pub struct Page {
pub after: i64,
pub watermark: i64,
pub cursor: i64,
pub has_more: bool,
pub records: Vec<Accepted>,
}
#[derive(Clone, Serialize, Deserialize)]
#[serde(transparent)]
pub struct BatchRecord(
#[serde(
serialize_with = "crate::sync::base64_bytes::serialize",
deserialize_with = "crate::sync::base64_bytes::bounded::<_, RECORD_LIMIT>"
)]
pub Vec<u8>,
);
#[derive(Clone, Serialize, Deserialize)]
#[serde(deny_unknown_fields)]
pub enum BatchOperation {
Append {
#[serde(deserialize_with = "batch::bounded_items")]
records: Vec<BatchRecord>,
},
Resolve {
#[serde(deserialize_with = "batch::bounded_items")]
operation_ids: Vec<String>,
},
}
#[derive(Clone, Serialize, Deserialize)]
#[serde(deny_unknown_fields)]
pub enum Resolution {
Found(batch::CompactMapping),
Absent { operation_id: String },
Bootstrap { operation_id: String },
}
#[derive(Clone, Serialize, Deserialize)]
#[serde(deny_unknown_fields)]
pub enum BatchReply {
Appended(#[serde(deserialize_with = "batch::bounded_items")] Vec<batch::CompactMapping>),
Resolved(#[serde(deserialize_with = "batch::bounded_items")] Vec<Resolution>),
}
#[derive(Clone, Serialize, Deserialize)]
#[serde(deny_unknown_fields)]
pub enum Reply {
Features(BatchFeatures),
Appended(Mapping),
Found(Accepted),
Absent,
Bootstrap,
Page(Page),
}
pub(crate) use crate::sync::codec::hash;
pub(crate) fn valid(ok: bool) -> Result<()> {
ensure!(ok, "error encrypted-tail-invalid");
Ok(())
}
#[cfg(test)]
mod tests;
#[cfg(any(test, feature = "test-support"))]
pub(crate) fn fixture_authority() -> Authority {
let (membership, keys) = crate::sync::seed_claim::membership::test_support::content_authority();
let b = membership.publication().binding();
Authority {
context: Context {
vault: b.vault_id,
genesis: membership.genesis().commitment(),
device: [3; 32],
head: membership.head(),
stream: b.stream_id,
descriptor: b.descriptor_commitment,
},
prefix: b.prefix_count as i64,
membership,
keys,
association: "test".into(),
sync_generation: 1,
}
}
#[cfg(any(test, feature = "test-support"))]
pub fn attachment_integrity_scan_metrics() -> (u64, u64) {
attachments::client::integrity_scan_metrics()
}
#[cfg(any(test, feature = "test-support"))]
pub(crate) fn open_record(authority: &Authority, record: &[u8]) -> Result<super::wire::ChangeWire> {
codec::open(authority, record)
}
#[cfg(any(test, feature = "test-support"))]
pub(crate) fn reseal(
authority: &Authority,
accepted: &Accepted,
edit: impl FnOnce(&mut super::wire::ChangeWire),
) -> Result<Accepted> {
let mut change = codec::open(authority, &accepted.record)?;
edit(&mut change);
let record = codec::seal(authority, &change)?;
Ok(Accepted {
mapping: Mapping {
operation_id: change.change_id,
sequence: accepted.mapping.sequence,
commitment: hash(&record),
},
record,
})
}