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