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}