Skip to main content

kcode_kweb_db/
db.rs

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, &current_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                &current_creates,
452                &current_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}