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