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 fn commit_new(
302 &self,
303 parsed: ParsedTransaction,
304 transaction: &[u8],
305 objects: CommitObjects<'_>,
306 ) -> Result<()> {
307 let mut state = store::read_state(&self.inner.root)?;
308 for parent in &parsed.unsigned.heads {
309 if !store::transaction_committed(&self.inner.root, *parent)? {
310 return Err(Error::invalid_transaction(format!(
311 "transaction head {parent} has not been processed"
312 )));
313 }
314 }
315 let dag_generation = next_dag_generation(
316 parsed
317 .unsigned
318 .heads
319 .iter()
320 .map(|parent| store::dag_generation(&self.inner.root, *parent))
321 .collect::<Result<Vec<_>>>()?,
322 )?;
323 let meta = TxMeta {
324 format: store::record_version(),
325 id: parsed.id,
326 parents: parsed.unsigned.heads.clone(),
327 dag_generation,
328 };
329 let (nodes, histories) = self.project_transaction(&parsed)?;
330 let frame_length = store::log_frame_length(transaction.len())?;
331 let mut commit = store::Commit::new(&self.inner.root, parsed.id, &state)?;
332 commit.stage_log(parsed.id, transaction)?;
333
334 match objects {
335 CommitObjects::Memory(objects) => {
336 for payload in objects {
337 if store::object_exists(&self.inner.root, payload.id) {
338 return Err(Error::invalid_transaction(format!(
339 "object locator collision at {}",
340 payload.id
341 )));
342 }
343 commit.stage_object(store::object_rel(payload.id), parsed.id, payload)?;
344 }
345 }
346 CommitObjects::Spooled(objects) => {
347 if objects.len() != parsed.unsigned.objects.len() {
348 return Err(Error::corrupt(
349 "spooled object count differs from signed transaction",
350 ));
351 }
352 for (object, declaration) in objects.iter().zip(&parsed.unsigned.objects) {
353 if object.id != declaration.id {
354 return Err(Error::corrupt(
355 "spooled object order differs from signed transaction",
356 ));
357 }
358 if store::object_exists(&self.inner.root, object.id) {
359 return Err(Error::invalid_transaction(format!(
360 "object locator collision at {}",
361 object.id
362 )));
363 }
364 commit.stage_spooled_object(store::object_rel(object.id), object)?;
365 }
366 }
367 }
368 commit.stage_signed_transaction(store::tx_bytes_rel(parsed.id), parsed.id, transaction)?;
369 commit.stage_bytes(
370 store::tx_meta_rel(parsed.id),
371 &store::tx_meta_bytes(&meta)?,
372 true,
373 )?;
374 commit.stage_bytes(
375 store::outbox_rel(parsed.id),
376 &store::queue_bytes(parsed.id)?,
377 true,
378 )?;
379 stage_projection(&mut commit, nodes, histories)?;
380
381 for parent in &parsed.unsigned.heads {
382 state.heads.retain(|head| head != parent);
383 }
384 state.heads.push(parsed.id);
385 state.heads.sort();
386 state.heads.dedup();
387 state.generation = state
388 .generation
389 .checked_add(1)
390 .ok_or_else(|| Error::corrupt("database generation overflow"))?;
391 state.log_offset = state
392 .log_offset
393 .checked_add(frame_length)
394 .ok_or_else(|| Error::corrupt("transaction log offset overflow"))?;
395 commit.stage_bytes(
396 PathBuf::from("state.kws"),
397 &store::state_bytes(&state)?,
398 false,
399 )?;
400 let _visibility = self
401 .inner
402 .visibility
403 .write()
404 .map_err(|_| Error::corrupt("visibility lock is poisoned"))?;
405 self.finish_commit(commit)
406 }
407
408 fn project_transaction(
409 &self,
410 parsed: &ParsedTransaction,
411 ) -> Result<(BTreeMap<NodeId, NodeFile>, BTreeMap<NodeId, HistoryUpdate>)> {
412 let overlay = store::DagOverlay {
413 id: parsed.id,
414 parents: &parsed.unsigned.heads,
415 };
416 let current_creates = parsed
417 .unsigned
418 .creates
419 .iter()
420 .map(|operation| operation.id)
421 .collect::<BTreeSet<_>>();
422 let current_objects = parsed
423 .unsigned
424 .objects
425 .iter()
426 .map(|object| object.id)
427 .collect::<BTreeSet<_>>();
428 let current_creates =
429 self.resolvable_current_creates(parsed, current_creates, ¤t_objects, &overlay)?;
430 let mut nodes = BTreeMap::new();
431 let mut histories = BTreeMap::new();
432 for (created, operation) in parsed
433 .unsigned
434 .creates
435 .iter()
436 .map(|operation| (true, operation))
437 .chain(
438 parsed
439 .unsigned
440 .updates
441 .iter()
442 .map(|operation| (false, operation)),
443 )
444 {
445 let mut history = store::read_history_index_optional(&self.inner.root, operation.id)?
446 .unwrap_or_else(|| store::empty_history(operation.id));
447 let existing_node = store::read_node_optional(&self.inner.root, operation.id)?;
448 let references = self.references_resolve(
449 parsed.id,
450 operation.id,
451 &operation.data,
452 ¤t_creates,
453 ¤t_objects,
454 &overlay,
455 )?;
456 let effective = if created {
457 references && !self.has_ancestor_create(&history, parsed.id, &overlay)?
458 } else {
459 references && self.has_ancestor_create(&history, parsed.id, &overlay)?
460 };
461 let node = if effective {
462 Some(self.apply_candidate(parsed, operation, existing_node, &overlay)?)
463 } else {
464 existing_node
465 };
466 let existing_entry =
467 store::read_history_entry_optional(&self.inner.root, operation.id, parsed.id)?;
468 if existing_entry.is_some() {
469 return Err(Error::corrupt(
470 "new transaction collides with an existing history entry",
471 ));
472 }
473 let entry = store::history_entry(parsed, operation.data.clone(), created);
474 if effective && created {
475 history.creations.push(parsed.id);
476 history.creations.sort();
477 history.creations.dedup();
478 }
479 if let Some(node) = &node {
480 history.frontier = node
481 .frontier
482 .iter()
483 .map(|candidate| candidate.transaction)
484 .collect();
485 history.visible = Some(node.visible_transaction);
486 }
487 if let Some(node) = node {
488 nodes.insert(operation.id, node);
489 }
490 histories.insert(
491 operation.id,
492 HistoryUpdate {
493 index: history,
494 entry,
495 create_entry: true,
496 },
497 );
498 }
499 Ok((nodes, histories))
500 }
501
502 fn resolvable_current_creates(
503 &self,
504 parsed: &ParsedTransaction,
505 mut resolvable: BTreeSet<NodeId>,
506 current_objects: &BTreeSet<ObjectId>,
507 overlay: &store::DagOverlay<'_>,
508 ) -> Result<BTreeSet<NodeId>> {
509 loop {
510 let mut invalid = Vec::new();
511 for operation in &parsed.unsigned.creates {
512 if resolvable.contains(&operation.id)
513 && !self.references_resolve(
514 parsed.id,
515 operation.id,
516 &operation.data,
517 &resolvable,
518 current_objects,
519 overlay,
520 )?
521 {
522 invalid.push(operation.id);
523 }
524 }
525 if invalid.is_empty() {
526 return Ok(resolvable);
527 }
528 for id in invalid {
529 resolvable.remove(&id);
530 }
531 }
532 }
533
534 fn apply_candidate(
535 &self,
536 parsed: &ParsedTransaction,
537 operation: &NodeOperation,
538 existing: Option<NodeFile>,
539 overlay: &store::DagOverlay<'_>,
540 ) -> Result<NodeFile> {
541 let mut frontier = existing.map_or_else(Vec::new, |node| node.frontier);
542 for candidate in &frontier {
543 if store::is_ancestor(
544 &self.inner.root,
545 parsed.id,
546 candidate.transaction,
547 Some(overlay),
548 )? {
549 return preferred_node(operation.id, frontier, &self.inner.writers);
550 }
551 }
552 let mut retained = Vec::with_capacity(frontier.len() + 1);
553 for candidate in frontier.drain(..) {
554 if !store::is_ancestor(
555 &self.inner.root,
556 candidate.transaction,
557 parsed.id,
558 Some(overlay),
559 )? {
560 retained.push(candidate);
561 }
562 }
563 retained.push(Candidate {
564 transaction: parsed.id,
565 writer: parsed.unsigned.writer,
566 committed_at: parsed.unsigned.committed_at,
567 provenance: parsed.unsigned.provenance.clone(),
568 data: operation.data.clone(),
569 });
570 retained.sort_by_key(|candidate| candidate.transaction);
571 preferred_node(operation.id, retained, &self.inner.writers)
572 }
573
574 fn has_ancestor_create(
575 &self,
576 history: &HistoryIndex,
577 transaction: TransactionId,
578 overlay: &store::DagOverlay<'_>,
579 ) -> Result<bool> {
580 for creation in &history.creations {
581 if *creation != transaction
582 && store::is_ancestor(&self.inner.root, *creation, transaction, Some(overlay))?
583 {
584 return Ok(true);
585 }
586 }
587 Ok(false)
588 }
589
590 fn references_resolve(
591 &self,
592 transaction: TransactionId,
593 self_id: NodeId,
594 data: &NodeData,
595 current_creates: &BTreeSet<NodeId>,
596 current_objects: &BTreeSet<ObjectId>,
597 overlay: &store::DagOverlay<'_>,
598 ) -> Result<bool> {
599 let mut node_refs = data
600 .fixed_connections
601 .iter()
602 .chain(&data.recent_connections)
603 .copied()
604 .collect::<Vec<_>>();
605 if let Owner::Node(owner) = data.owner {
606 node_refs.push(owner);
607 }
608 for reference in node_refs {
609 if reference == self_id || current_creates.contains(&reference) {
610 continue;
611 }
612 let Some(history) = store::read_history_index_optional(&self.inner.root, reference)?
613 else {
614 return Ok(false);
615 };
616 if !self.has_ancestor_create(&history, transaction, overlay)? {
617 return Ok(false);
618 }
619 }
620 for object in &data.objects {
621 if current_objects.contains(object) {
622 continue;
623 }
624 let creator = match store::object_creator(&self.inner.root, *object) {
625 Ok(creator) => creator,
626 Err(Error::Io(error)) if error.kind() == std::io::ErrorKind::NotFound => {
627 return Ok(false);
628 }
629 Err(error) => return Err(error),
630 };
631 if !store::transaction_committed(&self.inner.root, creator)?
632 || !store::is_ancestor(&self.inner.root, creator, transaction, Some(overlay))?
633 {
634 return Ok(false);
635 }
636 }
637 Ok(true)
638 }
639
640 fn missing_heads(&self, parsed: &ParsedTransaction) -> Result<BTreeSet<TransactionId>> {
641 let mut missing = BTreeSet::new();
642 for head in &parsed.unsigned.heads {
643 if !store::transaction_committed(&self.inner.root, *head)? {
644 missing.insert(*head);
645 }
646 }
647 Ok(missing)
648 }
649
650 fn enqueue_pending(
651 &self,
652 parsed: ParsedTransaction,
653 package: TransactionPackage,
654 missing: BTreeSet<TransactionId>,
655 ) -> Result<Vec<TransactionId>> {
656 let TransactionPackage {
657 transaction,
658 objects,
659 } = package;
660 let objects = store::spool_objects(&self.inner.root, parsed.id, objects)?;
661 let mut pending = self
662 .inner
663 .pending
664 .lock()
665 .map_err(|_| Error::corrupt("pending mutex is poisoned"))?;
666 let requests = missing
667 .iter()
668 .filter(|head| !pending.transactions.contains_key(head))
669 .copied()
670 .collect::<Vec<_>>();
671 for head in &missing {
672 pending
673 .waiting_on
674 .entry(*head)
675 .or_default()
676 .insert(parsed.id);
677 }
678 for object in &objects {
679 pending.object_reservations.insert(object.id, parsed.id);
680 }
681 pending.transactions.insert(
682 parsed.id,
683 PendingTransaction {
684 id: parsed.id,
685 transaction,
686 objects,
687 missing,
688 },
689 );
690 Ok(requests)
691 }
692
693 fn ensure_object_ids_available(
694 &self,
695 transaction: TransactionId,
696 declarations: &[ObjectDeclaration],
697 ) -> Result<()> {
698 let pending = self
699 .inner
700 .pending
701 .lock()
702 .map_err(|_| Error::corrupt("pending mutex is poisoned"))?;
703 for declaration in declarations {
704 if store::object_exists(&self.inner.root, declaration.id)
705 || pending
706 .object_reservations
707 .get(&declaration.id)
708 .is_some_and(|owner| *owner != transaction)
709 {
710 return Err(Error::invalid_transaction(format!(
711 "object locator collision at {}",
712 declaration.id
713 )));
714 }
715 }
716 Ok(())
717 }
718
719 fn process_committed_transaction(&self, committed: TransactionId) -> Result<()> {
720 self.release_pending_dependents(committed)?;
721 loop {
722 let pending_transaction = {
723 let mut pending = self
724 .inner
725 .pending
726 .lock()
727 .map_err(|_| Error::corrupt("pending mutex is poisoned"))?;
728 let Some(id) = pending.ready.pop_front() else {
729 return Ok(());
730 };
731 pending.transactions.remove(&id).ok_or_else(|| {
732 Error::corrupt("ready transaction is missing from the pending pool")
733 })?
734 };
735 let parsed = match wire::parse_signed_authorized(
736 &pending_transaction.transaction,
737 &self.inner.writers,
738 ) {
739 Ok(parsed) => parsed,
740 Err(error) => {
741 self.requeue_pending(pending_transaction)?;
742 return Err(error);
743 }
744 };
745 let missing = match self.missing_heads(&parsed) {
746 Ok(missing) => missing,
747 Err(error) => {
748 self.requeue_pending(pending_transaction)?;
749 return Err(error);
750 }
751 };
752 if parsed.id != pending_transaction.id || !missing.is_empty() {
753 self.requeue_pending(pending_transaction)?;
754 return Err(Error::corrupt(
755 "ready transaction still has an unresolved dependency",
756 ));
757 }
758 let id = pending_transaction.id;
759 let result = self.commit_new(
760 parsed,
761 &pending_transaction.transaction,
762 CommitObjects::Spooled(&pending_transaction.objects),
763 );
764 if let Err(error) = result {
765 self.requeue_pending(pending_transaction)?;
766 return Err(error);
767 }
768 self.complete_pending(id)?;
769 let _ = store::discard_spooled_objects(&self.inner.root, id);
770 self.release_pending_dependents(id)?;
771 }
772 }
773
774 fn release_pending_dependents(&self, committed: TransactionId) -> Result<()> {
775 let mut pending = self
776 .inner
777 .pending
778 .lock()
779 .map_err(|_| Error::corrupt("pending mutex is poisoned"))?;
780 let Some(dependents) = pending.waiting_on.remove(&committed) else {
781 return Ok(());
782 };
783 for dependent in dependents {
784 let transaction = pending.transactions.get_mut(&dependent).ok_or_else(|| {
785 Error::corrupt("dependency waiter is missing from the pending pool")
786 })?;
787 transaction.missing.remove(&committed);
788 if transaction.missing.is_empty() {
789 pending.ready.push_back(dependent);
790 }
791 }
792 Ok(())
793 }
794
795 fn complete_pending(&self, id: TransactionId) -> Result<()> {
796 let mut pending = self
797 .inner
798 .pending
799 .lock()
800 .map_err(|_| Error::corrupt("pending mutex is poisoned"))?;
801 pending
802 .object_reservations
803 .retain(|_, transaction| *transaction != id);
804 Ok(())
805 }
806
807 fn requeue_pending(&self, transaction: PendingTransaction) -> Result<()> {
808 let mut pending = self
809 .inner
810 .pending
811 .lock()
812 .map_err(|_| Error::corrupt("pending mutex is poisoned"))?;
813 pending.ready.push_front(transaction.id);
814 pending.transactions.insert(transaction.id, transaction);
815 Ok(())
816 }
817
818 fn drain_one_outbox(&self) -> Result<()> {
819 if self
820 .inner
821 .outbox_draining
822 .compare_exchange(false, true, AtomicOrdering::AcqRel, AtomicOrdering::Acquire)
823 .is_err()
824 {
825 return Ok(());
826 }
827 let _drain_guard = AtomicFlagGuard(&self.inner.outbox_draining);
828 store::recover_outbox_claim(&self.inner.root)?;
829 let Some(id) = store::next_outbox(&self.inner.root)? else {
830 return Ok(());
831 };
832 store::claim_outbox(&self.inner.root, id)?;
833 let package = match store::package_for(&self.inner.root, id) {
834 Ok(package) => package,
835 Err(error) => {
836 store::requeue_outbox_claim(&self.inner.root, id)?;
837 return Err(error);
838 }
839 };
840 if self.inner.gossip.announce(package) {
841 store::acknowledge_outbox_claim(&self.inner.root)?;
842 } else {
843 store::requeue_outbox_claim(&self.inner.root, id)?;
844 }
845 Ok(())
846 }
847
848 fn finish_commit(&self, commit: store::Commit) -> Result<()> {
849 match commit.finish() {
850 Ok(()) => Ok(()),
851 Err(failure) => {
852 if failure.prepared {
853 self.inner.poisoned.store(true, AtomicOrdering::Release);
854 }
855 Err(failure.error)
856 }
857 }
858 }
859
860 fn ensure_healthy(&self) -> Result<()> {
861 if self.inner.poisoned.load(AtomicOrdering::Acquire) {
862 Err(Error::corrupt(
863 "a prepared WAL could not be fully applied; close and reopen the database",
864 ))
865 } else {
866 Ok(())
867 }
868 }
869}
870
871struct AtomicFlagGuard<'a>(&'a AtomicBool);
872
873impl Drop for AtomicFlagGuard<'_> {
874 fn drop(&mut self) {
875 self.0.store(false, AtomicOrdering::Release);
876 }
877}
878
879pub struct Transaction<'a> {
880 db: &'a KwebDb,
881 guard: Option<MutexGuard<'a, ()>>,
882 provenance: Provenance,
883 heads: Vec<TransactionId>,
884 object_bytes: u64,
885 objects: BTreeMap<ObjectId, Vec<u8>>,
886 reserved_nodes: BTreeSet<NodeId>,
887 creates: BTreeMap<NodeId, NodeData>,
888 updates: BTreeMap<NodeId, NodeData>,
889 merges: BTreeSet<MergePair>,
890}
891
892impl fmt::Debug for Transaction<'_> {
893 fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
894 formatter
895 .debug_struct("Transaction")
896 .field("heads", &self.heads)
897 .field("object_count", &self.objects.len())
898 .field("object_bytes", &self.object_bytes)
899 .field("reserved_nodes", &self.reserved_nodes.len())
900 .field("creates", &self.creates.len())
901 .field("updates", &self.updates.len())
902 .field("merges", &self.merges.len())
903 .finish_non_exhaustive()
904 }
905}
906
907impl Transaction<'_> {
908 pub fn create_object(&mut self, bytes: Vec<u8>) -> Result<ObjectId> {
909 let length = bytes.len() as u64;
910 if length > crate::MAX_OBJECT_BYTES {
911 return Err(Error::invalid_input("object exceeds the 32 GiB limit"));
912 }
913 let object_bytes = self
914 .object_bytes
915 .checked_add(length)
916 .ok_or_else(|| Error::invalid_input("transaction object length overflow"))?;
917 if object_bytes > crate::MAX_TRANSACTION_OBJECT_BYTES {
918 return Err(Error::invalid_input(
919 "transaction object payload total exceeds 32 GiB",
920 ));
921 }
922 let id = loop {
923 let candidate = ObjectId::random();
924 if !self.objects.contains_key(&candidate)
925 && !store::object_exists(&self.db.inner.root, candidate)
926 && !self
927 .db
928 .inner
929 .pending
930 .lock()
931 .map_err(|_| Error::corrupt("pending mutex is poisoned"))?
932 .object_reservations
933 .contains_key(&candidate)
934 {
935 break candidate;
936 }
937 };
938 self.object_bytes = object_bytes;
939 self.objects.insert(id, bytes);
940 Ok(id)
941 }
942
943 pub fn reserve_node_id(&mut self) -> Result<NodeId> {
944 let id = loop {
945 let candidate = NodeId::random();
946 if !self.reserved_nodes.contains(&candidate)
947 && !self.creates.contains_key(&candidate)
948 && !store::node_id_occupied(&self.db.inner.root, candidate)
949 {
950 break candidate;
951 }
952 };
953 self.reserved_nodes.insert(id);
954 Ok(id)
955 }
956
957 pub fn create_reserved_node(&mut self, id: NodeId, data: NodeData) -> Result<()> {
958 if self.creates.contains_key(&id) {
959 return Err(Error::invalid_input(format!(
960 "reserved node {id} has already been materialized"
961 )));
962 }
963 if !self.reserved_nodes.contains(&id) {
964 return Err(Error::invalid_input(format!(
965 "node ID {id} was not reserved by this transaction"
966 )));
967 }
968 data.validate()?;
969 self.reserved_nodes.remove(&id);
970 self.creates.insert(id, data);
971 Ok(())
972 }
973
974 pub fn create_node(&mut self, data: NodeData) -> Result<NodeId> {
975 data.validate()?;
976 let id = self.reserve_node_id()?;
977 self.create_reserved_node(id, data)?;
978 Ok(id)
979 }
980
981 pub fn update_node(&mut self, id: NodeId, data: NodeData) -> Result<()> {
982 if self.reserved_nodes.contains(&id) || self.creates.contains_key(&id) {
983 return Err(Error::invalid_input(
984 "one transaction cannot create and update the same node",
985 ));
986 }
987 data.validate()?;
988 if self.updates.insert(id, data).is_some() {
989 return Err(Error::invalid_input(
990 "one transaction cannot update the same node twice",
991 ));
992 }
993 Ok(())
994 }
995
996 pub fn merge(&mut self, first: TransactionId, second: TransactionId) -> Result<()> {
997 let pair = MergePair::new(first, second)?;
998 if !self.merges.insert(pair) {
999 return Err(Error::invalid_input("duplicate merge pair"));
1000 }
1001 Ok(())
1002 }
1003
1004 pub fn finalize(mut self) -> Result<TransactionId> {
1005 self.validate_local()?;
1006 let unsigned = UnsignedTransaction {
1007 writer: self.db.inner.local_writer,
1008 committed_at: Utc::now(),
1009 heads: self.heads.clone(),
1010 provenance: self.provenance.clone(),
1011 merge_pairs: self.merges.iter().copied().collect(),
1012 objects: self
1013 .objects
1014 .iter()
1015 .map(|(id, bytes)| ObjectDeclaration {
1016 id: *id,
1017 length: bytes.len() as u64,
1018 sha256: wire::object_hash(bytes),
1019 })
1020 .collect(),
1021 creates: self
1022 .creates
1023 .iter()
1024 .map(|(id, data)| NodeOperation {
1025 id: *id,
1026 data: data.clone(),
1027 })
1028 .collect(),
1029 updates: self
1030 .updates
1031 .iter()
1032 .map(|(id, data)| NodeOperation {
1033 id: *id,
1034 data: data.clone(),
1035 })
1036 .collect(),
1037 };
1038 let transaction = wire::build_signed(&unsigned, &self.db.inner.signing_key)?;
1039 let parsed = wire::parse_signed(&transaction)?;
1040 let id = parsed.id;
1041 let objects = self
1042 .objects
1043 .into_iter()
1044 .map(|(id, bytes)| ObjectPayload { id, bytes })
1045 .collect::<Vec<_>>();
1046 self.db
1047 .commit_new(parsed, &transaction, CommitObjects::Memory(&objects))?;
1048 drop(objects);
1049 self.db.process_committed_transaction(id)?;
1050 drop(self.guard.take());
1051 self.db.drain_one_outbox()?;
1052 Ok(id)
1053 }
1054
1055 fn validate_local(&self) -> Result<()> {
1056 if !self.reserved_nodes.is_empty() {
1057 return Err(Error::invalid_input(format!(
1058 "transaction has {} unmaterialized reserved node ID(s)",
1059 self.reserved_nodes.len()
1060 )));
1061 }
1062 let created = self.creates.keys().copied().collect::<BTreeSet<_>>();
1063 let objects = self.objects.keys().copied().collect::<BTreeSet<_>>();
1064 for (id, data) in self.creates.iter().chain(&self.updates) {
1065 if self.updates.contains_key(id) && !store::node_exists(&self.db.inner.root, *id) {
1066 return Err(Error::invalid_input(format!(
1067 "cannot update nonvisible node {id}"
1068 )));
1069 }
1070 validate_local_references(&self.db.inner.root, *id, data, &created, &objects)?;
1071 }
1072 for pair in &self.merges {
1073 let valid = self.updates.keys().any(|id| {
1074 store::read_node(&self.db.inner.root, *id)
1075 .map(|node| {
1076 let frontier = node
1077 .frontier
1078 .iter()
1079 .map(|candidate| candidate.transaction)
1080 .collect::<BTreeSet<_>>();
1081 frontier.contains(&pair.first) && frontier.contains(&pair.second)
1082 })
1083 .unwrap_or(false)
1084 });
1085 if !valid {
1086 return Err(Error::invalid_input(
1087 "merge pair is not an exact current frontier for an updated node",
1088 ));
1089 }
1090 }
1091 Ok(())
1092 }
1093}
1094
1095fn validate_local_references(
1096 root: &Path,
1097 self_id: NodeId,
1098 data: &NodeData,
1099 created: &BTreeSet<NodeId>,
1100 objects: &BTreeSet<ObjectId>,
1101) -> Result<()> {
1102 let mut node_refs = data
1103 .fixed_connections
1104 .iter()
1105 .chain(&data.recent_connections)
1106 .copied()
1107 .collect::<Vec<_>>();
1108 if let Owner::Node(owner) = data.owner {
1109 node_refs.push(owner);
1110 }
1111 for reference in node_refs {
1112 if reference != self_id
1113 && !created.contains(&reference)
1114 && !store::node_exists(root, reference)
1115 {
1116 return Err(Error::invalid_input(format!(
1117 "node reference {reference} is not locally resolvable"
1118 )));
1119 }
1120 }
1121 for object in &data.objects {
1122 if !objects.contains(object) && !store::object_exists(root, *object) {
1123 return Err(Error::invalid_input(format!(
1124 "object {object} is not locally resolvable"
1125 )));
1126 }
1127 }
1128 Ok(())
1129}
1130
1131fn preferred_node(id: NodeId, frontier: Vec<Candidate>, writers: &[WriterId]) -> Result<NodeFile> {
1132 let visible = frontier
1133 .iter()
1134 .max_by(|left, right| compare_preference(left, right, writers))
1135 .ok_or_else(|| Error::corrupt("node frontier is empty"))?;
1136 Ok(NodeFile {
1137 format: store::record_version(),
1138 id,
1139 node: Node {
1140 id,
1141 data: visible.data.clone(),
1142 last_author: visible.provenance.author.clone(),
1143 committed_at: visible.committed_at,
1144 },
1145 visible_transaction: visible.transaction,
1146 frontier,
1147 })
1148}
1149
1150fn compare_preference(left: &Candidate, right: &Candidate, writers: &[WriterId]) -> Ordering {
1151 let left_rank = writers
1152 .iter()
1153 .position(|writer| *writer == left.writer)
1154 .unwrap_or(usize::MAX);
1155 let right_rank = writers
1156 .iter()
1157 .position(|writer| *writer == right.writer)
1158 .unwrap_or(usize::MAX);
1159 right_rank
1160 .cmp(&left_rank)
1161 .then_with(|| left.transaction.cmp(&right.transaction))
1162}
1163
1164fn next_dag_generation(generations: Vec<u64>) -> Result<u64> {
1165 match generations.into_iter().max() {
1166 Some(generation) => generation
1167 .checked_add(1)
1168 .ok_or_else(|| Error::corrupt("transaction DAG generation overflow")),
1169 None => Ok(0),
1170 }
1171}
1172
1173fn sort_history(root: &Path, history: &mut NodeHistory) -> Result<()> {
1174 let mut generations = BTreeMap::new();
1175 for entry in &history.entries {
1176 let generation = store::read_tx_meta(root, entry.transaction_id)?.dag_generation;
1177 generations.insert(entry.transaction_id, generation);
1178 }
1179 history.entries.sort_by(|left, right| {
1180 (generations[&right.transaction_id], right.transaction_id)
1181 .cmp(&(generations[&left.transaction_id], left.transaction_id))
1182 });
1183 Ok(())
1184}
1185
1186fn stage_projection(
1187 commit: &mut store::Commit,
1188 nodes: BTreeMap<NodeId, NodeFile>,
1189 histories: BTreeMap<NodeId, HistoryUpdate>,
1190) -> Result<()> {
1191 for (id, node) in nodes {
1192 commit.stage_bytes(store::node_rel(id), &store::node_bytes(&node)?, false)?;
1193 }
1194 for (id, history) in histories {
1195 commit.stage_bytes(
1196 store::history_index_rel(id),
1197 &store::history_index_bytes(&history.index)?,
1198 false,
1199 )?;
1200 commit.stage_bytes(
1201 store::history_entry_rel(id, history.entry.transaction_id),
1202 &store::history_entry_bytes(&history.entry)?,
1203 history.create_entry,
1204 )?;
1205 }
1206 Ok(())
1207}
1208
1209struct HistoryUpdate {
1210 index: HistoryIndex,
1211 entry: crate::HistoryEntry,
1212 create_entry: bool,
1213}
1214
1215fn verify_package(parsed: &ParsedTransaction, package: &TransactionPackage) -> Result<()> {
1216 if parsed.unsigned.objects.len() != package.objects.len() {
1217 return Err(Error::invalid_transaction(
1218 "package does not contain exactly every declared object",
1219 ));
1220 }
1221 let mut total = 0_u64;
1222 for (declaration, payload) in parsed.unsigned.objects.iter().zip(&package.objects) {
1223 if declaration.id != payload.id {
1224 return Err(Error::invalid_transaction(
1225 "package object order or ID differs",
1226 ));
1227 }
1228 let length = payload.bytes.len() as u64;
1229 total = total
1230 .checked_add(length)
1231 .ok_or_else(|| Error::invalid_transaction("package object length overflow"))?;
1232 if length != declaration.length
1233 || length > crate::MAX_OBJECT_BYTES
1234 || total > crate::MAX_TRANSACTION_OBJECT_BYTES
1235 {
1236 return Err(Error::invalid_transaction(
1237 "package object length is invalid",
1238 ));
1239 }
1240 if wire::object_hash(&payload.bytes) != declaration.sha256 {
1241 return Err(Error::invalid_transaction(
1242 "package object SHA-256 mismatch",
1243 ));
1244 }
1245 }
1246 Ok(())
1247}
1248
1249fn validate_config(config: &Config) -> Result<()> {
1250 if config.writers_by_priority.is_empty() {
1251 return Err(Error::invalid_config("writers_by_priority cannot be empty"));
1252 }
1253 let writers = config
1254 .writers_by_priority
1255 .iter()
1256 .copied()
1257 .collect::<BTreeSet<_>>();
1258 if writers.len() != config.writers_by_priority.len() {
1259 return Err(Error::invalid_config("writers_by_priority must be unique"));
1260 }
1261 if config
1262 .writers_by_priority
1263 .iter()
1264 .any(|writer| ed25519_dalek::VerifyingKey::from_bytes(&writer.0).is_err())
1265 {
1266 return Err(Error::invalid_config(
1267 "writers_by_priority contains an invalid Ed25519 public key",
1268 ));
1269 }
1270 if !writers.contains(&WriterId::from_signing_key(&config.signing_key)) {
1271 return Err(Error::invalid_config(
1272 "writers_by_priority must contain the local writer",
1273 ));
1274 }
1275 Ok(())
1276}
1277
1278fn open_lock_file(path: &Path) -> Result<File> {
1279 let file = match fs::symlink_metadata(path) {
1280 Ok(metadata) => {
1281 if metadata.file_type().is_symlink() || !metadata.file_type().is_file() {
1282 return Err(Error::invalid_config(
1283 "database LOCK must be a regular file",
1284 ));
1285 }
1286 OpenOptions::new().read(true).write(true).open(path)?
1287 }
1288 Err(error) if error.kind() == std::io::ErrorKind::NotFound => OpenOptions::new()
1289 .read(true)
1290 .write(true)
1291 .create_new(true)
1292 .open(path)?,
1293 Err(error) => return Err(error.into()),
1294 };
1295 if !file.metadata()?.file_type().is_file() {
1296 return Err(Error::invalid_config(
1297 "database LOCK must be a regular file",
1298 ));
1299 }
1300 Ok(file)
1301}