Skip to main content

datumesh_store/
lib.rs

1//! A PostgreSQL store for [Datumesh](https://datumesh.com): function descriptors, receipts,
2//! blobs, versions of cells over transaction time, lineage, namespaces and function names.
3//! [`PgStore`] implements [`Store`], and through it `datumesh_core`'s [`Resolver`], so
4//! `datumesh_core::verify` can recompute receipts of stored cells from it.
5//!
6//! The API is synchronous (the `postgres` driver, a small connection pool). Async callers run
7//! it on a blocking thread, e.g. `tokio::task::spawn_blocking`.
8//!
9//! [`PgStore::connect`] / [`PgStore::connect_with`] take a URL and [`PoolOptions`]; TLS
10//! (rustls) is used as the URL's `sslmode` says and always verifies the server's
11//! certificate and host name, against the Mozilla roots plus those added with
12//! [`PoolOptions::tls_root_certificates_pem`]. No type of the driver or of a TLS crate is
13//! part of the API, except in [`pg_unstable`] (any connection function, the driver
14//! itself), which is outside semver until 1.0.
15//!
16//! What is stored, and how:
17//!
18//! * Descriptors are checked by the core (`Descriptor::from_json`) and stored in canonical
19//!   form (JCS) under their hash. Receipts are verified (`datumesh_core::verify`, SPEC 10.1
20//!   steps 1-6) against the key document stored for their signer host, so nothing
21//!   unverified enters the store, and are stored canonical under their hash. Storing the
22//!   same thing twice stores it once. A second, different valid signature over the same
23//!   payload (possible only with a randomised signer: the payload names the key, so a
24//!   rotated key gives a new hash) is refused ([`Error::Conflict`]) and the stored receipt
25//!   kept.
26//! * Blobs are stored in the database, under the SHA-256 of their content.
27//! * Versions: from transaction time `tx` on, cell `cell_id` has the value of a receipt.
28//!   Append-only and in tx order per cell, enforced by the database (triggers) as well as by
29//!   [`Store::put_version`], which also checks that the receipt is stored and is of that
30//!   cell at that tx.
31//! * Computed versions: from transaction time `tx` on, cell `cell_id` has the value a
32//!   receipt computed, as of the receipt's asof. Derived rather than facts, so they are
33//!   added in any order, keyed on (cell, tx, asof), insert-only
34//!   ([`Store::put_computed`], [`Store::computed_asof`]).
35//! * Answering as of A: a receipt answers as of A only when its `tx` and its `asof` are
36//!   both at or before A ([`Store::stored_asof`], and the [`Resolver`]'s `evaluate` of a
37//!   stored function; [`Store::version_asof`] looks at `tx` only).
38//! * Lineage: `put_receipt` records an edge to each of the receipt's `inputs`, held or not;
39//!   [`Store::cell_dependents`] lifts it to cells.
40//! * Namespaces: a host's key document (checked as SPEC 9.2 requires), with when it was
41//!   validated. Store it before the host's receipts. Replacing it later (a key revoked or
42//!   dropped) does not touch receipts already stored: they verified when they were stored,
43//!   and putting one of them again is still idempotent.
44//! * Names: which function hash a fully qualified function name meant from a transaction
45//!   time on. Append-only, like versions. [`Store::resolve_binding`] gives the binding
46//!   answering as of A with when it began; [`Store::resolve_name`] its hash.
47//!
48//! Instants are compared as text (fixed width, `COLLATE "C"`), which is exact; each is also
49//! stored as `timestamptz`, which round-trips every instant of the specification exactly.
50//!
51//! Every [`Store`] method returns a `Result`: a malformed argument is [`Error::Invalid`] with
52//! the core's code, a database failure [`Error::Database`]. The [`Resolver`] methods keep the
53//! core's `Option` signatures; there a failure is logged with `tracing` (target
54//! `datumesh_store`) and answered `None`, which `verify` reports as unavailable.
55//!
56//! [`Store`] is sealed: only this crate implements it. The store is the trust boundary:
57//! everything in it was checked on the way in (every receipt verified against its host's
58//! key document), and its callers rely on that rather than checking again. A `dyn Store`
59//! is still a trait object as before.
60//!
61//! The schema comes from [`migrate::apply`] (the `datumesh-store migrate` command), through
62//! a store on the (possibly empty) database.
63
64#![forbid(unsafe_code)]
65#![deny(missing_docs)]
66
67use std::borrow::Cow;
68
69pub use datumesh_core::Resolver;
70
71mod error;
72pub mod migrate;
73mod pg;
74pub mod pg_unstable;
75mod pool;
76mod sql;
77mod tls;
78
79pub use error::{DatabaseError, Error, Result};
80pub use pg::PgStore;
81pub use pool::PoolOptions;
82
83/// A version of a cell: from transaction time `tx` on, the cell's value is that of the
84/// receipt `receipt_hash`, which states it as of `asof`.
85///
86/// A stored version ([`Store::put_version`]) or a computed one ([`Store::put_computed`]).
87#[non_exhaustive]
88#[derive(Clone, Debug, PartialEq, Eq, Hash)]
89pub struct Version {
90    /// The cell id.
91    pub cell_id: String,
92    /// The transaction time (an instant, SPEC 3.3).
93    pub tx: String,
94    /// The receipt stating the value.
95    pub receipt_hash: String,
96    /// The receipt's `asof` (an instant; never earlier than `tx`). Since 0.2.0.
97    pub asof: String,
98}
99
100/// A name binding: from transaction time `tx` on, a fully qualified function name means the
101/// function `fn_hash` ([`Store::bind_name`]). Since 0.3.0.
102#[non_exhaustive]
103#[derive(Clone, Debug, PartialEq, Eq, Hash)]
104pub struct Binding {
105    /// The function hash the name means.
106    pub fn_hash: String,
107    /// When the binding began: the transaction time from which the name means `fn_hash`
108    /// (an instant, SPEC 3.3).
109    pub tx: String,
110}
111
112/// A namespace's host and its key document.
113#[non_exhaustive]
114#[derive(Clone, Debug, PartialEq, Eq, Hash)]
115pub struct Namespace {
116    /// The host (a DNS name).
117    pub host: String,
118    /// The key document, canonical (JCS).
119    pub keydoc: Vec<u8>,
120    /// When the document was last retrieved and checked.
121    pub validated_at: String,
122    /// When the namespace was suspended, if it is. Nothing in this version sets it.
123    pub suspended_at: Option<String>,
124}
125
126/// A Datumesh store: a [`Resolver`] that can also be written to.
127///
128/// Instants are SPEC 3.3 instants (`YYYY-MM-DDTHH:MM:SS.ffffffZ`), hashes and cell ids 64
129/// lowercase hex digits, hosts DNS names and function names fully qualified. Every method,
130/// read or write, refuses anything else with the core's error code ([`Error::Invalid`]),
131/// and reports a database failure as [`Error::Database`] (or [`Error::PoolTimeout`]): a read
132/// answers `Ok(None)` / `Ok(vec![])` only when the store really holds nothing.
133///
134/// The [`Resolver`] methods keep the core's `Option` signatures: there a malformed argument
135/// or a database failure is `None` (logged with `tracing` when it is a failure), which
136/// `verify` reports as unavailable.
137///
138/// Only this crate implements `Store` (it is sealed, since 0.3.0): the store is the trust
139/// boundary, where every receipt is verified against its host's key document before it is
140/// kept, and what a store answers is relied on as checked. An implementation elsewhere
141/// could answer anything. `Store` stays object-safe (`Arc<dyn Store + Send + Sync>`). Even a complete implementation that
142/// delegates every method to a [`PgStore`] is refused (`E0277`: the private supertrait is
143/// not implemented; `tests/sealed.rs` checks that this is the only error):
144///
145/// ```compile_fail
146/// use std::borrow::Cow;
147/// use datumesh_core::{Cell, Scalar};
148/// use datumesh_store::{Binding, Namespace, PgStore, Resolver, Result, Store, Version};
149///
150/// struct Wrapper(PgStore);
151/// impl Resolver for Wrapper {
152///     fn descriptor(&self, h: &str) -> Option<Cow<'_, [u8]>> { self.0.descriptor(h) }
153///     fn receipt(&self, h: &str) -> Option<Cow<'_, [u8]>> { self.0.receipt(h) }
154///     fn evaluate(&self, c: &Cell, a: &str) -> Option<Scalar> { self.0.evaluate(c, a) }
155/// }
156/// impl Store for Wrapper {
157///     fn put_descriptor(&self, b: &[u8]) -> Result<String> { self.0.put_descriptor(b) }
158///     fn put_receipt(&self, b: &[u8]) -> Result<String> { self.0.put_receipt(b) }
159///     fn put_blob(&self, b: &[u8]) -> Result<String> { self.0.put_blob(b) }
160///     fn blob(&self, s: &str) -> Result<Option<Cow<'_, [u8]>>> { self.0.blob(s) }
161///     fn put_version(&self, c: &str, t: &str, r: &str) -> Result<()> { self.0.put_version(c, t, r) }
162///     fn version_asof(&self, c: &str, a: &str) -> Result<Option<Version>> { self.0.version_asof(c, a) }
163///     fn versions(&self, c: &str, f: &str, t: &str) -> Result<Vec<Version>> { self.0.versions(c, f, t) }
164///     fn latest(&self, c: &str) -> Result<Option<Version>> { self.0.latest(c) }
165///     fn dependents(&self, r: &str) -> Result<Vec<String>> { self.0.dependents(r) }
166///     fn inputs(&self, r: &str) -> Result<Vec<String>> { self.0.inputs(r) }
167///     fn put_namespace(&self, h: &str, k: &[u8], v: &str) -> Result<()> { self.0.put_namespace(h, k, v) }
168///     fn namespace(&self, h: &str) -> Result<Option<Namespace>> { self.0.namespace(h) }
169///     fn bind_name(&self, n: &str, f: &str, t: &str) -> Result<()> { self.0.bind_name(n, f, t) }
170///     fn resolve_name(&self, n: &str, a: &str) -> Result<Option<String>> { self.0.resolve_name(n, a) }
171///     fn resolve_binding(&self, n: &str, a: &str) -> Result<Option<Binding>> { self.0.resolve_binding(n, a) }
172///     fn put_computed(&self, c: &str, t: &str, r: &str) -> Result<()> { self.0.put_computed(c, t, r) }
173///     fn computed_asof(&self, c: &str, a: &str) -> Result<Option<Version>> { self.0.computed_asof(c, a) }
174///     fn stored_asof(&self, c: &str, a: &str) -> Result<Option<Version>> { self.0.stored_asof(c, a) }
175///     fn cell_dependents(&self, c: &str) -> Result<Vec<String>> { self.0.cell_dependents(c) }
176/// }
177/// ```
178pub trait Store: Resolver + sealed::Sealed {
179    /// Stores a descriptor (checked and canonicalised by the core); returns its function
180    /// hash. Idempotent.
181    fn put_descriptor(&self, bytes: &[u8]) -> Result<String>;
182    /// Stores a receipt that verifies (SPEC 10.1 steps 1-6) against the key document stored
183    /// for its signer host with [`put_namespace`](Store::put_namespace), in canonical form,
184    /// with an edge to each of its inputs; returns its hash. A receipt that does not verify
185    /// is refused with `verify`'s code (no key document for the host: `E_KEY_UNAVAILABLE`).
186    /// Idempotent: bytes whose canonical form is the stored copy return its hash without
187    /// checking the signature again (it verified when stored), even if the key has since
188    /// been revoked. Other bytes with a stored hash are verified first, then refused as
189    /// [`Error::Conflict`].
190    fn put_receipt(&self, bytes: &[u8]) -> Result<String>;
191    /// Stores blob content; returns its SHA-256 (lowercase hex). Idempotent.
192    fn put_blob(&self, bytes: &[u8]) -> Result<String>;
193    /// Blob content by SHA-256.
194    fn blob(&self, sha256: &str) -> Result<Option<Cow<'_, [u8]>>>;
195    /// Appends a version: from `tx` on, `cell_id` has the value of receipt `receipt_hash`,
196    /// which must be stored, of that cell and at that tx. `tx` must be later than the cell's
197    /// latest version ([`Error::NotLater`]).
198    fn put_version(&self, cell_id: &str, tx: &str, receipt_hash: &str) -> Result<()>;
199    /// The cell's latest version with `tx <= asof`.
200    fn version_asof(&self, cell_id: &str, asof: &str) -> Result<Option<Version>>;
201    /// The cell's versions with `from <= tx <= to`, in tx order.
202    fn versions(&self, cell_id: &str, from: &str, to: &str) -> Result<Vec<Version>>;
203    /// The cell's latest version.
204    fn latest(&self, cell_id: &str) -> Result<Option<Version>>;
205    /// The stored receipts that list `receipt_hash` among their inputs, by hash.
206    fn dependents(&self, receipt_hash: &str) -> Result<Vec<String>>;
207    /// The inputs of a stored receipt, by hash (whether or not they are stored).
208    fn inputs(&self, receipt_hash: &str) -> Result<Vec<String>>;
209    /// Records a host's key document, checked as a 200 response for that host would be
210    /// (SPEC 9.2, 9.3), validated at `validated_at`. Re-validation replaces the document; a
211    /// `validated_at` earlier than the recorded one is [`Error::NotLater`]. Receipts already
212    /// stored are not re-checked: they verified when they were stored (and putting one again
213    /// stays idempotent).
214    fn put_namespace(&self, host: &str, keydoc_bytes: &[u8], validated_at: &str) -> Result<()>;
215    /// A host's recorded key document.
216    fn namespace(&self, host: &str) -> Result<Option<Namespace>>;
217    /// Appends a binding: from `tx` on, the fully qualified function name `fn_name` means
218    /// `fn_hash`, whose descriptor must be stored. `tx` must be later than the name's latest
219    /// binding ([`Error::NotLater`]).
220    fn bind_name(&self, fn_name: &str, fn_hash: &str, tx: &str) -> Result<()>;
221    /// The function hash `fn_name` meant as of `asof`: the hash of
222    /// [`resolve_binding`](Store::resolve_binding)'s binding.
223    fn resolve_name(&self, fn_name: &str, asof: &str) -> Result<Option<String>>;
224    /// The binding of `fn_name` that answers as of `asof`: the one with the latest `tx <=
225    /// asof`, with its function hash and its `tx`, when it began; `None` before the first.
226    /// A name bound again to a hash it meant before (even the hash of the binding before
227    /// it) begins again: the later binding's `tx`. Since 0.3.0.
228    fn resolve_binding(&self, fn_name: &str, asof: &str) -> Result<Option<Binding>>;
229
230    /// Records that `receipt_hash` (stored, a receipt of `cell_id` at `tx`) is a computed
231    /// version of the cell. Insert-only, in any order (an older version may be added after a
232    /// newer one): `tx` need not be later than anything. The version's asof is the
233    /// receipt's. Idempotent for the same (cell_id, tx, asof, receipt_hash); another receipt
234    /// for an existing (cell_id, tx, asof) is [`Error::Conflict`] (its `hash` is the receipt
235    /// refused) and the stored one is kept. The receipt is checked as for
236    /// [`put_version`](Store::put_version): [`Error::NotFound`] when it is not stored,
237    /// [`Error::Mismatch`] when it is of another cell or at another tx. Since 0.2.0.
238    fn put_computed(&self, cell_id: &str, tx: &str, receipt_hash: &str) -> Result<()>;
239    /// The computed version that answers as of `asof`: the one with the greatest asof <=
240    /// `asof` (between equal asof, the greatest tx), or `None`. Since 0.2.0.
241    fn computed_asof(&self, cell_id: &str, asof: &str) -> Result<Option<Version>>;
242    /// The stored version that answers as of `asof`: the latest version (by tx) with `tx <=
243    /// asof` whose receipt's asof is also `<= asof`, or `None`. A version whose receipt is
244    /// late-signed (asof later than tx) answers only from its asof on; until then the
245    /// version before it answers. ([`version_asof`](Store::version_asof) keeps its tx-only
246    /// meaning.) Since 0.2.0.
247    fn stored_asof(&self, cell_id: &str, asof: &str) -> Result<Option<Version>>;
248    /// The cells that read `cell_id`: the cell ids of the stored receipts that list, among
249    /// their inputs, any stored receipt of `cell_id` (lineage: edges joined to receipts).
250    /// Ascending, each once. Since 0.2.0.
251    fn cell_dependents(&self, cell_id: &str) -> Result<Vec<String>>;
252
253    /// [`computed_asof`](Store::computed_asof) and the bytes of the version's receipt (as
254    /// [`receipt`](Resolver::receipt) returns them), which is what a caller reads next.
255    /// The default asks for the two in turn (a receipt that cannot be read then is
256    /// [`Error::NotFound`]); [`PgStore`] answers both with one query. Since 0.2.0.
257    fn computed_asof_with_receipt(&self, cell_id: &str, asof: &str) -> Result<Option<(Version, Cow<'_, [u8]>)>> {
258        match self.computed_asof(cell_id, asof)? {
259            Some(v) => with_receipt(self, v).map(Some),
260            None => Ok(None),
261        }
262    }
263    /// [`stored_asof`](Store::stored_asof) and the bytes of the version's receipt, as
264    /// [`computed_asof_with_receipt`](Store::computed_asof_with_receipt) does. Since 0.2.0.
265    fn stored_asof_with_receipt(&self, cell_id: &str, asof: &str) -> Result<Option<(Version, Cow<'_, [u8]>)>> {
266        match self.stored_asof(cell_id, asof)? {
267            Some(v) => with_receipt(self, v).map(Some),
268            None => Ok(None),
269        }
270    }
271}
272
273/// The seal of [`Store`]: a public trait in a private module, so no other crate can name it,
274/// and so none can implement `Store`.
275mod sealed {
276    pub trait Sealed {}
277    impl Sealed for crate::PgStore {}
278}
279
280/// A version and its receipt's bytes, for the default `*_with_receipt` methods.
281fn with_receipt<S: Store + ?Sized>(store: &S, v: Version) -> Result<(Version, Cow<'_, [u8]>)> {
282    match store.receipt(&v.receipt_hash) {
283        Some(bytes) => Ok((v, bytes)),
284        None => Err(Error::NotFound { what: "receipt", key: v.receipt_hash }),
285    }
286}