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