Skip to main content

candid_core/
artifact_id.rs

1//! Detached, exact-octet identity for a serialized artifact.
2//!
3//! The three identities this crate already computes are not one family:
4//!
5//! * `contract_id` and `interface_id` are **semantic Contract identities**.
6//!   Each hashes a canonicalized projection of *meaning*, so inputs that mean
7//!   the same thing collide on purpose.
8//! * `source_bundle_id` is a **raw-source bundle content identity**. It does
9//!   identify raw source-file content — the source bytes and their import edges
10//!   are exactly what it covers — so reformatting or editing a comment inside a
11//!   source *does* move it, while data *derived* from those sources never
12//!   enters it.
13//!
14//! None of the three identifies a complete serialized [`crate::Contract`],
15//! [`crate::ContractEnvelope`], or `Compilation` document. `source_bundle_id`
16//! identifies the input bundle, not the document compiled from it, and a
17//! Contract's `contract_id` is unchanged by rewriting its `producer`, by adding,
18//! removing, or editing an envelope extension, by reformatting the document, and
19//! by replacing its `SourceInfo` sidecar. That is what makes a semantic identity
20//! useful as a cache key and a compatibility key, and exactly what makes it the
21//! wrong thing to commit to when the octets themselves are what matter.
22//!
23//! [`artifact_id_with_limits`] closes that gap without touching any existing
24//! identity. It hashes the **exact octet sequence** a caller hands it, under a
25//! kind-specific domain, and returns the digest to the caller. Nothing is
26//! stored, nothing is serialized back into the artifact, and no existing
27//! payload, domain, or framing changes.
28//!
29//! # What an `artifact_id` claims
30//!
31//! Two artifact IDs are equal if and only if the [`ArtifactKind`] and the byte
32//! sequence were equal, subject to the SHA-256 collision assumption. That is
33//! the whole claim.
34//!
35//! An `artifact_id` therefore does **not** establish:
36//!
37//! * semantic equality — two byte-different encodings of the same Contract have
38//!   different artifact IDs, which is the point;
39//! * structural validity — the bytes are never parsed, so an ID exists for
40//!   input that would fail validation;
41//! * authenticity, integrity against a chosen signer, producer truth, or
42//!   signature trust — it is a content address, not a credential.
43//!
44//! No unkeyed content ID authenticates itself, and this one is no exception. A
45//! caller validates the artifact separately, through the bounded parse entry
46//! point for its kind, and uses the detached ID as a content address or as the
47//! value a signature or other external mechanism commits to — that mechanism,
48//! not the digest, is what authenticates. This crate has no signer model, key
49//! format, signature algorithm, trust policy, or registry protocol, and this
50//! module does not introduce one.
51//!
52//! # Coverage is exactly the bytes passed
53//!
54//! Only the octets actually supplied are covered — the bytes passed to the
55//! call, whether or not they have already been persisted anywhere — so what an
56//! artifact ID binds depends on which kind the caller names.
57//!
58//! [`ArtifactKind`] selects a domain and does nothing else: it neither parses
59//! nor validates. What follows therefore describes a *valid serialized document
60//! of the declared kind*. A `Contract` document contains the Contract alone —
61//! including its `producer`, which no semantic identity covers — and contains
62//! neither extensions nor `SourceInfo`. A `ContractEnvelope` document contains a
63//! Contract and its extensions and does not contain `SourceInfo`. A
64//! `Compilation` document contains a Contract and an optional `SourceInfo`
65//! sidecar and does not contain envelope extensions. Package or application
66//! version is covered only when it is literally present in the bytes passed.
67//!
68//! Arbitrary bytes hash just as well under any kind, and the resulting ID makes
69//! no claim that they are a document of that kind at all.
70//!
71//! # Construction
72//!
73//! ```text
74//! preimage    = <domain UTF-8 bytes> || 0x00 || <exact artifact bytes>
75//! artifact_id = <domain> ":sha256:" <64 lowercase hex digits of SHA-256(preimage)>
76//! ```
77//!
78//! The domain is self-describing and frozen per kind, so a digest computed for
79//! one kind can never be mistaken for another's even when the bytes are
80//! identical. There is no separate kind label and no length field in the
81//! preimage: the domain already names the kind, and the digest is taken over a
82//! single contiguous byte run whose length the caller supplied.
83//!
84//! `docs/artifact-identity-v1.md` specifies this normatively.
85
86use crate::budget::{Budget, BudgetError};
87use crate::{ContractValidationError, Limits, RuntimeContext};
88use sha2::{Digest, Sha256};
89
90/// The kind of serialized artifact an identity is computed over.
91///
92/// The kind selects the frozen domain tag, and therefore separates the digest
93/// space: identical bytes under two kinds produce two different IDs. Selecting
94/// a domain is *all* it does — no variant parses or validates anything, so any
95/// byte sequence hashes under any kind and the resulting ID makes no claim that
96/// those bytes are a document of that kind. Each variant's coverage note below
97/// therefore describes a valid serialized document of the declared kind.
98///
99/// `#[non_exhaustive]` for the same reason [`crate::LimitsProfile`] is: naming
100/// a further artifact kind must not be a breaking change. The variants below
101/// are frozen — a new kind becomes a new variant with a new domain, never a
102/// redefinition of an existing one.
103#[non_exhaustive]
104#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
105pub enum ArtifactKind {
106    /// A serialized [`crate::Contract`] document on its own: the strict
107    /// Contract with no enclosing envelope and no provenance sidecar, as
108    /// produced by [`crate::Contract::to_json_pretty_with_limits`] or by any
109    /// other encoder the caller uses.
110    ///
111    /// Covers the Contract bytes only. `producer` *is* covered, because those
112    /// bytes are in the document even though `contract_id` and `interface_id`
113    /// deliberately exclude them. Envelope extensions and `SourceInfo` are not
114    /// covered: a bare Contract document contains neither.
115    ContractJsonV1,
116    /// A serialized [`crate::ContractEnvelope`] document: a strict Contract
117    /// plus its namespaced extension map, as produced by
118    /// [`crate::ContractEnvelope::to_json_pretty_with_limits`] or by any other
119    /// encoder the caller uses.
120    ///
121    /// Covers the envelope bytes only. Extensions *are* covered, because they
122    /// are in those bytes and `contract_id` deliberately excludes them.
123    /// `SourceInfo` is not covered: an envelope document does not contain one.
124    ///
125    /// This is not the same identity as [`Self::ContractJsonV1`] over the
126    /// nested Contract: the two hash different byte sequences under different
127    /// domains.
128    ContractEnvelopeJsonV1,
129    /// A serialized `Compilation` document: a Contract plus its optional
130    /// `SourceInfo` provenance sidecar.
131    ///
132    /// Covers the compilation bytes only, including the sidecar when the
133    /// document carries one and including `producer`, which `contract_id`
134    /// deliberately excludes. Envelope extensions are not covered: a
135    /// compilation document does not contain any.
136    ///
137    /// The variant is base surface even though `Compilation` itself is
138    /// `compiler` surface — hashing bytes needs no Candid engine, so a base
139    /// consumer can content-address a compilation document it was handed.
140    CompilationJsonV1,
141}
142
143impl ArtifactKind {
144    /// The frozen, self-describing domain tag for this kind.
145    ///
146    /// Kept private: it is already the literal prefix of every rendered ID, and
147    /// `docs/artifact-identity-v1.md` states every value normatively, so
148    /// exposing an accessor would widen the public surface without adding
149    /// information.
150    const fn domain(self) -> &'static str {
151        match self {
152            Self::ContractJsonV1 => "candid-core:artifact:contract-json:v1",
153            Self::ContractEnvelopeJsonV1 => "candid-core:artifact:contract-envelope-json:v1",
154            Self::CompilationJsonV1 => "candid-core:artifact:compilation-json:v1",
155        }
156    }
157}
158
159/// The resource name every artifact identity computation charges.
160const RESOURCE: &str = "artifact_identity_work";
161
162/// Bytes hashed between two budget observations.
163///
164/// The input is hashed as borrowed slices of this length, so a large artifact
165/// never becomes one uninterruptible block and never becomes a second full
166/// allocation. Cancellation, deadlines, and work exhaustion are all observed at
167/// each boundary, because `Budget::charge` checkpoints before it charges.
168const HASH_CHUNK_BYTES: usize = 64 * 1024;
169
170/// Compute the detached artifact identity of `bytes` under caller-supplied
171/// limits.
172///
173/// This is an explicit call, never implicit in a decode: no bounded parse
174/// computes it, no serialized artifact gains a field for it, and existing
175/// validation and error precedence are unchanged by its existence.
176///
177/// # Equality and security claim
178///
179/// Equal IDs mean equal `kind` and an equal octet sequence, under the SHA-256
180/// collision assumption — nothing more. Reformatting, whitespace, JSON key
181/// order, numeric spelling, a rewritten `producer`, an added or edited
182/// extension, and a changed `SourceInfo` field all change the ID when they
183/// change the bytes.
184///
185/// The ID authenticates nothing by itself — no unkeyed content ID does. It
186/// establishes neither semantic equality, nor structural validity — the bytes
187/// are never parsed, so an ID exists for input that would fail validation — nor
188/// authenticity, producer truth, or signature trust. It is a content address,
189/// not a credential: validate the artifact separately through the bounded parse
190/// entry point for its kind, and use this ID as the value a signature or other
191/// external mechanism commits to. This crate defines no signer model, key
192/// format, signature algorithm, trust policy, or registry protocol.
193///
194/// Coverage is exactly the bytes passed to this call, whether or not they have
195/// already been persisted anywhere, so what travels with them depends on `kind`.
196/// `kind` selects a domain and neither parses nor validates, so the following
197/// describes a *valid serialized document of the declared kind*: a `Contract`
198/// document carries the Contract alone, `producer` included; a
199/// `ContractEnvelope` document carries extensions and no `SourceInfo`; a
200/// `Compilation` document carries a `SourceInfo` sidecar and no extensions;
201/// package or application version is covered only when it is literally present
202/// in those bytes. Arbitrary bytes hash just as well under any kind, and the ID
203/// makes no validity claim about them. `docs/artifact-identity-v1.md` specifies
204/// all of this normatively.
205///
206/// # Resources
207///
208/// 1. [`Limits::max_input_bytes`] is enforced against `bytes.len()` *before*
209///    any hashing, reported as resource `input_bytes`.
210/// 2. [`Limits::max_artifact_identity_work`] is then charged one unit per
211///    artifact byte plus the fixed domain framing cost, reported as resource
212///    `artifact_identity_work`. No other counter is consumed — in particular
213///    not `canonicalization_work` or `source_identity_work`.
214///
215/// # Examples
216///
217/// ```
218/// use candid_core::{artifact_id_with_limits, ArtifactKind, Limits};
219///
220/// let document = br#"{"contract":{}}"#;
221/// let id = artifact_id_with_limits(
222///     ArtifactKind::ContractEnvelopeJsonV1,
223///     document,
224///     &Limits::default(),
225/// )?;
226/// assert!(id.starts_with("candid-core:artifact:contract-envelope-json:v1:sha256:"));
227///
228/// // The same bytes under any other kind are a different identity.
229/// for other in [ArtifactKind::ContractJsonV1, ArtifactKind::CompilationJsonV1] {
230///     let rehashed = artifact_id_with_limits(other, document, &Limits::default())?;
231///     assert_ne!(id, rehashed);
232/// }
233///
234/// // One byte of whitespace is a different artifact.
235/// let reformatted = artifact_id_with_limits(
236///     ArtifactKind::ContractEnvelopeJsonV1,
237///     br#"{"contract": {}}"#,
238///     &Limits::default(),
239/// )?;
240/// assert_ne!(id, reformatted);
241/// # Ok::<(), candid_core::ContractValidationError>(())
242/// ```
243pub fn artifact_id_with_limits(
244    kind: ArtifactKind,
245    bytes: &[u8],
246    limits: &Limits,
247) -> Result<String, ContractValidationError> {
248    artifact_id_with_context(kind, bytes, &RuntimeContext::new(limits.clone()))
249}
250
251/// [`artifact_id_with_limits`] under a full [`RuntimeContext`], so a caller's
252/// [`CancellationToken`](crate::CancellationToken) and deadline are observed
253/// while hashing.
254///
255/// Cancellation and an elapsed deadline are checked before the first byte is
256/// hashed and again at every chunk boundary, and both fail closed with
257/// `operation_cancelled` / `operation_deadline_exceeded` rather than returning
258/// a partial digest.
259pub fn artifact_id_with_context(
260    kind: ArtifactKind,
261    bytes: &[u8],
262    context: &RuntimeContext,
263) -> Result<String, ContractValidationError> {
264    let mut budget = context.budget();
265    artifact_id_with_budget(kind, bytes, &mut budget)
266}
267
268fn artifact_id_with_budget(
269    kind: ArtifactKind,
270    bytes: &[u8],
271    budget: &mut Budget<'_>,
272) -> Result<String, ContractValidationError> {
273    // The byte gate first, exactly as every bounded parse entry point does it,
274    // so an oversized artifact is rejected on `input_bytes` before any hashing
275    // work is charged or performed.
276    crate::budget::observe_input_bytes(budget, bytes.len())?;
277
278    let domain = kind.domain();
279    let limit = budget.limits().max_artifact_identity_work;
280    let mut hasher = Sha256::new();
281
282    // The fixed framing cost: the domain tag plus its one-byte separator. The
283    // preimage carries no length field and no second kind label, so this is the
284    // whole constant overhead.
285    charge(budget, limit, domain.len().saturating_add(1))?;
286    hasher.update(domain.as_bytes());
287    hasher.update([0]);
288
289    // `chunks` borrows into the caller's slice, so hashing a large artifact
290    // never allocates a second copy of it.
291    for chunk in bytes.chunks(HASH_CHUNK_BYTES) {
292        charge(budget, limit, chunk.len())?;
293        hasher.update(chunk);
294        observe_chunk_boundary();
295    }
296
297    budget
298        .checkpoint()
299        .map_err(BudgetError::into_contract_error)?;
300    Ok(format!(
301        "{domain}:sha256:{}",
302        hex::encode(hasher.finalize())
303    ))
304}
305
306fn charge(
307    budget: &mut Budget<'_>,
308    limit: usize,
309    amount: usize,
310) -> Result<(), ContractValidationError> {
311    budget
312        .charge(RESOURCE, limit, amount)
313        .map(|_| ())
314        .map_err(BudgetError::into_contract_error)
315}
316
317// Test-only observation point at a hashed-chunk boundary.
318//
319// The chunk loop exists so cancellation and deadlines are observed *between*
320// chunks rather than only before the first one. Proving that deterministically
321// requires cancellation to flip while the loop is running, which no public API
322// can do from the calling thread; this seam does it, fires once, and is
323// compiled only for this crate's own unit tests. Production keeps an empty
324// function, so the loop has exactly the shape it is documented to have.
325#[cfg(test)]
326thread_local! {
327    static CANCEL_AT_CHUNK_BOUNDARY: std::cell::Cell<Option<crate::CancellationToken>> =
328        const { std::cell::Cell::new(None) };
329}
330
331#[cfg(test)]
332fn observe_chunk_boundary() {
333    CANCEL_AT_CHUNK_BOUNDARY.with(|slot| {
334        if let Some(token) = slot.take() {
335            token.cancel();
336        }
337    });
338}
339
340#[cfg(not(test))]
341fn observe_chunk_boundary() {}
342
343#[cfg(test)]
344mod tests {
345    use super::*;
346    use crate::CancellationToken;
347
348    /// Every frozen kind. `ArtifactKind::domain` matches exhaustively, so a new
349    /// variant cannot compile without declaring its domain; this list is what
350    /// the loops below iterate, and a new variant belongs in it too.
351    const ALL_KINDS: &[ArtifactKind] = &[
352        ArtifactKind::ContractJsonV1,
353        ArtifactKind::ContractEnvelopeJsonV1,
354        ArtifactKind::CompilationJsonV1,
355    ];
356
357    /// Exact `artifact_identity_work` cost of hashing `len` bytes under `kind`.
358    fn exact_work(kind: ArtifactKind, len: usize) -> usize {
359        kind.domain().len() + 1 + len
360    }
361
362    fn id(kind: ArtifactKind, bytes: &[u8]) -> String {
363        artifact_id_with_limits(kind, bytes, &Limits::default()).unwrap()
364    }
365
366    fn resource_failure(error: &ContractValidationError) -> (String, u64, u64) {
367        let violation = &error.violations[0];
368        assert_eq!(violation.code, "resource_limit_exceeded", "{error:#?}");
369        let info = violation.resource_limit.as_ref().unwrap();
370        (info.resource.clone(), info.limit, info.observed)
371    }
372
373    /// The framing anchor: hashing an empty artifact pins the domain tag and
374    /// the single separator byte, with no artifact bytes to hide a mistake in.
375    /// The same two literals are pinned independently by
376    /// `tests/fixtures/artifact-identity/manifest.json` and its Python verifier.
377    #[test]
378    fn empty_input_pins_the_domain_framing() {
379        assert_eq!(
380            id(ArtifactKind::ContractJsonV1, b""),
381            "candid-core:artifact:contract-json:v1:sha256:66c1371d29c896c2b292edc5dc1d344bf39103c5a1011141ed6883ace3e95401"
382        );
383        assert_eq!(
384            id(ArtifactKind::ContractEnvelopeJsonV1, b""),
385            "candid-core:artifact:contract-envelope-json:v1:sha256:1642aac2ca520b95cc0c31068934081c206f8673bb3779058bb88e331ff21603"
386        );
387        assert_eq!(
388            id(ArtifactKind::CompilationJsonV1, b""),
389            "candid-core:artifact:compilation-json:v1:sha256:6e716227d7ae7ac930966faafa9812eeac2fa34a85c1f03b91d949ca88b21807"
390        );
391    }
392
393    /// The domains are frozen literals, so they are pinned here as well as in
394    /// the rendered golden IDs above: a typo that moved a domain would move
395    /// every ID under it at once, and this states the intended strings directly.
396    #[test]
397    fn every_domain_is_frozen_distinct_and_well_formed() {
398        assert_eq!(
399            ALL_KINDS
400                .iter()
401                .map(|kind| kind.domain())
402                .collect::<Vec<_>>(),
403            [
404                "candid-core:artifact:contract-json:v1",
405                "candid-core:artifact:contract-envelope-json:v1",
406                "candid-core:artifact:compilation-json:v1",
407            ]
408        );
409        for (position, kind) in ALL_KINDS.iter().enumerate() {
410            let domain = kind.domain();
411            // ASCII with no NUL is what makes the single 0x00 separator
412            // unambiguous, and the prefix keeps the namespace self-describing.
413            assert!(domain.is_ascii() && !domain.contains('\0'), "{domain}");
414            assert!(domain.starts_with("candid-core:artifact:"), "{domain}");
415            for other in &ALL_KINDS[position + 1..] {
416                assert_ne!(domain, other.domain());
417            }
418        }
419    }
420
421    #[test]
422    fn the_preimage_is_the_domain_a_nul_byte_and_the_exact_bytes() {
423        for &kind in ALL_KINDS {
424            let bytes = b"{\"contract\":{}}";
425            let mut preimage = kind.domain().as_bytes().to_vec();
426            preimage.push(0);
427            preimage.extend_from_slice(bytes);
428            assert_eq!(
429                id(kind, bytes),
430                format!(
431                    "{}:sha256:{}",
432                    kind.domain(),
433                    hex::encode(Sha256::digest(&preimage))
434                )
435            );
436        }
437    }
438
439    #[test]
440    fn identical_bytes_under_different_kinds_differ() {
441        let bytes = b"{}";
442        let rendered: Vec<String> = ALL_KINDS.iter().map(|&kind| id(kind, bytes)).collect();
443        for (position, one) in rendered.iter().enumerate() {
444            for other in &rendered[position + 1..] {
445                assert_ne!(one, other, "the domain must separate the digest space");
446            }
447        }
448    }
449
450    #[test]
451    fn input_bytes_is_enforced_before_any_hashing_work() {
452        let limits = Limits::default()
453            .with_max_input_bytes(3)
454            // Zero work would fail the framing charge immediately, so a result
455            // that reports `input_bytes` proves the byte gate ran first.
456            .with_max_artifact_identity_work(0);
457        let error =
458            artifact_id_with_limits(ArtifactKind::CompilationJsonV1, b"abcd", &limits).unwrap_err();
459        assert_eq!(
460            resource_failure(&error),
461            ("input_bytes".to_string(), 3, 4),
462            "the byte gate must precede identity work"
463        );
464    }
465
466    #[test]
467    fn work_succeeds_at_the_exact_limit_and_fails_one_unit_below() {
468        let kind = ArtifactKind::ContractEnvelopeJsonV1;
469        let bytes = b"{\"contract\":{},\"extensions\":{}}";
470        let work = exact_work(kind, bytes.len());
471
472        artifact_id_with_limits(
473            kind,
474            bytes,
475            &Limits::default().with_max_artifact_identity_work(work),
476        )
477        .expect("the exact work bound must succeed");
478
479        let error = artifact_id_with_limits(
480            kind,
481            bytes,
482            &Limits::default().with_max_artifact_identity_work(work - 1),
483        )
484        .unwrap_err();
485        assert_eq!(
486            resource_failure(&error),
487            (RESOURCE.to_string(), (work - 1) as u64, work as u64)
488        );
489    }
490
491    /// Neither of the two pre-existing identity counters may be touched: an
492    /// artifact identity that spent `canonicalization_work` would let a caller
493    /// starve Contract canonicalization by content-addressing a document, and
494    /// one that spent `source_identity_work` would do the same to provenance.
495    #[test]
496    fn no_other_identity_counter_is_consumed() {
497        let limits = Limits::default();
498        let context = RuntimeContext::new(limits);
499        let mut budget = context.budget();
500        artifact_id_with_budget(
501            ArtifactKind::CompilationJsonV1,
502            &vec![b'x'; HASH_CHUNK_BYTES * 2 + 7],
503            &mut budget,
504        )
505        .unwrap();
506        assert_eq!(budget.consumed("canonicalization_work"), 0);
507        assert_eq!(budget.consumed("source_identity_work"), 0);
508        assert_eq!(
509            budget.consumed(RESOURCE),
510            exact_work(ArtifactKind::CompilationJsonV1, HASH_CHUNK_BYTES * 2 + 7)
511        );
512    }
513
514    /// Work is charged per chunk, not as one lump sum: an artifact spanning
515    /// several chunks with a budget that runs out mid-way stops at a chunk
516    /// boundary, so the reported `observed` lands strictly inside the range.
517    #[test]
518    fn work_is_charged_incrementally_across_chunks() {
519        let kind = ArtifactKind::CompilationJsonV1;
520        let bytes = vec![b'x'; HASH_CHUNK_BYTES * 3];
521        let framing = kind.domain().len() + 1;
522        let limit = framing + HASH_CHUNK_BYTES + 1;
523
524        let error = artifact_id_with_limits(
525            kind,
526            &bytes,
527            &Limits::default().with_max_artifact_identity_work(limit),
528        )
529        .unwrap_err();
530        let (resource, reported_limit, observed) = resource_failure(&error);
531        assert_eq!(resource, RESOURCE);
532        assert_eq!(reported_limit, limit as u64);
533        assert!(
534            observed > framing as u64 && observed < exact_work(kind, bytes.len()) as u64,
535            "a chunked charge must fail part-way, not at the total: {observed}"
536        );
537    }
538
539    #[test]
540    fn a_cancelled_token_fails_closed_before_hashing() {
541        let cancellation = CancellationToken::new();
542        cancellation.cancel();
543        let context = RuntimeContext::new(Limits::default()).with_cancellation(cancellation);
544        let error =
545            artifact_id_with_context(ArtifactKind::CompilationJsonV1, b"{}", &context).unwrap_err();
546        assert_eq!(error.violations[0].code, "operation_cancelled");
547    }
548
549    /// The between-chunks claim, proved directly: cancellation flips at the
550    /// first chunk boundary and the second chunk is never charged.
551    #[test]
552    fn cancellation_is_observed_between_chunks() {
553        let kind = ArtifactKind::ContractEnvelopeJsonV1;
554        let cancellation = CancellationToken::new();
555        let context =
556            RuntimeContext::new(Limits::default()).with_cancellation(cancellation.clone());
557        let mut budget = context.budget();
558        CANCEL_AT_CHUNK_BOUNDARY.with(|slot| slot.set(Some(cancellation)));
559
560        let error = artifact_id_with_budget(kind, &vec![b'x'; HASH_CHUNK_BYTES * 3], &mut budget)
561            .unwrap_err();
562        assert_eq!(error.violations[0].code, "operation_cancelled");
563        assert_eq!(
564            budget.consumed(RESOURCE),
565            kind.domain().len() + 1 + HASH_CHUNK_BYTES,
566            "exactly one chunk may be charged before cancellation is observed"
567        );
568    }
569
570    /// Native only: bare `wasm32-unknown-unknown` has no clock, and there every
571    /// explicit deadline is already reported as elapsed by `Deadline::snapshot`.
572    #[cfg(not(target_os = "unknown"))]
573    #[test]
574    fn an_elapsed_deadline_fails_closed() {
575        let context = RuntimeContext::new(Limits::default().with_deadline_unix_ms(Some(1)));
576        let error = artifact_id_with_context(
577            ArtifactKind::CompilationJsonV1,
578            &vec![b'x'; HASH_CHUNK_BYTES * 2],
579            &context,
580        )
581        .unwrap_err();
582        assert_eq!(error.violations[0].code, "operation_deadline_exceeded");
583    }
584
585    /// The default work limit must cover the largest artifact the default byte
586    /// gate admits, for *every* kind — a new kind with a longer domain must not
587    /// quietly make a maximum-size artifact unhashable out of the box. The
588    /// longest domain is asserted explicitly so the worst case is named rather
589    /// than merely satisfied.
590    #[test]
591    fn the_default_work_limit_covers_the_default_byte_gate() {
592        let limits = Limits::default();
593        let longest = ALL_KINDS
594            .iter()
595            .map(|kind| kind.domain().len())
596            .max()
597            .expect("at least one kind exists");
598        assert_eq!(
599            longest,
600            ArtifactKind::ContractEnvelopeJsonV1.domain().len(),
601            "the documented worst-case domain must still be the longest"
602        );
603        for &kind in ALL_KINDS {
604            assert!(
605                exact_work(kind, limits.max_input_bytes()) <= limits.max_artifact_identity_work(),
606                "{kind:?} at the byte gate must fit the default work limit"
607            );
608        }
609    }
610}