use crate::{NodeId, ObjectId, TransactionId, WriterId};
use chrono::{DateTime, Utc};
use std::{fmt, sync::Arc};
pub const MAX_OBJECT_BYTES: u64 = 32 * 1024 * 1024 * 1024;
pub const MAX_TRANSACTION_OBJECT_BYTES: u64 = MAX_OBJECT_BYTES;
pub(crate) const MAX_PROVENANCE_STRING_BYTES: usize = 1024 * 1024;
pub(crate) const MAX_TRANSACTION_BYTES: usize = 64 * 1024 * 1024;
pub type Result<T> = std::result::Result<T, Error>;
#[derive(Debug)]
pub enum Error {
Io(std::io::Error),
Busy(String),
Corrupt(String),
InvalidConfig(String),
InvalidInput(String),
InvalidTransaction(String),
NotFound(String),
OfflineUpgradeRequired(String),
}
impl Error {
pub(crate) fn corrupt(message: impl Into<String>) -> Self {
Self::Corrupt(message.into())
}
pub(crate) fn invalid_config(message: impl Into<String>) -> Self {
Self::InvalidConfig(message.into())
}
pub(crate) fn invalid_input(message: impl Into<String>) -> Self {
Self::InvalidInput(message.into())
}
pub(crate) fn invalid_transaction(message: impl Into<String>) -> Self {
Self::InvalidTransaction(message.into())
}
pub(crate) fn not_found(message: impl Into<String>) -> Self {
Self::NotFound(message.into())
}
pub(crate) fn offline_upgrade(message: impl Into<String>) -> Self {
Self::OfflineUpgradeRequired(message.into())
}
}
impl fmt::Display for Error {
fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
match self {
Self::Io(error) => write!(formatter, "I/O error: {error}"),
Self::Busy(message) => write!(formatter, "database is busy: {message}"),
Self::Corrupt(message) => write!(formatter, "database corruption: {message}"),
Self::InvalidConfig(message) => write!(formatter, "invalid configuration: {message}"),
Self::InvalidInput(message) => write!(formatter, "invalid input: {message}"),
Self::InvalidTransaction(message) => {
write!(formatter, "invalid transaction: {message}")
}
Self::NotFound(message) => write!(formatter, "not found: {message}"),
Self::OfflineUpgradeRequired(message) => {
write!(formatter, "offline upgrade required: {message}")
}
}
}
}
impl std::error::Error for Error {
fn source(&self) -> Option<&(dyn std::error::Error + 'static)> {
match self {
Self::Io(error) => Some(error),
_ => None,
}
}
}
impl From<std::io::Error> for Error {
fn from(error: std::io::Error) -> Self {
Self::Io(error)
}
}
pub trait Gossip: Send + Sync + 'static {
fn announce(&self, package: TransactionPackage) -> bool;
}
pub trait TransactionSource: Send + Sync {
fn request_transactions(&self, transactions: Vec<TransactionId>);
}
#[derive(Clone, Copy, Debug, Default)]
pub struct NoopGossip;
impl Gossip for NoopGossip {
fn announce(&self, _package: TransactionPackage) -> bool {
true
}
}
#[derive(Clone)]
pub struct Config {
pub signing_key: [u8; 32],
pub writers_by_priority: Vec<WriterId>,
pub gossip: Arc<dyn Gossip>,
}
impl fmt::Debug for Config {
fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
formatter
.debug_struct("Config")
.field("signing_key", &"[redacted]")
.field("writers_by_priority", &self.writers_by_priority)
.field("gossip", &"dyn Gossip")
.finish()
}
}
#[derive(Clone, Debug, Eq, PartialEq)]
pub struct Provenance {
pub author: String,
pub source: String,
pub source_created_at: DateTime<Utc>,
pub data: String,
}
impl Provenance {
pub(crate) fn validate(&self) -> Result<()> {
if self.author.trim().is_empty() || self.author.len() > MAX_PROVENANCE_STRING_BYTES {
return Err(Error::invalid_input(
"provenance author is empty or too large",
));
}
if self.source.trim().is_empty() || self.source.len() > MAX_PROVENANCE_STRING_BYTES {
return Err(Error::invalid_input(
"provenance source is empty or too large",
));
}
if self.data.len() > MAX_PROVENANCE_STRING_BYTES {
return Err(Error::invalid_input("provenance data exceeds 1 MiB"));
}
Ok(())
}
}
#[derive(Clone, Copy, Debug, Eq, PartialEq)]
pub enum Owner {
Unowned,
SelfNode,
Node(NodeId),
}
#[derive(Clone, Debug, Eq, PartialEq)]
pub struct NodeData {
pub short_name: String,
pub short_description: String,
pub long_description: String,
pub owner: Owner,
pub fixed_connections: Vec<NodeId>,
pub recent_connections: Vec<NodeId>,
pub objects: Vec<ObjectId>,
}
impl NodeData {
pub(crate) fn validate(&self) -> Result<()> {
ensure_unique(&self.fixed_connections, "fixed_connections")?;
ensure_unique(&self.recent_connections, "recent_connections")?;
ensure_unique(&self.objects, "objects")?;
if self.fixed_connections.iter().any(|id| !id.valid_domain())
|| self.recent_connections.iter().any(|id| !id.valid_domain())
|| self.objects.iter().any(|id| !id.valid_domain())
|| matches!(self.owner, Owner::Node(id) if !id.valid_domain())
{
return Err(Error::invalid_input(
"node contains an identifier in the wrong type domain",
));
}
Ok(())
}
}
fn ensure_unique<T: Copy + Ord>(values: &[T], name: &str) -> Result<()> {
let mut ordered = values.to_vec();
ordered.sort_unstable();
if ordered.windows(2).any(|pair| pair[0] == pair[1]) {
return Err(Error::invalid_input(format!(
"{name} must contain unique IDs"
)));
}
Ok(())
}
#[derive(Clone, Debug, Eq, PartialEq)]
pub struct Node {
pub id: NodeId,
pub data: NodeData,
pub last_author: String,
pub committed_at: DateTime<Utc>,
}
#[derive(Clone, Copy, Debug, Eq, Ord, PartialEq, PartialOrd)]
pub struct MergePair {
pub first: TransactionId,
pub second: TransactionId,
}
impl MergePair {
pub fn new(first: TransactionId, second: TransactionId) -> Result<Self> {
if first == second {
return Err(Error::invalid_input(
"a merge pair must name two different transactions",
));
}
let (first, second) = if first < second {
(first, second)
} else {
(second, first)
};
Ok(Self { first, second })
}
}
#[derive(Clone, Debug, Eq, PartialEq)]
pub struct HistoryEntry {
pub transaction_id: TransactionId,
pub writer: WriterId,
pub committed_at: DateTime<Utc>,
pub provenance: Provenance,
pub active: bool,
pub created: bool,
pub updated: bool,
pub data: Option<NodeData>,
pub merge_pairs: Vec<MergePair>,
}
#[derive(Clone, Debug, Eq, PartialEq)]
pub struct NodeHistory {
pub node_id: NodeId,
pub frontier: Vec<TransactionId>,
pub visible: Option<TransactionId>,
pub entries: Vec<HistoryEntry>,
}
#[derive(Eq, PartialEq)]
pub struct ObjectPayload {
pub id: ObjectId,
pub bytes: Vec<u8>,
}
impl fmt::Debug for ObjectPayload {
fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
formatter
.debug_struct("ObjectPayload")
.field("id", &self.id)
.field("length", &self.bytes.len())
.finish()
}
}
#[derive(Eq, PartialEq)]
pub struct TransactionPackage {
pub transaction: Vec<u8>,
pub objects: Vec<ObjectPayload>,
}
impl fmt::Debug for TransactionPackage {
fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
formatter
.debug_struct("TransactionPackage")
.field("transaction_bytes", &self.transaction.len())
.field("objects", &self.objects)
.finish()
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn node_text_has_no_database_policy_limits() {
let mut data = NodeData {
short_name: String::new(),
short_description: "x".repeat(201),
long_description: std::iter::repeat_n("word", 1_001)
.collect::<Vec<_>>()
.join(" "),
owner: Owner::SelfNode,
fixed_connections: Vec::new(),
recent_connections: Vec::new(),
objects: Vec::new(),
};
assert!(data.short_description.chars().count() > 200);
assert!(data.long_description.split_whitespace().count() > 1_000);
data.validate().unwrap();
data.short_name = "x".repeat(51);
assert!(data.short_name.chars().count() > 50);
data.validate().unwrap();
data.short_description = "x".repeat(MAX_PROVENANCE_STRING_BYTES + 1);
data.validate().unwrap();
}
}