1use crate::{
2 Config, Error, MergePair, Node, NodeData, NodeHistory, NodeId, ObjectId, ObjectPayload, Owner,
3 Provenance, Result, TransactionId, TransactionPackage, TransactionSource, WriterId,
4 store::{self, Candidate, HistoryIndex, NodeFile, TxMeta},
5 wire::{self, NodeOperation, ObjectDeclaration, ParsedTransaction, UnsignedTransaction},
6};
7use chrono::Utc;
8use ed25519_dalek::SigningKey;
9use fs2::FileExt;
10use std::{
11 cmp::Ordering,
12 collections::{BTreeMap, BTreeSet, VecDeque},
13 fmt,
14 fs::{self, File, OpenOptions},
15 path::{Path, PathBuf},
16 sync::{
17 Arc, Mutex, MutexGuard, RwLock,
18 atomic::{AtomicBool, Ordering as AtomicOrdering},
19 },
20};
21
22#[derive(Clone)]
23pub struct KwebDb {
24 inner: Arc<Inner>,
25}
26
27struct Inner {
28 root: PathBuf,
29 _lock_file: File,
30 mutation: Mutex<()>,
31 visibility: RwLock<()>,
32 pending: Mutex<PendingPool>,
33 outbox_draining: AtomicBool,
34 signing_key: SigningKey,
35 local_writer: WriterId,
36 writers: Vec<WriterId>,
37 gossip: Arc<dyn crate::Gossip>,
38 poisoned: AtomicBool,
39}
40
41impl Drop for Inner {
42 fn drop(&mut self) {
43 let _ = store::clear_incoming(&self.root);
44 }
45}
46
47#[derive(Default)]
48struct PendingPool {
49 transactions: BTreeMap<TransactionId, PendingTransaction>,
50 waiting_on: BTreeMap<TransactionId, BTreeSet<TransactionId>>,
51 ready: VecDeque<TransactionId>,
52 object_reservations: BTreeMap<ObjectId, TransactionId>,
53}
54
55struct PendingTransaction {
56 id: TransactionId,
57 transaction: Vec<u8>,
58 objects: Vec<store::SpooledObject>,
59 missing: BTreeSet<TransactionId>,
60}
61
62enum CommitObjects<'a> {
63 Memory(&'a [ObjectPayload]),
64 Spooled(&'a [store::SpooledObject]),
65}
66
67impl fmt::Debug for KwebDb {
68 fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
69 formatter
70 .debug_struct("KwebDb")
71 .field("root", &self.inner.root)
72 .finish_non_exhaustive()
73 }
74}
75
76impl KwebDb {
77 pub fn open(path: impl AsRef<Path>, config: Config) -> Result<Self> {
78 validate_config(&config)?;
79 let root = path.as_ref().to_path_buf();
80 match fs::symlink_metadata(&root) {
81 Ok(metadata) => {
82 if !metadata.file_type().is_dir() || metadata.file_type().is_symlink() {
83 return Err(Error::invalid_config(
84 "database root must be a real directory",
85 ));
86 }
87 }
88 Err(error) if error.kind() == std::io::ErrorKind::NotFound => {
89 fs::create_dir_all(&root)?;
90 let metadata = fs::symlink_metadata(&root)?;
91 if !metadata.file_type().is_dir() || metadata.file_type().is_symlink() {
92 return Err(Error::invalid_config(
93 "database root must be a real directory",
94 ));
95 }
96 }
97 Err(error) => return Err(error.into()),
98 }
99 let lock_file = open_lock_file(&root.join("LOCK"))?;
100 FileExt::try_lock_exclusive(&lock_file).map_err(|error| {
101 Error::Busy(format!(
102 "cannot exclusively lock {}: {error}",
103 root.display()
104 ))
105 })?;
106 store::open_root(&root, &config.writers_by_priority)?;
107 let signing_key = SigningKey::from_bytes(&config.signing_key);
108 let database = Self {
109 inner: Arc::new(Inner {
110 root,
111 _lock_file: lock_file,
112 mutation: Mutex::new(()),
113 visibility: RwLock::new(()),
114 pending: Mutex::new(PendingPool::default()),
115 outbox_draining: AtomicBool::new(false),
116 signing_key,
117 local_writer: WriterId::from_signing_key(&config.signing_key),
118 writers: config.writers_by_priority,
119 gossip: config.gossip,
120 poisoned: AtomicBool::new(false),
121 }),
122 };
123 database.drain_one_outbox()?;
124 Ok(database)
125 }
126
127 pub fn start_transaction(&self, provenance: Provenance) -> Result<Transaction<'_>> {
128 provenance.validate()?;
129 let guard = self
130 .inner
131 .mutation
132 .lock()
133 .map_err(|_| Error::corrupt("mutation mutex is poisoned"))?;
134 self.ensure_healthy()?;
135 let state = store::read_state(&self.inner.root)?;
136 Ok(Transaction {
137 db: self,
138 guard: Some(guard),
139 provenance,
140 heads: state.heads,
141 object_bytes: 0,
142 objects: BTreeMap::new(),
143 reserved_nodes: BTreeSet::new(),
144 creates: BTreeMap::new(),
145 updates: BTreeMap::new(),
146 merges: BTreeSet::new(),
147 })
148 }
149
150 pub fn accept_transaction(&self, package: TransactionPackage) -> Result<bool> {
151 self.accept_transaction_inner(package, None)
152 }
153
154 pub fn accept_gossip_transaction(
155 &self,
156 package: TransactionPackage,
157 source: &dyn TransactionSource,
158 ) -> Result<bool> {
159 self.accept_transaction_inner(package, Some(source))
160 }
161
162 fn accept_transaction_inner(
163 &self,
164 package: TransactionPackage,
165 source: Option<&dyn TransactionSource>,
166 ) -> Result<bool> {
167 let guard = self
168 .inner
169 .mutation
170 .lock()
171 .map_err(|_| Error::corrupt("mutation mutex is poisoned"))?;
172 self.ensure_healthy()?;
173 let parsed = wire::parse_signed_authorized(&package.transaction, &self.inner.writers)?;
174 if parsed.unsigned.heads.contains(&parsed.id) {
175 return Err(Error::invalid_transaction(
176 "transaction cannot name itself as a head",
177 ));
178 }
179 if store::tx_exists(&self.inner.root, parsed.id) {
180 if store::read_signed_bytes(&self.inner.root, parsed.id)? == package.transaction {
181 drop(guard);
182 return Ok(false);
183 }
184 return Err(Error::corrupt(
185 "retained transaction ID has different signed bytes",
186 ));
187 }
188 {
189 let pending = self
190 .inner
191 .pending
192 .lock()
193 .map_err(|_| Error::corrupt("pending mutex is poisoned"))?;
194 if let Some(existing) = pending.transactions.get(&parsed.id) {
195 if existing.transaction == package.transaction {
196 drop(pending);
197 drop(guard);
198 return Ok(false);
199 }
200 return Err(Error::corrupt(
201 "pending transaction ID has different signed bytes",
202 ));
203 }
204 }
205 let missing = self.missing_heads(&parsed)?;
206 if source.is_none()
207 && let Some(first) = missing.first()
208 {
209 return Err(Error::invalid_transaction(format!(
210 "transaction depends on {} uncommitted head(s), beginning with {first}; \
211 only gossip admission may wait for dependencies",
212 missing.len()
213 )));
214 }
215 verify_package(&parsed, &package)?;
216 self.ensure_object_ids_available(parsed.id, &parsed.unsigned.objects)?;
217 let requests = if missing.is_empty() {
218 let TransactionPackage {
219 transaction,
220 objects,
221 } = package;
222 let id = parsed.id;
223 self.commit_new(parsed, &transaction, CommitObjects::Memory(&objects))?;
224 self.process_committed_transaction(id)?;
225 Vec::new()
226 } else {
227 self.enqueue_pending(parsed, package, missing)?
228 };
229 drop(guard);
230 if !requests.is_empty() {
231 source
232 .ok_or_else(|| Error::corrupt("missing gossip transaction source"))?
233 .request_transactions(requests);
234 }
235 self.drain_one_outbox()?;
236 Ok(true)
237 }
238
239 pub fn get_node(&self, id: NodeId) -> Result<Node> {
240 let _guard = self
241 .inner
242 .visibility
243 .read()
244 .map_err(|_| Error::corrupt("visibility lock is poisoned"))?;
245 self.ensure_healthy()?;
246 match store::read_node(&self.inner.root, id) {
247 Ok(node) => Ok(node.node),
248 Err(Error::Io(error)) if error.kind() == std::io::ErrorKind::NotFound => {
249 if store::read_history_index_optional(&self.inner.root, id)?
250 .is_some_and(|history| history.visible.is_some())
251 {
252 Err(Error::corrupt(format!(
253 "authoritative node file {id} is missing"
254 )))
255 } else {
256 Err(Error::not_found(format!("node {id}")))
257 }
258 }
259 Err(error) => Err(error),
260 }
261 }
262
263 pub fn get_node_history(&self, id: NodeId) -> Result<NodeHistory> {
264 let _guard = self
265 .inner
266 .visibility
267 .read()
268 .map_err(|_| Error::corrupt("visibility lock is poisoned"))?;
269 self.ensure_healthy()?;
270 let mut history =
271 store::read_history(&self.inner.root, id).map_err(|error| match error {
272 Error::Io(io) if io.kind() == std::io::ErrorKind::NotFound => {
273 Error::not_found(format!("node history {id}"))
274 }
275 other => other,
276 })?;
277 sort_history(&self.inner.root, &mut history)?;
278 Ok(history)
279 }
280
281 pub fn get_object(&self, id: ObjectId) -> Result<Vec<u8>> {
282 let _guard = self
283 .inner
284 .visibility
285 .read()
286 .map_err(|_| Error::corrupt("visibility lock is poisoned"))?;
287 self.ensure_healthy()?;
288 let (creator, bytes) =
289 store::read_object(&self.inner.root, id).map_err(|error| match error {
290 Error::Io(io) if io.kind() == std::io::ErrorKind::NotFound => {
291 Error::not_found(format!("object {id}"))
292 }
293 other => other,
294 })?;
295 if !store::transaction_committed(&self.inner.root, creator)? {
296 return Err(Error::not_found(format!("object {id} is not committed")));
297 }
298 Ok(bytes)
299 }
300
301 pub fn get_object_with_provenance(&self, id: ObjectId) -> Result<(Vec<u8>, Provenance)> {
303 let _guard = self
304 .inner
305 .visibility
306 .read()
307 .map_err(|_| Error::corrupt("visibility lock is poisoned"))?;
308 self.ensure_healthy()?;
309 let (creator, bytes) =
310 store::read_object(&self.inner.root, id).map_err(|error| match error {
311 Error::Io(io) if io.kind() == std::io::ErrorKind::NotFound => {
312 Error::not_found(format!("object {id}"))
313 }
314 other => other,
315 })?;
316 if !store::transaction_committed(&self.inner.root, creator)? {
317 return Err(Error::not_found(format!("object {id} is not committed")));
318 }
319 let signed = store::read_signed_bytes(&self.inner.root, creator)?;
320 let parsed = wire::parse_signed(&signed)?;
321 if parsed.id != creator {
322 return Err(Error::corrupt(format!(
323 "object {id} names a mismatched creating transaction"
324 )));
325 }
326 Ok((bytes, parsed.unsigned.provenance))
327 }
328
329 fn commit_new(
330 &self,
331 parsed: ParsedTransaction,
332 transaction: &[u8],
333 objects: CommitObjects<'_>,
334 ) -> Result<()> {
335 let mut state = store::read_state(&self.inner.root)?;
336 for parent in &parsed.unsigned.heads {
337 if !store::transaction_committed(&self.inner.root, *parent)? {
338 return Err(Error::invalid_transaction(format!(
339 "transaction head {parent} has not been processed"
340 )));
341 }
342 }
343 let dag_generation = next_dag_generation(
344 parsed
345 .unsigned
346 .heads
347 .iter()
348 .map(|parent| store::dag_generation(&self.inner.root, *parent))
349 .collect::<Result<Vec<_>>>()?,
350 )?;
351 let meta = TxMeta {
352 format: store::record_version(),
353 id: parsed.id,
354 parents: parsed.unsigned.heads.clone(),
355 dag_generation,
356 };
357 let (nodes, histories) = self.project_transaction(&parsed)?;
358 let frame_length = store::log_frame_length(transaction.len())?;
359 let mut commit = store::Commit::new(&self.inner.root, parsed.id, &state)?;
360 commit.stage_log(parsed.id, transaction)?;
361
362 match objects {
363 CommitObjects::Memory(objects) => {
364 for payload in objects {
365 if store::object_exists(&self.inner.root, payload.id) {
366 return Err(Error::invalid_transaction(format!(
367 "object locator collision at {}",
368 payload.id
369 )));
370 }
371 commit.stage_object(store::object_rel(payload.id), parsed.id, payload)?;
372 }
373 }
374 CommitObjects::Spooled(objects) => {
375 if objects.len() != parsed.unsigned.objects.len() {
376 return Err(Error::corrupt(
377 "spooled object count differs from signed transaction",
378 ));
379 }
380 for (object, declaration) in objects.iter().zip(&parsed.unsigned.objects) {
381 if object.id != declaration.id {
382 return Err(Error::corrupt(
383 "spooled object order differs from signed transaction",
384 ));
385 }
386 if store::object_exists(&self.inner.root, object.id) {
387 return Err(Error::invalid_transaction(format!(
388 "object locator collision at {}",
389 object.id
390 )));
391 }
392 commit.stage_spooled_object(store::object_rel(object.id), object)?;
393 }
394 }
395 }
396 commit.stage_signed_transaction(store::tx_bytes_rel(parsed.id), parsed.id, transaction)?;
397 commit.stage_bytes(
398 store::tx_meta_rel(parsed.id),
399 &store::tx_meta_bytes(&meta)?,
400 true,
401 )?;
402 commit.stage_bytes(
403 store::outbox_rel(parsed.id),
404 &store::queue_bytes(parsed.id)?,
405 true,
406 )?;
407 stage_projection(&mut commit, nodes, histories)?;
408
409 for parent in &parsed.unsigned.heads {
410 state.heads.retain(|head| head != parent);
411 }
412 state.heads.push(parsed.id);
413 state.heads.sort();
414 state.heads.dedup();
415 state.generation = state
416 .generation
417 .checked_add(1)
418 .ok_or_else(|| Error::corrupt("database generation overflow"))?;
419 state.log_offset = state
420 .log_offset
421 .checked_add(frame_length)
422 .ok_or_else(|| Error::corrupt("transaction log offset overflow"))?;
423 commit.stage_bytes(
424 PathBuf::from("state.kws"),
425 &store::state_bytes(&state)?,
426 false,
427 )?;
428 let _visibility = self
429 .inner
430 .visibility
431 .write()
432 .map_err(|_| Error::corrupt("visibility lock is poisoned"))?;
433 self.finish_commit(commit)
434 }
435
436 fn project_transaction(
437 &self,
438 parsed: &ParsedTransaction,
439 ) -> Result<(BTreeMap<NodeId, NodeFile>, BTreeMap<NodeId, HistoryUpdate>)> {
440 let overlay = store::DagOverlay {
441 id: parsed.id,
442 parents: &parsed.unsigned.heads,
443 };
444 let current_creates = parsed
445 .unsigned
446 .creates
447 .iter()
448 .map(|operation| operation.id)
449 .collect::<BTreeSet<_>>();
450 let current_objects = parsed
451 .unsigned
452 .objects
453 .iter()
454 .map(|object| object.id)
455 .collect::<BTreeSet<_>>();
456 let current_creates =
457 self.resolvable_current_creates(parsed, current_creates, ¤t_objects, &overlay)?;
458 let mut nodes = BTreeMap::new();
459 let mut histories = BTreeMap::new();
460 for (created, operation) in parsed
461 .unsigned
462 .creates
463 .iter()
464 .map(|operation| (true, operation))
465 .chain(
466 parsed
467 .unsigned
468 .updates
469 .iter()
470 .map(|operation| (false, operation)),
471 )
472 {
473 let mut history = store::read_history_index_optional(&self.inner.root, operation.id)?
474 .unwrap_or_else(|| store::empty_history(operation.id));
475 let existing_node = store::read_node_optional(&self.inner.root, operation.id)?;
476 let references = self.references_resolve(
477 parsed.id,
478 operation.id,
479 &operation.data,
480 ¤t_creates,
481 ¤t_objects,
482 &overlay,
483 )?;
484 let effective = if created {
485 references && !self.has_ancestor_create(&history, parsed.id, &overlay)?
486 } else {
487 references && self.has_ancestor_create(&history, parsed.id, &overlay)?
488 };
489 let node = if effective {
490 Some(self.apply_candidate(parsed, operation, existing_node, &overlay)?)
491 } else {
492 existing_node
493 };
494 let existing_entry =
495 store::read_history_entry_optional(&self.inner.root, operation.id, parsed.id)?;
496 if existing_entry.is_some() {
497 return Err(Error::corrupt(
498 "new transaction collides with an existing history entry",
499 ));
500 }
501 let entry = store::history_entry(parsed, operation.data.clone(), created);
502 if effective && created {
503 history.creations.push(parsed.id);
504 history.creations.sort();
505 history.creations.dedup();
506 }
507 if let Some(node) = &node {
508 history.frontier = node
509 .frontier
510 .iter()
511 .map(|candidate| candidate.transaction)
512 .collect();
513 history.visible = Some(node.visible_transaction);
514 }
515 if let Some(node) = node {
516 nodes.insert(operation.id, node);
517 }
518 histories.insert(
519 operation.id,
520 HistoryUpdate {
521 index: history,
522 entry,
523 create_entry: true,
524 },
525 );
526 }
527 Ok((nodes, histories))
528 }
529
530 fn resolvable_current_creates(
531 &self,
532 parsed: &ParsedTransaction,
533 mut resolvable: BTreeSet<NodeId>,
534 current_objects: &BTreeSet<ObjectId>,
535 overlay: &store::DagOverlay<'_>,
536 ) -> Result<BTreeSet<NodeId>> {
537 loop {
538 let mut invalid = Vec::new();
539 for operation in &parsed.unsigned.creates {
540 if resolvable.contains(&operation.id)
541 && !self.references_resolve(
542 parsed.id,
543 operation.id,
544 &operation.data,
545 &resolvable,
546 current_objects,
547 overlay,
548 )?
549 {
550 invalid.push(operation.id);
551 }
552 }
553 if invalid.is_empty() {
554 return Ok(resolvable);
555 }
556 for id in invalid {
557 resolvable.remove(&id);
558 }
559 }
560 }
561
562 fn apply_candidate(
563 &self,
564 parsed: &ParsedTransaction,
565 operation: &NodeOperation,
566 existing: Option<NodeFile>,
567 overlay: &store::DagOverlay<'_>,
568 ) -> Result<NodeFile> {
569 let mut frontier = existing.map_or_else(Vec::new, |node| node.frontier);
570 for candidate in &frontier {
571 if store::is_ancestor(
572 &self.inner.root,
573 parsed.id,
574 candidate.transaction,
575 Some(overlay),
576 )? {
577 return preferred_node(operation.id, frontier, &self.inner.writers);
578 }
579 }
580 let mut retained = Vec::with_capacity(frontier.len() + 1);
581 for candidate in frontier.drain(..) {
582 if !store::is_ancestor(
583 &self.inner.root,
584 candidate.transaction,
585 parsed.id,
586 Some(overlay),
587 )? {
588 retained.push(candidate);
589 }
590 }
591 retained.push(Candidate {
592 transaction: parsed.id,
593 writer: parsed.unsigned.writer,
594 committed_at: parsed.unsigned.committed_at,
595 provenance: parsed.unsigned.provenance.clone(),
596 data: operation.data.clone(),
597 });
598 retained.sort_by_key(|candidate| candidate.transaction);
599 preferred_node(operation.id, retained, &self.inner.writers)
600 }
601
602 fn has_ancestor_create(
603 &self,
604 history: &HistoryIndex,
605 transaction: TransactionId,
606 overlay: &store::DagOverlay<'_>,
607 ) -> Result<bool> {
608 for creation in &history.creations {
609 if *creation != transaction
610 && store::is_ancestor(&self.inner.root, *creation, transaction, Some(overlay))?
611 {
612 return Ok(true);
613 }
614 }
615 Ok(false)
616 }
617
618 fn references_resolve(
619 &self,
620 transaction: TransactionId,
621 self_id: NodeId,
622 data: &NodeData,
623 current_creates: &BTreeSet<NodeId>,
624 current_objects: &BTreeSet<ObjectId>,
625 overlay: &store::DagOverlay<'_>,
626 ) -> Result<bool> {
627 let mut node_refs = data
628 .fixed_connections
629 .iter()
630 .chain(&data.recent_connections)
631 .copied()
632 .collect::<Vec<_>>();
633 if let Owner::Node(owner) = data.owner {
634 node_refs.push(owner);
635 }
636 for reference in node_refs {
637 if reference == self_id || current_creates.contains(&reference) {
638 continue;
639 }
640 let Some(history) = store::read_history_index_optional(&self.inner.root, reference)?
641 else {
642 return Ok(false);
643 };
644 if !self.has_ancestor_create(&history, transaction, overlay)? {
645 return Ok(false);
646 }
647 }
648 for object in &data.objects {
649 if current_objects.contains(object) {
650 continue;
651 }
652 let creator = match store::object_creator(&self.inner.root, *object) {
653 Ok(creator) => creator,
654 Err(Error::Io(error)) if error.kind() == std::io::ErrorKind::NotFound => {
655 return Ok(false);
656 }
657 Err(error) => return Err(error),
658 };
659 if !store::transaction_committed(&self.inner.root, creator)?
660 || !store::is_ancestor(&self.inner.root, creator, transaction, Some(overlay))?
661 {
662 return Ok(false);
663 }
664 }
665 Ok(true)
666 }
667
668 fn missing_heads(&self, parsed: &ParsedTransaction) -> Result<BTreeSet<TransactionId>> {
669 let mut missing = BTreeSet::new();
670 for head in &parsed.unsigned.heads {
671 if !store::transaction_committed(&self.inner.root, *head)? {
672 missing.insert(*head);
673 }
674 }
675 Ok(missing)
676 }
677
678 fn enqueue_pending(
679 &self,
680 parsed: ParsedTransaction,
681 package: TransactionPackage,
682 missing: BTreeSet<TransactionId>,
683 ) -> Result<Vec<TransactionId>> {
684 let TransactionPackage {
685 transaction,
686 objects,
687 } = package;
688 let objects = store::spool_objects(&self.inner.root, parsed.id, objects)?;
689 let mut pending = self
690 .inner
691 .pending
692 .lock()
693 .map_err(|_| Error::corrupt("pending mutex is poisoned"))?;
694 let requests = missing
695 .iter()
696 .filter(|head| !pending.transactions.contains_key(head))
697 .copied()
698 .collect::<Vec<_>>();
699 for head in &missing {
700 pending
701 .waiting_on
702 .entry(*head)
703 .or_default()
704 .insert(parsed.id);
705 }
706 for object in &objects {
707 pending.object_reservations.insert(object.id, parsed.id);
708 }
709 pending.transactions.insert(
710 parsed.id,
711 PendingTransaction {
712 id: parsed.id,
713 transaction,
714 objects,
715 missing,
716 },
717 );
718 Ok(requests)
719 }
720
721 fn ensure_object_ids_available(
722 &self,
723 transaction: TransactionId,
724 declarations: &[ObjectDeclaration],
725 ) -> Result<()> {
726 let pending = self
727 .inner
728 .pending
729 .lock()
730 .map_err(|_| Error::corrupt("pending mutex is poisoned"))?;
731 for declaration in declarations {
732 if store::object_exists(&self.inner.root, declaration.id)
733 || pending
734 .object_reservations
735 .get(&declaration.id)
736 .is_some_and(|owner| *owner != transaction)
737 {
738 return Err(Error::invalid_transaction(format!(
739 "object locator collision at {}",
740 declaration.id
741 )));
742 }
743 }
744 Ok(())
745 }
746
747 fn process_committed_transaction(&self, committed: TransactionId) -> Result<()> {
748 self.release_pending_dependents(committed)?;
749 loop {
750 let pending_transaction = {
751 let mut pending = self
752 .inner
753 .pending
754 .lock()
755 .map_err(|_| Error::corrupt("pending mutex is poisoned"))?;
756 let Some(id) = pending.ready.pop_front() else {
757 return Ok(());
758 };
759 pending.transactions.remove(&id).ok_or_else(|| {
760 Error::corrupt("ready transaction is missing from the pending pool")
761 })?
762 };
763 let parsed = match wire::parse_signed_authorized(
764 &pending_transaction.transaction,
765 &self.inner.writers,
766 ) {
767 Ok(parsed) => parsed,
768 Err(error) => {
769 self.requeue_pending(pending_transaction)?;
770 return Err(error);
771 }
772 };
773 let missing = match self.missing_heads(&parsed) {
774 Ok(missing) => missing,
775 Err(error) => {
776 self.requeue_pending(pending_transaction)?;
777 return Err(error);
778 }
779 };
780 if parsed.id != pending_transaction.id || !missing.is_empty() {
781 self.requeue_pending(pending_transaction)?;
782 return Err(Error::corrupt(
783 "ready transaction still has an unresolved dependency",
784 ));
785 }
786 let id = pending_transaction.id;
787 let result = self.commit_new(
788 parsed,
789 &pending_transaction.transaction,
790 CommitObjects::Spooled(&pending_transaction.objects),
791 );
792 if let Err(error) = result {
793 self.requeue_pending(pending_transaction)?;
794 return Err(error);
795 }
796 self.complete_pending(id)?;
797 let _ = store::discard_spooled_objects(&self.inner.root, id);
798 self.release_pending_dependents(id)?;
799 }
800 }
801
802 fn release_pending_dependents(&self, committed: TransactionId) -> Result<()> {
803 let mut pending = self
804 .inner
805 .pending
806 .lock()
807 .map_err(|_| Error::corrupt("pending mutex is poisoned"))?;
808 let Some(dependents) = pending.waiting_on.remove(&committed) else {
809 return Ok(());
810 };
811 for dependent in dependents {
812 let transaction = pending.transactions.get_mut(&dependent).ok_or_else(|| {
813 Error::corrupt("dependency waiter is missing from the pending pool")
814 })?;
815 transaction.missing.remove(&committed);
816 if transaction.missing.is_empty() {
817 pending.ready.push_back(dependent);
818 }
819 }
820 Ok(())
821 }
822
823 fn complete_pending(&self, id: TransactionId) -> Result<()> {
824 let mut pending = self
825 .inner
826 .pending
827 .lock()
828 .map_err(|_| Error::corrupt("pending mutex is poisoned"))?;
829 pending
830 .object_reservations
831 .retain(|_, transaction| *transaction != id);
832 Ok(())
833 }
834
835 fn requeue_pending(&self, transaction: PendingTransaction) -> Result<()> {
836 let mut pending = self
837 .inner
838 .pending
839 .lock()
840 .map_err(|_| Error::corrupt("pending mutex is poisoned"))?;
841 pending.ready.push_front(transaction.id);
842 pending.transactions.insert(transaction.id, transaction);
843 Ok(())
844 }
845
846 fn drain_one_outbox(&self) -> Result<()> {
847 if self
848 .inner
849 .outbox_draining
850 .compare_exchange(false, true, AtomicOrdering::AcqRel, AtomicOrdering::Acquire)
851 .is_err()
852 {
853 return Ok(());
854 }
855 let _drain_guard = AtomicFlagGuard(&self.inner.outbox_draining);
856 store::recover_outbox_claim(&self.inner.root)?;
857 let Some(id) = store::next_outbox(&self.inner.root)? else {
858 return Ok(());
859 };
860 store::claim_outbox(&self.inner.root, id)?;
861 let package = match store::package_for(&self.inner.root, id) {
862 Ok(package) => package,
863 Err(error) => {
864 store::requeue_outbox_claim(&self.inner.root, id)?;
865 return Err(error);
866 }
867 };
868 if self.inner.gossip.announce(package) {
869 store::acknowledge_outbox_claim(&self.inner.root)?;
870 } else {
871 store::requeue_outbox_claim(&self.inner.root, id)?;
872 }
873 Ok(())
874 }
875
876 fn finish_commit(&self, commit: store::Commit) -> Result<()> {
877 match commit.finish() {
878 Ok(()) => Ok(()),
879 Err(failure) => {
880 if failure.prepared {
881 self.inner.poisoned.store(true, AtomicOrdering::Release);
882 }
883 Err(failure.error)
884 }
885 }
886 }
887
888 fn ensure_healthy(&self) -> Result<()> {
889 if self.inner.poisoned.load(AtomicOrdering::Acquire) {
890 Err(Error::corrupt(
891 "a prepared WAL could not be fully applied; close and reopen the database",
892 ))
893 } else {
894 Ok(())
895 }
896 }
897}
898
899struct AtomicFlagGuard<'a>(&'a AtomicBool);
900
901impl Drop for AtomicFlagGuard<'_> {
902 fn drop(&mut self) {
903 self.0.store(false, AtomicOrdering::Release);
904 }
905}
906
907pub struct Transaction<'a> {
908 db: &'a KwebDb,
909 guard: Option<MutexGuard<'a, ()>>,
910 provenance: Provenance,
911 heads: Vec<TransactionId>,
912 object_bytes: u64,
913 objects: BTreeMap<ObjectId, Vec<u8>>,
914 reserved_nodes: BTreeSet<NodeId>,
915 creates: BTreeMap<NodeId, NodeData>,
916 updates: BTreeMap<NodeId, NodeData>,
917 merges: BTreeSet<MergePair>,
918}
919
920impl fmt::Debug for Transaction<'_> {
921 fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
922 formatter
923 .debug_struct("Transaction")
924 .field("heads", &self.heads)
925 .field("object_count", &self.objects.len())
926 .field("object_bytes", &self.object_bytes)
927 .field("reserved_nodes", &self.reserved_nodes.len())
928 .field("creates", &self.creates.len())
929 .field("updates", &self.updates.len())
930 .field("merges", &self.merges.len())
931 .finish_non_exhaustive()
932 }
933}
934
935impl Transaction<'_> {
936 pub fn create_object(&mut self, bytes: Vec<u8>) -> Result<ObjectId> {
937 let length = bytes.len() as u64;
938 if length > crate::MAX_OBJECT_BYTES {
939 return Err(Error::invalid_input("object exceeds the 32 GiB limit"));
940 }
941 let object_bytes = self
942 .object_bytes
943 .checked_add(length)
944 .ok_or_else(|| Error::invalid_input("transaction object length overflow"))?;
945 if object_bytes > crate::MAX_TRANSACTION_OBJECT_BYTES {
946 return Err(Error::invalid_input(
947 "transaction object payload total exceeds 32 GiB",
948 ));
949 }
950 let id = loop {
951 let candidate = ObjectId::random();
952 if !self.objects.contains_key(&candidate)
953 && !store::object_exists(&self.db.inner.root, candidate)
954 && !self
955 .db
956 .inner
957 .pending
958 .lock()
959 .map_err(|_| Error::corrupt("pending mutex is poisoned"))?
960 .object_reservations
961 .contains_key(&candidate)
962 {
963 break candidate;
964 }
965 };
966 self.object_bytes = object_bytes;
967 self.objects.insert(id, bytes);
968 Ok(id)
969 }
970
971 pub fn reserve_node_id(&mut self) -> Result<NodeId> {
972 let id = loop {
973 let candidate = NodeId::random();
974 if !self.reserved_nodes.contains(&candidate)
975 && !self.creates.contains_key(&candidate)
976 && !store::node_id_occupied(&self.db.inner.root, candidate)
977 {
978 break candidate;
979 }
980 };
981 self.reserved_nodes.insert(id);
982 Ok(id)
983 }
984
985 pub fn create_reserved_node(&mut self, id: NodeId, data: NodeData) -> Result<()> {
986 if self.creates.contains_key(&id) {
987 return Err(Error::invalid_input(format!(
988 "reserved node {id} has already been materialized"
989 )));
990 }
991 if !self.reserved_nodes.contains(&id) {
992 return Err(Error::invalid_input(format!(
993 "node ID {id} was not reserved by this transaction"
994 )));
995 }
996 data.validate()?;
997 self.reserved_nodes.remove(&id);
998 self.creates.insert(id, data);
999 Ok(())
1000 }
1001
1002 pub fn create_node(&mut self, data: NodeData) -> Result<NodeId> {
1003 data.validate()?;
1004 let id = self.reserve_node_id()?;
1005 self.create_reserved_node(id, data)?;
1006 Ok(id)
1007 }
1008
1009 pub fn update_node(&mut self, id: NodeId, data: NodeData) -> Result<()> {
1010 if self.reserved_nodes.contains(&id) || self.creates.contains_key(&id) {
1011 return Err(Error::invalid_input(
1012 "one transaction cannot create and update the same node",
1013 ));
1014 }
1015 data.validate()?;
1016 if self.updates.insert(id, data).is_some() {
1017 return Err(Error::invalid_input(
1018 "one transaction cannot update the same node twice",
1019 ));
1020 }
1021 Ok(())
1022 }
1023
1024 pub fn merge(&mut self, first: TransactionId, second: TransactionId) -> Result<()> {
1025 let pair = MergePair::new(first, second)?;
1026 if !self.merges.insert(pair) {
1027 return Err(Error::invalid_input("duplicate merge pair"));
1028 }
1029 Ok(())
1030 }
1031
1032 pub fn finalize(mut self) -> Result<TransactionId> {
1033 self.validate_local()?;
1034 let unsigned = UnsignedTransaction {
1035 writer: self.db.inner.local_writer,
1036 committed_at: Utc::now(),
1037 heads: self.heads.clone(),
1038 provenance: self.provenance.clone(),
1039 merge_pairs: self.merges.iter().copied().collect(),
1040 objects: self
1041 .objects
1042 .iter()
1043 .map(|(id, bytes)| ObjectDeclaration {
1044 id: *id,
1045 length: bytes.len() as u64,
1046 sha256: wire::object_hash(bytes),
1047 })
1048 .collect(),
1049 creates: self
1050 .creates
1051 .iter()
1052 .map(|(id, data)| NodeOperation {
1053 id: *id,
1054 data: data.clone(),
1055 })
1056 .collect(),
1057 updates: self
1058 .updates
1059 .iter()
1060 .map(|(id, data)| NodeOperation {
1061 id: *id,
1062 data: data.clone(),
1063 })
1064 .collect(),
1065 };
1066 let transaction = wire::build_signed(&unsigned, &self.db.inner.signing_key)?;
1067 let parsed = wire::parse_signed(&transaction)?;
1068 let id = parsed.id;
1069 let objects = self
1070 .objects
1071 .into_iter()
1072 .map(|(id, bytes)| ObjectPayload { id, bytes })
1073 .collect::<Vec<_>>();
1074 self.db
1075 .commit_new(parsed, &transaction, CommitObjects::Memory(&objects))?;
1076 drop(objects);
1077 self.db.process_committed_transaction(id)?;
1078 drop(self.guard.take());
1079 self.db.drain_one_outbox()?;
1080 Ok(id)
1081 }
1082
1083 fn validate_local(&self) -> Result<()> {
1084 if !self.reserved_nodes.is_empty() {
1085 return Err(Error::invalid_input(format!(
1086 "transaction has {} unmaterialized reserved node ID(s)",
1087 self.reserved_nodes.len()
1088 )));
1089 }
1090 let created = self.creates.keys().copied().collect::<BTreeSet<_>>();
1091 let objects = self.objects.keys().copied().collect::<BTreeSet<_>>();
1092 for (id, data) in self.creates.iter().chain(&self.updates) {
1093 if self.updates.contains_key(id) && !store::node_exists(&self.db.inner.root, *id) {
1094 return Err(Error::invalid_input(format!(
1095 "cannot update nonvisible node {id}"
1096 )));
1097 }
1098 validate_local_references(&self.db.inner.root, *id, data, &created, &objects)?;
1099 }
1100 for pair in &self.merges {
1101 let valid = self.updates.keys().any(|id| {
1102 store::read_node(&self.db.inner.root, *id)
1103 .map(|node| {
1104 let frontier = node
1105 .frontier
1106 .iter()
1107 .map(|candidate| candidate.transaction)
1108 .collect::<BTreeSet<_>>();
1109 frontier.contains(&pair.first) && frontier.contains(&pair.second)
1110 })
1111 .unwrap_or(false)
1112 });
1113 if !valid {
1114 return Err(Error::invalid_input(
1115 "merge pair is not an exact current frontier for an updated node",
1116 ));
1117 }
1118 }
1119 Ok(())
1120 }
1121}
1122
1123fn validate_local_references(
1124 root: &Path,
1125 self_id: NodeId,
1126 data: &NodeData,
1127 created: &BTreeSet<NodeId>,
1128 objects: &BTreeSet<ObjectId>,
1129) -> Result<()> {
1130 let mut node_refs = data
1131 .fixed_connections
1132 .iter()
1133 .chain(&data.recent_connections)
1134 .copied()
1135 .collect::<Vec<_>>();
1136 if let Owner::Node(owner) = data.owner {
1137 node_refs.push(owner);
1138 }
1139 for reference in node_refs {
1140 if reference != self_id
1141 && !created.contains(&reference)
1142 && !store::node_exists(root, reference)
1143 {
1144 return Err(Error::invalid_input(format!(
1145 "node reference {reference} is not locally resolvable"
1146 )));
1147 }
1148 }
1149 for object in &data.objects {
1150 if !objects.contains(object) && !store::object_exists(root, *object) {
1151 return Err(Error::invalid_input(format!(
1152 "object {object} is not locally resolvable"
1153 )));
1154 }
1155 }
1156 Ok(())
1157}
1158
1159fn preferred_node(id: NodeId, frontier: Vec<Candidate>, writers: &[WriterId]) -> Result<NodeFile> {
1160 let visible = frontier
1161 .iter()
1162 .max_by(|left, right| compare_preference(left, right, writers))
1163 .ok_or_else(|| Error::corrupt("node frontier is empty"))?;
1164 Ok(NodeFile {
1165 format: store::record_version(),
1166 id,
1167 node: Node {
1168 id,
1169 data: visible.data.clone(),
1170 last_author: visible.provenance.author.clone(),
1171 committed_at: visible.committed_at,
1172 },
1173 visible_transaction: visible.transaction,
1174 frontier,
1175 })
1176}
1177
1178fn compare_preference(left: &Candidate, right: &Candidate, writers: &[WriterId]) -> Ordering {
1179 let left_rank = writers
1180 .iter()
1181 .position(|writer| *writer == left.writer)
1182 .unwrap_or(usize::MAX);
1183 let right_rank = writers
1184 .iter()
1185 .position(|writer| *writer == right.writer)
1186 .unwrap_or(usize::MAX);
1187 right_rank
1188 .cmp(&left_rank)
1189 .then_with(|| left.transaction.cmp(&right.transaction))
1190}
1191
1192fn next_dag_generation(generations: Vec<u64>) -> Result<u64> {
1193 match generations.into_iter().max() {
1194 Some(generation) => generation
1195 .checked_add(1)
1196 .ok_or_else(|| Error::corrupt("transaction DAG generation overflow")),
1197 None => Ok(0),
1198 }
1199}
1200
1201fn sort_history(root: &Path, history: &mut NodeHistory) -> Result<()> {
1202 let mut generations = BTreeMap::new();
1203 for entry in &history.entries {
1204 let generation = store::read_tx_meta(root, entry.transaction_id)?.dag_generation;
1205 generations.insert(entry.transaction_id, generation);
1206 }
1207 history.entries.sort_by(|left, right| {
1208 (generations[&right.transaction_id], right.transaction_id)
1209 .cmp(&(generations[&left.transaction_id], left.transaction_id))
1210 });
1211 Ok(())
1212}
1213
1214fn stage_projection(
1215 commit: &mut store::Commit,
1216 nodes: BTreeMap<NodeId, NodeFile>,
1217 histories: BTreeMap<NodeId, HistoryUpdate>,
1218) -> Result<()> {
1219 for (id, node) in nodes {
1220 commit.stage_bytes(store::node_rel(id), &store::node_bytes(&node)?, false)?;
1221 }
1222 for (id, history) in histories {
1223 commit.stage_bytes(
1224 store::history_index_rel(id),
1225 &store::history_index_bytes(&history.index)?,
1226 false,
1227 )?;
1228 commit.stage_bytes(
1229 store::history_entry_rel(id, history.entry.transaction_id),
1230 &store::history_entry_bytes(&history.entry)?,
1231 history.create_entry,
1232 )?;
1233 }
1234 Ok(())
1235}
1236
1237struct HistoryUpdate {
1238 index: HistoryIndex,
1239 entry: crate::HistoryEntry,
1240 create_entry: bool,
1241}
1242
1243fn verify_package(parsed: &ParsedTransaction, package: &TransactionPackage) -> Result<()> {
1244 if parsed.unsigned.objects.len() != package.objects.len() {
1245 return Err(Error::invalid_transaction(
1246 "package does not contain exactly every declared object",
1247 ));
1248 }
1249 let mut total = 0_u64;
1250 for (declaration, payload) in parsed.unsigned.objects.iter().zip(&package.objects) {
1251 if declaration.id != payload.id {
1252 return Err(Error::invalid_transaction(
1253 "package object order or ID differs",
1254 ));
1255 }
1256 let length = payload.bytes.len() as u64;
1257 total = total
1258 .checked_add(length)
1259 .ok_or_else(|| Error::invalid_transaction("package object length overflow"))?;
1260 if length != declaration.length
1261 || length > crate::MAX_OBJECT_BYTES
1262 || total > crate::MAX_TRANSACTION_OBJECT_BYTES
1263 {
1264 return Err(Error::invalid_transaction(
1265 "package object length is invalid",
1266 ));
1267 }
1268 if wire::object_hash(&payload.bytes) != declaration.sha256 {
1269 return Err(Error::invalid_transaction(
1270 "package object SHA-256 mismatch",
1271 ));
1272 }
1273 }
1274 Ok(())
1275}
1276
1277fn validate_config(config: &Config) -> Result<()> {
1278 if config.writers_by_priority.is_empty() {
1279 return Err(Error::invalid_config("writers_by_priority cannot be empty"));
1280 }
1281 let writers = config
1282 .writers_by_priority
1283 .iter()
1284 .copied()
1285 .collect::<BTreeSet<_>>();
1286 if writers.len() != config.writers_by_priority.len() {
1287 return Err(Error::invalid_config("writers_by_priority must be unique"));
1288 }
1289 if config
1290 .writers_by_priority
1291 .iter()
1292 .any(|writer| ed25519_dalek::VerifyingKey::from_bytes(&writer.0).is_err())
1293 {
1294 return Err(Error::invalid_config(
1295 "writers_by_priority contains an invalid Ed25519 public key",
1296 ));
1297 }
1298 if !writers.contains(&WriterId::from_signing_key(&config.signing_key)) {
1299 return Err(Error::invalid_config(
1300 "writers_by_priority must contain the local writer",
1301 ));
1302 }
1303 Ok(())
1304}
1305
1306fn open_lock_file(path: &Path) -> Result<File> {
1307 let file = match fs::symlink_metadata(path) {
1308 Ok(metadata) => {
1309 if metadata.file_type().is_symlink() || !metadata.file_type().is_file() {
1310 return Err(Error::invalid_config(
1311 "database LOCK must be a regular file",
1312 ));
1313 }
1314 OpenOptions::new().read(true).write(true).open(path)?
1315 }
1316 Err(error) if error.kind() == std::io::ErrorKind::NotFound => OpenOptions::new()
1317 .read(true)
1318 .write(true)
1319 .create_new(true)
1320 .open(path)?,
1321 Err(error) => return Err(error.into()),
1322 };
1323 if !file.metadata()?.file_type().is_file() {
1324 return Err(Error::invalid_config(
1325 "database LOCK must be a regular file",
1326 ));
1327 }
1328 Ok(file)
1329}