Skip to main content

datumesh_core/
verify.rs

1//! The verification procedure (10.1).
2
3use std::borrow::Cow;
4use std::collections::BTreeMap;
5
6use crate::crypto;
7use crate::descriptor::FunctionDescriptor;
8use crate::error::{err, member, Error, ErrorCode, Result};
9use crate::json::{self, Value};
10use crate::keys::Keys;
11use crate::lexical;
12use crate::model::{Cell, Scalar};
13use crate::receipt::Receipt;
14use crate::slice::{nest, Manifest};
15
16/// How far a receipt verified.
17#[derive(Clone, Copy, Debug, PartialEq, Eq, PartialOrd, Ord, Hash)]
18pub enum Level {
19    /// Steps 1-6 passed: the holder of `computed_by` stated this value.
20    Signed,
21    /// Steps 7.1-7.6 passed as well.
22    Recomputed,
23}
24
25impl Level {
26    /// `"signed"` or `"recomputed"`.
27    pub const fn as_str(self) -> &'static str {
28        match self {
29            Level::Signed => "signed",
30            Level::Recomputed => "recomputed",
31        }
32    }
33}
34
35/// What step 7 of SPEC 10.1 obtains: the descriptor, the input receipts (slice members
36/// included), the manifests of slice entries, and the evaluation. This crate evaluates no
37/// functions and fetches nothing; the caller supplies a resolver, and [`verify`] asks it only
38/// for what step 7 reaches for the receipt at hand:
39///
40/// * [`descriptor`](Resolver::descriptor) once, for `cell.fn_hash` (7.1);
41/// * for each entry of `inputs`, in order, stopping at the first that fails (7.5):
42///   [`receipt`](Resolver::receipt) once for a receipt hash; for a slice entry
43///   `slice:<slice id>:<members hash>` (`spec-0.4.0`), [`manifest`](Resolver::manifest) once,
44///   then `receipt` once per member, in the manifest's order;
45/// * [`evaluation`](Resolver::evaluation) once, only after 7.1-7.5 pass (7.6);
46/// * when the value differs and the evaluation came with a lineage, `receipt` for the
47///   lineage's receipt hashes that `inputs` does not list, in lineage order, until one shows
48///   a replaced input (`E_INPUT_SUPERSEDED`, 7.6).
49///
50/// Nothing is asked when steps 1-6 fail, and nothing about receipts below the inputs. The
51/// descriptor, receipts and manifests are JSON text, so that 7.1 hashes the descriptor before
52/// checking it and 7.5 reports an unparseable input as `E_INPUT_INVALID`, as the
53/// specification orders.
54///
55/// `manifest` and `evaluation` are provided (0.3.4): a resolver written for 0.3.3 offers no
56/// manifests (a slice entry is `E_INPUT_UNAVAILABLE`) and evaluates through
57/// [`evaluate`](Resolver::evaluate), with no lineage.
58pub trait Resolver {
59    /// The descriptor JSON text offered for `fn_hash`, or `None` if unavailable
60    /// (`E_FN_UNAVAILABLE`). Only `None` is unavailability: the text is parsed strictly
61    /// (unparseable text reports its parse code), and any other JSON value, `null` included,
62    /// is a descriptor offered that fails the hash check (`E_FN_HASH_MISMATCH`, SPEC 10.1
63    /// step 7.1).
64    fn descriptor(&self, fn_hash: &str) -> Option<Cow<'_, [u8]>>;
65
66    /// The receipt JSON text offered for receipt hash `hash`, or `None` if unavailable
67    /// (`E_INPUT_UNAVAILABLE`).
68    fn receipt(&self, hash: &str) -> Option<Cow<'_, [u8]>>;
69
70    /// The value of evaluating `cell` as of `asof` (for `stored`: the value looked up as of it,
71    /// SPEC 10.1 step 7.6), or `None` if this resolver cannot evaluate it: no runtime for the
72    /// language, a budget exceeded, inputs the evaluator cannot obtain
73    /// (`E_EVAL_UNAVAILABLE`, see [`VerifyResult`]). `asof` is the receipt's `asof`.
74    fn evaluate(&self, cell: &Cell, asof: &str) -> Option<Scalar>;
75
76    /// The manifest JSON text offered for slice entry `entry` (`slice:<slice id>:<members
77    /// hash>`), or `None` if unavailable (`E_INPUT_UNAVAILABLE`). The text is parsed strictly
78    /// and checked as a [`Manifest`]; one that does not hash to the entry is
79    /// `E_INPUT_UNAVAILABLE`, as a receipt with another hash. Provided: `None`.
80    fn manifest(&self, entry: &str) -> Option<Cow<'_, [u8]>> {
81        let _ = entry;
82        None
83    }
84
85    /// Evaluates `cell` as of `asof`, with the evaluation's lineage when the resolver has it:
86    /// the `inputs` entries the evaluation read (10.1 step 7.6). [`verify`] calls this once,
87    /// in place of [`evaluate`](Resolver::evaluate). When the value differs from the receipt's,
88    /// a lineage that replaces one of the receipt's inputs by a later version of the same cell
89    /// (whose receipt it offers through [`receipt`](Resolver::receipt)), or one of its slice
90    /// entries by an item beginning `slice:<the same slice id>:`, makes the result
91    /// `E_INPUT_SUPERSEDED` instead of `E_VALUE_MISMATCH`. Provided: `evaluate`'s value, with
92    /// no lineage.
93    fn evaluation(&self, cell: &Cell, asof: &str) -> Option<Evaluation> {
94        self.evaluate(cell, asof).map(Evaluation::new)
95    }
96}
97
98/// What evaluating a cell gave ([`Resolver::evaluation`]): the value and, when the resolver
99/// has it, the lineage: the `inputs` entries the evaluation read (receipt hashes and slice
100/// entries), whose receipts and manifests the resolver offers for those the receipt under
101/// verification does not list.
102#[derive(Clone, Debug, PartialEq, Eq, Hash)]
103#[non_exhaustive]
104pub struct Evaluation {
105    /// The evaluated value.
106    pub value: Scalar,
107    /// The entries the evaluation read, or `None` when the resolver does not say.
108    pub lineage: Option<Vec<String>>,
109}
110
111impl Evaluation {
112    /// A value without a lineage.
113    pub fn new(value: Scalar) -> Evaluation {
114        Evaluation { value, lineage: None }
115    }
116
117    /// A value with the entries the evaluation read.
118    pub fn with_lineage(value: Scalar, lineage: Vec<String>) -> Evaluation {
119        Evaluation { value, lineage: Some(lineage) }
120    }
121}
122
123/// A [`Resolver`] backed by maps: descriptors by function hash, receipts by receipt hash,
124/// manifests by slice entry, and evaluated values (with lineages) by cell id. It is what the
125/// vectors' `recompute` object describes (SPEC 11): the descriptor goes under the receipt's
126/// `cell.fn_hash`, the inputs under their hashes, the manifests under their entries, and
127/// `value` (and `lineage`) under the receipt's own [`Cell::id`].
128///
129/// A map holds one value per cell: [`evaluate`](Resolver::evaluate) ignores `asof`. A cell
130/// with no value inserted cannot be evaluated (`E_EVAL_UNAVAILABLE`), as a `recompute`
131/// without `value` means. A vector's `descriptor` is a JSON value: insert its JCS bytes
132/// (`null` as `null`, a string as its JSON serialisation); only an absent one is left out.
133#[derive(Clone, Debug, Default, PartialEq, Eq)]
134pub struct MapResolver {
135    descriptors: BTreeMap<String, Vec<u8>>,
136    receipts: BTreeMap<String, Vec<u8>>,
137    values: BTreeMap<String, Scalar>,
138    manifests: BTreeMap<String, Vec<u8>>,
139    lineages: BTreeMap<String, Vec<String>>,
140}
141
142impl MapResolver {
143    /// An empty resolver: every descriptor and receipt unavailable, nothing evaluable.
144    pub fn new() -> MapResolver {
145        MapResolver::default()
146    }
147
148    /// Offers descriptor JSON text for `fn_hash`.
149    pub fn insert_descriptor(&mut self, fn_hash: impl Into<String>, json: impl Into<Vec<u8>>) {
150        self.descriptors.insert(fn_hash.into(), json.into());
151    }
152
153    /// Offers receipt JSON text for receipt hash `hash`.
154    pub fn insert_receipt(&mut self, hash: impl Into<String>, json: impl Into<Vec<u8>>) {
155        self.receipts.insert(hash.into(), json.into());
156    }
157
158    /// Sets the value evaluating the cell with id `cell_id` gives.
159    pub fn insert_value(&mut self, cell_id: impl Into<String>, value: Scalar) {
160        self.values.insert(cell_id.into(), value);
161    }
162
163    /// Offers manifest JSON text for slice entry `entry`.
164    pub fn insert_manifest(&mut self, entry: impl Into<String>, json: impl Into<Vec<u8>>) {
165        self.manifests.insert(entry.into(), json.into());
166    }
167
168    /// Sets the lineage the evaluation of the cell with id `cell_id` reports with its value
169    /// (the `inputs` entries it read); without one, an evaluation has no lineage.
170    pub fn insert_lineage<I, S>(&mut self, cell_id: impl Into<String>, lineage: I)
171    where
172        I: IntoIterator<Item = S>,
173        S: Into<String>,
174    {
175        self.lineages.insert(cell_id.into(), lineage.into_iter().map(Into::into).collect());
176    }
177}
178
179impl Resolver for MapResolver {
180    fn descriptor(&self, fn_hash: &str) -> Option<Cow<'_, [u8]>> {
181        self.descriptors.get(fn_hash).map(|b| Cow::Borrowed(b.as_slice()))
182    }
183
184    fn receipt(&self, hash: &str) -> Option<Cow<'_, [u8]>> {
185        self.receipts.get(hash).map(|b| Cow::Borrowed(b.as_slice()))
186    }
187
188    fn evaluate(&self, cell: &Cell, _asof: &str) -> Option<Scalar> {
189        self.values.get(&cell.id()).cloned()
190    }
191
192    fn manifest(&self, entry: &str) -> Option<Cow<'_, [u8]>> {
193        self.manifests.get(entry).map(|b| Cow::Borrowed(b.as_slice()))
194    }
195
196    fn evaluation(&self, cell: &Cell, _asof: &str) -> Option<Evaluation> {
197        let id = cell.id();
198        let value = self.values.get(&id)?.clone();
199        Some(match self.lineages.get(&id) {
200            Some(l) => Evaluation::with_lineage(value, l.clone()),
201            None => Evaluation::new(value),
202        })
203    }
204}
205
206/// The outcome of [`verify`].
207///
208/// `code` is `None` when `ok`, with two exceptions, the codes of SPEC 10.2 that are not
209/// failures (10.1 step 7.6):
210///
211/// * [`ErrorCode::EvalUnavailable`]: a resolver is given and 7.1-7.5 pass but
212///   [`Resolver::evaluation`] returns `None`. The receipt is still signature-valid, so the
213///   result is `ok`, level [`Level::Signed`], with `code: Some(EvalUnavailable)`, `path`
214///   `$.value` and a `detail`: recomputation did not happen, and the code says why.
215/// * [`ErrorCode::InputSuperseded`] (`spec-0.4.0`): the evaluated value differs, and the
216///   evaluation's lineage shows that an input was replaced by a later version of its cell,
217///   or a slice by other members: `ok`, level `Signed`, `path` `$.inputs`. The receipt is
218///   historical, not wrong.
219///
220/// Step 7.7 still runs afterwards: if it passes, `blob_checked` is true and the code is kept;
221/// if it fails, its failure replaces the code as a normal failure (`ok` false, its own code).
222#[derive(Clone, Debug, PartialEq, Eq)]
223pub struct VerifyResult {
224    /// Whether the receipt verified.
225    pub ok: bool,
226    /// The failure's code; `None` when `ok`, except [`ErrorCode::EvalUnavailable`] and
227    /// [`ErrorCode::InputSuperseded`].
228    pub code: Option<ErrorCode>,
229    /// The level reached; `None` unless `ok`.
230    pub level: Option<Level>,
231    /// The receipt hash, once the receipt was canonical enough to compute it.
232    pub hash: Option<String>,
233    /// The receipt's `computed_by` (`<host>#<key id>`), once steps 1-4 passed.
234    pub computed_by: Option<String>,
235    /// Whether step 7.7 ran and passed.
236    pub blob_checked: bool,
237    /// A human explanation of the failure (or of a code reported with success); empty
238    /// otherwise.
239    pub detail: String,
240    /// Where the failure (or a code reported with success) was found; empty otherwise.
241    pub path: String,
242}
243
244impl VerifyResult {
245    /// `code` as an [`Error`], with `path` and `detail`: the failure, or a code reported with
246    /// success.
247    pub fn error(&self) -> Option<Error> {
248        self.code.map(|c| Error::new(c, self.path.clone(), self.detail.clone()))
249    }
250}
251
252/// Verifies a receipt (10.1). Never touches the network and never panics.
253///
254/// * `receipt`: the receipt's JSON text.
255/// * `keys`: the keys resolved for signer hosts (absent hosts are `E_KEY_UNAVAILABLE`).
256/// * `resolver`: when given, steps 7.1-7.6 run, asking it only for what they reach (see
257///   [`Resolver`]); success is level `recomputed`, an evaluation the resolver cannot do is
258///   `E_EVAL_UNAVAILABLE` at level `signed`, and a different value whose lineage shows a
259///   replaced input is `E_INPUT_SUPERSEDED` at level `signed` (see [`VerifyResult`]).
260/// * `blob`: content for step 7.7 (the value must be a blob); success sets `blob_checked`.
261pub fn verify(receipt: &[u8], keys: &Keys, resolver: Option<&dyn Resolver>, blob: Option<&[u8]>) -> VerifyResult {
262    let mut out = VerifyResult {
263        ok: false,
264        code: None,
265        level: None,
266        hash: None,
267        computed_by: None,
268        blob_checked: false,
269        detail: String::new(),
270        path: String::new(),
271    };
272    let r = match signature_valid(receipt, keys) {
273        Ok(r) => r,
274        Err((e, r)) => {
275            if let Some(r) = r {
276                out.hash = Some(r.hash().to_string());
277                out.computed_by = Some(r.computed_by().to_string());
278            }
279            return fail(out, e);
280        }
281    };
282    out.hash = Some(r.hash().to_string());
283    out.computed_by = Some(r.computed_by().to_string());
284    let mut level = Level::Signed;
285    if let Some(res) = resolver {
286        match recompute_steps(&r, keys, res) {
287            Ok(Recomputed::Yes) => level = Level::Recomputed,
288            Ok(Recomputed::Signed(e)) => out = fail(out, e),
289            Err(e) => return fail(out, e),
290        }
291    }
292    if let Some(content) = blob {
293        if let Err(e) = check_blob(&r, content) {
294            return fail(out, e);
295        }
296        out.blob_checked = true;
297    }
298    out.ok = true;
299    out.level = Some(level);
300    out
301}
302
303fn fail(mut out: VerifyResult, e: Error) -> VerifyResult {
304    out.code = Some(e.code());
305    out.detail = e.detail().to_string();
306    out.path = e.path().to_string();
307    out
308}
309
310/// A failure in steps 1-6, with the receipt when steps 1-4 passed.
311type StepError = (Error, Option<Box<Receipt>>);
312
313/// Steps 1-6 over JSON text.
314fn signature_valid(bytes: &[u8], keys: &Keys) -> core::result::Result<Receipt, StepError> {
315    let v = json::parse(bytes).map_err(|e| (e, None))?;
316    signature_valid_value(&v, keys)
317}
318
319/// Steps 2-6 over a parsed value (also used for receipts inside envelopes).
320pub(crate) fn signature_valid_value(v: &Value, keys: &Keys) -> core::result::Result<Receipt, StepError> {
321    let r = Receipt::from_value(v).map_err(|e| (e, None))?;
322    match key_and_signature(&r, keys) {
323        Ok(()) => Ok(r),
324        Err(e) => Err((e, Some(Box::new(r)))),
325    }
326}
327
328fn key_and_signature(r: &Receipt, keys: &Keys) -> Result<()> {
329    let doc = keys.get(r.signer_host()).map_err(|e| Error::new(e.code(), "$.computed_by", e.detail()))?;
330    let Some(key) = doc.key(r.key_id()) else {
331        return err(
332            ErrorCode::KeyUnknown,
333            "$.computed_by",
334            format!("{} publishes no key {}", r.signer_host(), r.key_id()),
335        );
336    };
337    if !key.usable_at(r.issued()) {
338        return err(ErrorCode::KeyNotValid, "$.issued", "issued is outside the key's validity window");
339    }
340    let Some(sig) = lexical::decode_hex_lower::<64>(r.sig()) else {
341        return err(ErrorCode::Signature, "$.sig", "signature is not 64 bytes");
342    };
343    if !crypto::verify_ed25519(&key.public, r.payload(), &sig) {
344        return err(ErrorCode::Signature, "$.sig", "Ed25519 verification failed");
345    }
346    Ok(())
347}
348
349/// How steps 7.1-7.6 ended when none failed.
350enum Recomputed {
351    /// The evaluated value equals the receipt's.
352    Yes,
353    /// Signed with a code: the resolver could not evaluate the function
354    /// (`E_EVAL_UNAVAILABLE`), or an input was superseded (`E_INPUT_SUPERSEDED`).
355    Signed(Error),
356}
357
358/// What 7.5 learnt about the receipt's inputs, for 7.6: each plain input's cell id and `tx`.
359struct Inputs {
360    cells: BTreeMap<String, (String, String)>,
361}
362
363fn recompute_steps(r: &Receipt, keys: &Keys, res: &dyn Resolver) -> Result<Recomputed> {
364    // 7.1
365    let Some(desc_bytes) = res.descriptor(&r.cell().fn_hash) else {
366        return err(ErrorCode::FnUnavailable, "$.cell.fn_hash", "no descriptor for fn_hash");
367    };
368    let desc_v = json::parse(&desc_bytes)?;
369    if crypto::sha256_hex(&desc_v.to_jcs()) != r.cell().fn_hash {
370        return err(ErrorCode::FnHashMismatch, "$.cell.fn_hash", "the descriptor does not hash to fn_hash");
371    }
372    let d = FunctionDescriptor::from_value(&desc_v)?;
373    let decl = &d.signature.args;
374    // 7.2: unknown, then missing, then types, each in JCS order of names.
375    for k in r.cell().args.keys() {
376        if !decl.contains_key(k) {
377            return err(ErrorCode::ArgUnknown, &member("$.cell.args", k), "the function declares no such argument");
378        }
379    }
380    for k in decl.keys() {
381        if !r.cell().args.contains_key(k) {
382            return err(ErrorCode::ArgMissing, &member("$.cell.args", k), "a declared argument is not bound");
383        }
384    }
385    for (k, a) in &r.cell().args {
386        let declared = decl.get(k).map(|x| x.ty);
387        if a.scalar().map(Scalar::ty) != declared {
388            return err(ErrorCode::ArgType, &member("$.cell.args", k), "type differs from the declaration");
389        }
390    }
391    // 7.3
392    let declared_valid: Vec<&String> = decl.iter().filter(|(_, x)| x.valid).map(|(k, _)| k).collect();
393    let given_valid: Vec<&String> = r.valid().keys().collect();
394    if declared_valid != given_valid {
395        return err(
396            ErrorCode::ValidMismatch,
397            "$.valid",
398            "valid does not hold exactly the declared valid-time arguments",
399        );
400    }
401    // 7.4
402    if r.ty() != d.signature.returns.ty {
403        return err(ErrorCode::ReturnType, "$.type", "type differs from the declared return type");
404    }
405    // 7.5
406    let mut latest_tx: Option<String> = None;
407    let mut seen = Inputs { cells: BTreeMap::new() };
408    for e in r.inputs() {
409        let p = member("$.inputs", e);
410        if lexical::split_slice_entry(e).is_some() {
411            slice_entry(r, keys, res, &d, e, &p, &mut latest_tx)?;
412            continue;
413        }
414        let input = obtain(keys, res, e, &p, "input")?;
415        if !d.inputs.contains(&input.cell().function_name()) {
416            return err(
417                ErrorCode::InputUndeclared,
418                &p,
419                format!("{} is not a declared input", input.cell().function_name()),
420            );
421        }
422        input_asof(r, &input, &p)?;
423        later_tx(&mut latest_tx, input.tx());
424        seen.cells.insert(e.clone(), (input.cell().id(), input.tx().to_string()));
425    }
426    if let Some(t) = latest_tx {
427        if r.tx() != t {
428            return err(ErrorCode::TimeOrder, "$.tx", "tx must equal the latest input tx");
429        }
430    }
431    // 7.6
432    let Some(ev) = res.evaluation(r.cell(), r.asof()) else {
433        return Ok(Recomputed::Signed(Error::new(
434            ErrorCode::EvalUnavailable,
435            "$.value",
436            "the resolver could not evaluate this function",
437        )));
438    };
439    if &ev.value == r.value() {
440        return Ok(Recomputed::Yes);
441    }
442    if let Some(lineage) = &ev.lineage {
443        if let Some(detail) = superseded(r, keys, res, &seen, lineage) {
444            return Ok(Recomputed::Signed(Error::new(ErrorCode::InputSuperseded, "$.inputs", detail)));
445        }
446    }
447    err(ErrorCode::ValueMismatch, "$.value", "recomputation gives a different type or value")
448}
449
450fn later_tx(latest: &mut Option<String>, tx: &str) {
451    if latest.as_ref().is_none_or(|t| tx > t.as_str()) {
452        *latest = Some(tx.to_string());
453    }
454}
455
456/// An input receipt (or slice member) obtained for `hash`: available (`E_INPUT_UNAVAILABLE`),
457/// signature-valid (`E_INPUT_INVALID`), of that hash (`E_INPUT_UNAVAILABLE`). Its `asof` is
458/// checked by `input_asof` after the caller's own checks.
459fn obtain(keys: &Keys, res: &dyn Resolver, hash: &str, p: &str, what: &str) -> Result<Receipt> {
460    let Some(bytes) = res.receipt(hash) else {
461        return err(ErrorCode::InputUnavailable, p, format!("no receipt offered for this {what}"));
462    };
463    let input = match signature_valid(&bytes, keys) {
464        Ok(i) => i,
465        Err((e, _)) => return err(ErrorCode::InputInvalid, p, format!("{}: {}", e.code(), e.detail())),
466    };
467    if input.hash() != hash {
468        return err(ErrorCode::InputUnavailable, p, format!("the receipt offered for this {what} has another hash"));
469    }
470    Ok(input)
471}
472
473/// D17 (revised in spec-0.3.0): an input's `asof` at or before the receipt's; instants are
474/// canonical, so text order is time order.
475fn input_asof(r: &Receipt, input: &Receipt, p: &str) -> Result<()> {
476    if input.asof() > r.asof() {
477        return err(ErrorCode::TimeOrder, p, "an input's asof is after the receipt's asof");
478    }
479    Ok(())
480}
481
482/// 10.1 step 7.5 for a slice entry (`spec-0.4.0`): the manifest (available, strict JSON,
483/// structure, canonical form, hashing to the entry), the slice's function declared, then each
484/// member: obtained, signature-valid, of its hash, in the slice, no second member of one
485/// cell in a membership slice, `asof` at or before the receipt's.
486fn slice_entry(
487    r: &Receipt,
488    keys: &Keys,
489    res: &dyn Resolver,
490    d: &FunctionDescriptor,
491    entry: &str,
492    p: &str,
493    latest_tx: &mut Option<String>,
494) -> Result<()> {
495    let Some((slice_id, members_hash)) = lexical::split_slice_entry(entry) else {
496        return err(ErrorCode::CanonHex, p, "not a slice entry");
497    };
498    let Some(bytes) = res.manifest(entry) else {
499        return err(ErrorCode::InputUnavailable, p, "no manifest offered for this slice entry");
500    };
501    let m = Manifest::from_json(&bytes).map_err(|e| nest(e, p))?;
502    if m.slice().id() != slice_id || m.members_hash() != members_hash {
503        return err(ErrorCode::InputUnavailable, p, "the manifest offered does not hash to the entry");
504    }
505    if !d.inputs.contains(&m.slice().function_name()) {
506        return err(
507            ErrorCode::InputUndeclared,
508            p,
509            format!("the slice's function {} is not a declared input", m.slice().function_name()),
510        );
511    }
512    let mut cells = std::collections::BTreeSet::new();
513    for h in m.members() {
514        let mp = format!("{p}.members.{h}");
515        let member = obtain(keys, res, h, &mp, "member")?;
516        if !m.slice().contains(&member) {
517            return err(ErrorCode::SliceMember, &mp, "the member does not belong to the slice");
518        }
519        if !m.slice().is_version_slice() && !cells.insert(member.cell().id()) {
520            return err(ErrorCode::SliceMember, &mp, "two members of the slice are one cell");
521        }
522        input_asof(r, &member, &mp)?;
523        later_tx(latest_tx, member.tx());
524    }
525    Ok(())
526}
527
528/// 10.1 step 7.6 (`spec-0.4.0`): whether the lineage replaces an entry `e` of the receipt's
529/// `inputs`: the lineage does not list `e` and lists an item `n` that `inputs` does not, where
530/// for a receipt hash `e`, a receipt offered for `n` is signature-valid, hashes to `n`, and is
531/// of the same cell id as `e`'s receipt with a strictly later `tx` (not compared with the
532/// receipt's own); for a slice entry `e`, `n` begins `slice:<e's slice id>:`. The lineage is a
533/// set; other items replace nothing. Returns a detail when it does.
534fn superseded(r: &Receipt, keys: &Keys, res: &dyn Resolver, seen: &Inputs, lineage: &[String]) -> Option<String> {
535    let dropped: Vec<&String> = r.inputs().iter().filter(|e| !lineage.contains(e)).collect();
536    if dropped.is_empty() {
537        return None;
538    }
539    let dropped_slices: Vec<&str> =
540        dropped.iter().filter_map(|e| lexical::split_slice_entry(e)).map(|(id, _)| id).collect();
541    let dropped_cells: Vec<&(String, String)> = dropped.iter().filter_map(|e| seen.cells.get(*e)).collect();
542    for e in lineage.iter().filter(|e| !r.inputs().contains(e)) {
543        // A slice entry is replaced by any item that begins `slice:<its slice id>:`; the rest
544        // of the item is not checked and no manifest is read (10.1 step 7.6, 18be70f).
545        if e.starts_with("slice:") {
546            if let Some(id) = dropped_slices.iter().find(|id| e.starts_with(&format!("slice:{id}:"))) {
547                return Some(format!("the evaluation read slice {id} with other members ({e})"));
548            }
549            continue;
550        }
551        if dropped_cells.is_empty() || lexical::check_hex(e, 64, "").is_err() {
552            continue;
553        }
554        let Ok(later) = obtain(keys, res, e, "", "receipt") else { continue };
555        let id = later.cell().id();
556        if dropped_cells.iter().any(|(cell, tx)| *cell == id && later.tx() > tx.as_str()) {
557            return Some(format!("the evaluation read a later version of an input's cell, receipt {e}"));
558        }
559    }
560    None
561}
562
563fn check_blob(r: &Receipt, content: &[u8]) -> Result<()> {
564    let Scalar::Blob(b) = &r.value() else {
565        return err(ErrorCode::Schema, "$.value", "content supplied for a value that is not a blob");
566    };
567    let Ok((hash, len, _)) = lexical::parse_blob(b) else {
568        return err(ErrorCode::CanonBlob, "$.value", "not a blob");
569    };
570    if content.len() as u64 != len {
571        return err(
572            ErrorCode::BlobMismatch,
573            "$.value",
574            format!("content has {} bytes, the blob states {len}", content.len()),
575        );
576    }
577    if crypto::sha256(content) != hash {
578        return err(ErrorCode::BlobMismatch, "$.value", "content does not hash to the blob's sha256");
579    }
580    Ok(())
581}