1use crate::{NodeId, ObjectId, TransactionId, WriterId};
2use chrono::{DateTime, Utc};
3use std::{fmt, sync::Arc};
4
5pub const MAX_OBJECT_BYTES: u64 = 32 * 1024 * 1024 * 1024;
6pub const MAX_TRANSACTION_OBJECT_BYTES: u64 = MAX_OBJECT_BYTES;
7pub(crate) const MAX_STRING_BYTES: usize = 1024 * 1024;
8pub(crate) const MAX_TRANSACTION_BYTES: usize = 64 * 1024 * 1024;
9
10pub type Result<T> = std::result::Result<T, Error>;
11
12#[derive(Debug)]
13pub enum Error {
14 Io(std::io::Error),
15 Busy(String),
16 Corrupt(String),
17 InvalidConfig(String),
18 InvalidInput(String),
19 InvalidTransaction(String),
20 NotFound(String),
21 OfflineUpgradeRequired(String),
22}
23
24impl Error {
25 pub(crate) fn corrupt(message: impl Into<String>) -> Self {
26 Self::Corrupt(message.into())
27 }
28
29 pub(crate) fn invalid_config(message: impl Into<String>) -> Self {
30 Self::InvalidConfig(message.into())
31 }
32
33 pub(crate) fn invalid_input(message: impl Into<String>) -> Self {
34 Self::InvalidInput(message.into())
35 }
36
37 pub(crate) fn invalid_transaction(message: impl Into<String>) -> Self {
38 Self::InvalidTransaction(message.into())
39 }
40
41 pub(crate) fn not_found(message: impl Into<String>) -> Self {
42 Self::NotFound(message.into())
43 }
44
45 pub(crate) fn offline_upgrade(message: impl Into<String>) -> Self {
46 Self::OfflineUpgradeRequired(message.into())
47 }
48}
49
50impl fmt::Display for Error {
51 fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
52 match self {
53 Self::Io(error) => write!(formatter, "I/O error: {error}"),
54 Self::Busy(message) => write!(formatter, "database is busy: {message}"),
55 Self::Corrupt(message) => write!(formatter, "database corruption: {message}"),
56 Self::InvalidConfig(message) => write!(formatter, "invalid configuration: {message}"),
57 Self::InvalidInput(message) => write!(formatter, "invalid input: {message}"),
58 Self::InvalidTransaction(message) => {
59 write!(formatter, "invalid transaction: {message}")
60 }
61 Self::NotFound(message) => write!(formatter, "not found: {message}"),
62 Self::OfflineUpgradeRequired(message) => {
63 write!(formatter, "offline upgrade required: {message}")
64 }
65 }
66 }
67}
68
69impl std::error::Error for Error {
70 fn source(&self) -> Option<&(dyn std::error::Error + 'static)> {
71 match self {
72 Self::Io(error) => Some(error),
73 _ => None,
74 }
75 }
76}
77
78impl From<std::io::Error> for Error {
79 fn from(error: std::io::Error) -> Self {
80 Self::Io(error)
81 }
82}
83
84pub trait Gossip: Send + Sync + 'static {
85 fn announce(&self, package: TransactionPackage) -> bool;
86}
87
88pub trait TransactionSource: Send + Sync {
89 fn request_transactions(&self, transactions: Vec<TransactionId>);
90}
91
92#[derive(Clone, Copy, Debug, Default)]
93pub struct NoopGossip;
94
95impl Gossip for NoopGossip {
96 fn announce(&self, _package: TransactionPackage) -> bool {
97 true
98 }
99}
100
101#[derive(Clone)]
102pub struct Config {
103 pub signing_key: [u8; 32],
104 pub writers_by_priority: Vec<WriterId>,
105 pub gossip: Arc<dyn Gossip>,
106}
107
108impl fmt::Debug for Config {
109 fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
110 formatter
111 .debug_struct("Config")
112 .field("signing_key", &"[redacted]")
113 .field("writers_by_priority", &self.writers_by_priority)
114 .field("gossip", &"dyn Gossip")
115 .finish()
116 }
117}
118
119#[derive(Clone, Debug, Eq, PartialEq)]
120pub struct Provenance {
121 pub author: String,
122 pub source: String,
123 pub source_created_at: DateTime<Utc>,
124 pub data: String,
125}
126
127impl Provenance {
128 pub(crate) fn validate(&self) -> Result<()> {
129 if self.author.trim().is_empty() || self.author.len() > MAX_STRING_BYTES {
130 return Err(Error::invalid_input(
131 "provenance author is empty or too large",
132 ));
133 }
134 if self.source.trim().is_empty() || self.source.len() > MAX_STRING_BYTES {
135 return Err(Error::invalid_input(
136 "provenance source is empty or too large",
137 ));
138 }
139 if self.data.len() > MAX_STRING_BYTES {
140 return Err(Error::invalid_input("provenance data exceeds 1 MiB"));
141 }
142 Ok(())
143 }
144}
145
146#[derive(Clone, Copy, Debug, Eq, PartialEq)]
147pub enum Owner {
148 Unowned,
149 SelfNode,
150 Node(NodeId),
151}
152
153#[derive(Clone, Debug, Eq, PartialEq)]
154pub struct NodeData {
155 pub short_name: String,
156 pub short_description: String,
157 pub long_description: String,
158 pub owner: Owner,
159 pub fixed_connections: Vec<NodeId>,
160 pub recent_connections: Vec<NodeId>,
161 pub objects: Vec<ObjectId>,
162}
163
164impl NodeData {
165 pub(crate) fn validate(&self) -> Result<()> {
166 let characters = self.short_name.chars().count();
167 if !(4..=50).contains(&characters) {
168 return Err(Error::invalid_input(
169 "short_name must contain between 4 and 50 characters",
170 ));
171 }
172 if self.short_description.chars().count() > 200 {
173 return Err(Error::invalid_input(
174 "short_description exceeds 200 characters",
175 ));
176 }
177 if self.long_description.split_whitespace().count() > 1_000 {
178 return Err(Error::invalid_input("long_description exceeds 1,000 words"));
179 }
180 if self.short_name.len() > MAX_STRING_BYTES
181 || self.short_description.len() > MAX_STRING_BYTES
182 || self.long_description.len() > MAX_STRING_BYTES
183 {
184 return Err(Error::invalid_input("node text exceeds 1 MiB"));
185 }
186 ensure_unique(&self.fixed_connections, "fixed_connections")?;
187 ensure_unique(&self.recent_connections, "recent_connections")?;
188 ensure_unique(&self.objects, "objects")?;
189 if self.fixed_connections.iter().any(|id| !id.valid_domain())
190 || self.recent_connections.iter().any(|id| !id.valid_domain())
191 || self.objects.iter().any(|id| !id.valid_domain())
192 || matches!(self.owner, Owner::Node(id) if !id.valid_domain())
193 {
194 return Err(Error::invalid_input(
195 "node contains an identifier in the wrong type domain",
196 ));
197 }
198 Ok(())
199 }
200}
201
202fn ensure_unique<T: Copy + Ord>(values: &[T], name: &str) -> Result<()> {
203 let mut ordered = values.to_vec();
204 ordered.sort_unstable();
205 if ordered.windows(2).any(|pair| pair[0] == pair[1]) {
206 return Err(Error::invalid_input(format!(
207 "{name} must contain unique IDs"
208 )));
209 }
210 Ok(())
211}
212
213#[derive(Clone, Debug, Eq, PartialEq)]
214pub struct Node {
215 pub id: NodeId,
216 pub data: NodeData,
217 pub last_author: String,
218 pub committed_at: DateTime<Utc>,
219}
220
221#[derive(Clone, Copy, Debug, Eq, Ord, PartialEq, PartialOrd)]
222pub struct MergePair {
223 pub first: TransactionId,
224 pub second: TransactionId,
225}
226
227impl MergePair {
228 pub fn new(first: TransactionId, second: TransactionId) -> Result<Self> {
229 if first == second {
230 return Err(Error::invalid_input(
231 "a merge pair must name two different transactions",
232 ));
233 }
234 let (first, second) = if first < second {
235 (first, second)
236 } else {
237 (second, first)
238 };
239 Ok(Self { first, second })
240 }
241}
242
243#[derive(Clone, Debug, Eq, PartialEq)]
244pub struct HistoryEntry {
245 pub transaction_id: TransactionId,
246 pub writer: WriterId,
247 pub committed_at: DateTime<Utc>,
248 pub provenance: Provenance,
249 pub active: bool,
250 pub created: bool,
251 pub updated: bool,
252 pub data: Option<NodeData>,
253 pub merge_pairs: Vec<MergePair>,
254}
255
256#[derive(Clone, Debug, Eq, PartialEq)]
257pub struct NodeHistory {
258 pub node_id: NodeId,
259 pub frontier: Vec<TransactionId>,
260 pub visible: Option<TransactionId>,
261 pub entries: Vec<HistoryEntry>,
262}
263
264#[derive(Eq, PartialEq)]
265pub struct ObjectPayload {
266 pub id: ObjectId,
267 pub bytes: Vec<u8>,
268}
269
270impl fmt::Debug for ObjectPayload {
271 fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
272 formatter
273 .debug_struct("ObjectPayload")
274 .field("id", &self.id)
275 .field("length", &self.bytes.len())
276 .finish()
277 }
278}
279
280#[derive(Eq, PartialEq)]
281pub struct TransactionPackage {
282 pub transaction: Vec<u8>,
283 pub objects: Vec<ObjectPayload>,
284}
285
286impl fmt::Debug for TransactionPackage {
287 fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
288 formatter
289 .debug_struct("TransactionPackage")
290 .field("transaction_bytes", &self.transaction.len())
291 .field("objects", &self.objects)
292 .finish()
293 }
294}