Skip to main content

u280_hardware_promoter/
model.rs

1//! Closed canonical request schema and the deliberate route-acceptance boundary.
2
3use crate::canonical::{is_lower_hex, parse_canonical_line};
4use crate::{FailureCode, PromotionError, PromotionResult};
5use serde::{Deserialize, Serialize};
6use std::collections::BTreeSet;
7
8const LEGACY_REQUEST_SCHEMA: u64 = 1;
9const LEGACY_REQUEST_KIND: &str = "hashsigs-u280-hardware-promotion-request-v1";
10const REQUEST_MAXIMUM_BYTES: usize = 1024 * 1024;
11
12/// Required HashSigs profile for this hardware-promotion path.
13pub const PERFORMANCE_PROFILE: &str = "HASHSIGS_SHA256_GENERIC_V1";
14/// Required performance batch size.
15pub const PERFORMANCE_BATCH: u32 = 16_384;
16/// Minimum number of measured performance samples.
17pub const MINIMUM_MEASURED_SAMPLES: u32 = 30;
18/// Clock used to normalize the baseline throughput claim.
19pub const BASELINE_CLOCK_HZ: u64 = 250_000_000;
20/// Minimum accepted sustained signatures per second.
21pub const MINIMUM_SIGNATURES_PER_SECOND: u64 = 500_000;
22/// Maximum p95 capture/core cycles per result.
23pub const MAXIMUM_CAPTURE_CYCLES_PER_RESULT: u64 = 500;
24/// Default LUT ceiling.
25pub const MAXIMUM_LUTS: u64 = 200_000;
26/// Default register ceiling.
27pub const MAXIMUM_REGISTERS: u64 = 300_000;
28/// Absolute DSP ceiling for the U280 acceptance flow.
29pub const MAXIMUM_DSPS: u64 = 4_000;
30
31#[derive(Clone, Debug, Deserialize, Eq, PartialEq, Serialize)]
32#[serde(deny_unknown_fields)]
33struct FileBinding {
34    absolute_path: String,
35    bytes: u64,
36    sha256: String,
37}
38
39impl FileBinding {
40    fn absolute_path(&self) -> &str {
41        &self.absolute_path
42    }
43
44    fn validate(&self, label: &str) -> PromotionResult<()> {
45        validate_absolute_path(&self.absolute_path, label)?;
46        if self.bytes == 0 || !is_lower_hex(&self.sha256, 64, false) {
47            return Err(PromotionError::new(
48                FailureCode::Noncanonical,
49                format!("{label} has a zero length or non-canonical SHA-256"),
50            ));
51        }
52        Ok(())
53    }
54
55    fn locator(&self) -> RequestFileLocator<'_> {
56        RequestFileLocator {
57            absolute_path: &self.absolute_path,
58            bytes: self.bytes,
59            sha256: &self.sha256,
60        }
61    }
62}
63
64/// Exact caller-supplied file locator retained only long enough for a secure
65/// descriptor opener to consume it.
66///
67/// This is not verified evidence: its fields are private, it has no serde or
68/// clone implementation, and later proof types must derive identity from the
69/// opened descriptor rather than copying this value.
70pub(crate) struct RequestFileLocator<'a> {
71    absolute_path: &'a str,
72    bytes: u64,
73    sha256: &'a str,
74}
75
76impl<'a> RequestFileLocator<'a> {
77    /// Canonical absolute path declared by the request.
78    pub(crate) const fn absolute_path(&self) -> &'a str {
79        self.absolute_path
80    }
81
82    /// Exact byte count declared by the request.
83    pub(crate) const fn bytes(&self) -> u64 {
84        self.bytes
85    }
86
87    /// Canonical lowercase SHA-256 declared by the request.
88    pub(crate) const fn sha256(&self) -> &'a str {
89        self.sha256
90    }
91}
92
93#[derive(Clone, Debug, Deserialize, Eq, PartialEq, Serialize)]
94#[serde(deny_unknown_fields)]
95struct DirectoryBinding {
96    absolute_path: String,
97}
98
99impl DirectoryBinding {
100    fn absolute_path(&self) -> &str {
101        &self.absolute_path
102    }
103
104    fn validate(&self, label: &str) -> PromotionResult<()> {
105        validate_absolute_path(&self.absolute_path, label)
106    }
107
108    fn locator(&self) -> RequestDirectoryLocator<'_> {
109        RequestDirectoryLocator {
110            absolute_path: &self.absolute_path,
111        }
112    }
113}
114
115/// Exact caller-supplied directory locator retained only long enough for a
116/// secure descriptor-rooted opener to consume it.
117///
118/// Like [`RequestFileLocator`], this is request metadata rather than verified
119/// route evidence.
120pub(crate) struct RequestDirectoryLocator<'a> {
121    absolute_path: &'a str,
122}
123
124impl<'a> RequestDirectoryLocator<'a> {
125    /// Canonical absolute path declared by the request.
126    pub(crate) const fn absolute_path(&self) -> &'a str {
127        self.absolute_path
128    }
129}
130
131#[derive(Clone, Debug, Deserialize, Eq, PartialEq, Serialize)]
132#[serde(deny_unknown_fields)]
133struct RouteInputs {
134    staged_run_root: DirectoryBinding,
135    staged_run_manifest: FileBinding,
136    acceptance_run_root: DirectoryBinding,
137    complete_run_inventory: FileBinding,
138    accepted_250mhz_route_seal: FileBinding,
139}
140
141impl RouteInputs {
142    fn validate(&self) -> PromotionResult<()> {
143        self.staged_run_root.validate("route staged-run root")?;
144        self.staged_run_manifest
145            .validate("route staged-run manifest")?;
146        self.acceptance_run_root
147            .validate("route acceptance-run root")?;
148        self.complete_run_inventory
149            .validate("route complete-run inventory")?;
150        self.accepted_250mhz_route_seal
151            .validate("accepted 250 MHz route seal")?;
152        require_child(
153            &self.staged_run_root,
154            &self.staged_run_manifest,
155            "route staged-run manifest",
156        )?;
157        require_child(
158            &self.acceptance_run_root,
159            &self.complete_run_inventory,
160            "route complete-run inventory",
161        )?;
162        require_child(
163            &self.acceptance_run_root,
164            &self.accepted_250mhz_route_seal,
165            "accepted 250 MHz route seal",
166        )
167    }
168}
169
170#[derive(Clone, Debug, Deserialize, Eq, PartialEq, Serialize)]
171#[serde(deny_unknown_fields)]
172struct TransportInputs {
173    root: DirectoryBinding,
174    top_manifest: FileBinding,
175}
176
177impl TransportInputs {
178    fn validate(&self) -> PromotionResult<()> {
179        self.root.validate("transport root")?;
180        self.top_manifest.validate("transport top manifest")?;
181        require_child(&self.root, &self.top_manifest, "transport top manifest")
182    }
183}
184
185#[derive(Clone, Debug, Deserialize, Eq, PartialEq, Serialize)]
186#[serde(deny_unknown_fields)]
187struct OracleInputs {
188    result_root: DirectoryBinding,
189    result_manifest: FileBinding,
190    cli_receipt_capture: FileBinding,
191}
192
193impl OracleInputs {
194    fn validate(&self) -> PromotionResult<()> {
195        self.result_root.validate("oracle result root")?;
196        self.result_manifest.validate("oracle result manifest")?;
197        self.cli_receipt_capture
198            .validate("oracle CLI receipt capture")?;
199        require_child(
200            &self.result_root,
201            &self.result_manifest,
202            "oracle result manifest",
203        )
204    }
205}
206
207#[derive(Clone, Debug, Deserialize, Eq, PartialEq, Serialize)]
208#[serde(deny_unknown_fields)]
209struct CoordinationInputs {
210    complete_burn_history_snapshot: FileBinding,
211    history_snapshot_manifest: FileBinding,
212    selected_burn_receipt: FileBinding,
213}
214
215impl CoordinationInputs {
216    fn validate(&self) -> PromotionResult<()> {
217        self.complete_burn_history_snapshot
218            .validate("complete coordination burn history")?;
219        self.history_snapshot_manifest
220            .validate("coordination history manifest")?;
221        self.selected_burn_receipt
222            .validate("selected coordination burn receipt")
223    }
224}
225
226#[derive(Clone, Debug, Deserialize, Eq, PartialEq, Serialize)]
227#[serde(deny_unknown_fields)]
228struct PromotionPolicy {
229    profile: String,
230    batch: u32,
231    minimum_measured_samples: u32,
232    baseline_clock_hz: u64,
233    minimum_signatures_per_second: u64,
234    maximum_capture_cycles_per_result: u64,
235    maximum_luts: u64,
236    maximum_registers: u64,
237    maximum_dsps: u64,
238}
239
240impl PromotionPolicy {
241    fn validate_fixed(&self) -> PromotionResult<()> {
242        let fixed = self.profile == PERFORMANCE_PROFILE
243            && self.batch == PERFORMANCE_BATCH
244            && self.minimum_measured_samples == MINIMUM_MEASURED_SAMPLES
245            && self.baseline_clock_hz == BASELINE_CLOCK_HZ
246            && self.minimum_signatures_per_second == MINIMUM_SIGNATURES_PER_SECOND
247            && self.maximum_capture_cycles_per_result == MAXIMUM_CAPTURE_CYCLES_PER_RESULT
248            && self.maximum_luts == MAXIMUM_LUTS
249            && self.maximum_registers == MAXIMUM_REGISTERS
250            && self.maximum_dsps == MAXIMUM_DSPS;
251        if !fixed {
252            return Err(PromotionError::new(
253                FailureCode::Schema,
254                "promotion request does not contain the fixed schema-1 policy",
255            ));
256        }
257        Ok(())
258    }
259}
260
261#[derive(Clone, Debug, Deserialize, Eq, PartialEq, Serialize)]
262#[serde(deny_unknown_fields)]
263struct LegacyPromotionRequestWire {
264    schema: u64,
265    kind: String,
266    run_id: String,
267    route: RouteInputs,
268    transport: TransportInputs,
269    oracle: OracleInputs,
270    coordination: CoordinationInputs,
271    policy: PromotionPolicy,
272}
273
274impl LegacyPromotionRequestWire {
275    fn validate(&self) -> PromotionResult<()> {
276        if self.schema != LEGACY_REQUEST_SCHEMA || self.kind != LEGACY_REQUEST_KIND {
277            return Err(PromotionError::new(
278                FailureCode::Schema,
279                "promotion request schema or kind is unsupported",
280            ));
281        }
282        if !is_lower_hex(&self.run_id, 32, true) {
283            return Err(PromotionError::new(
284                FailureCode::Noncanonical,
285                "promotion run_id must be a nonzero 128-bit lowercase hex value",
286            ));
287        }
288        self.route.validate()?;
289        self.transport.validate()?;
290        self.oracle.validate()?;
291        self.coordination.validate()?;
292        self.policy.validate_fixed()?;
293
294        let paths = [
295            self.route.staged_run_root.absolute_path(),
296            self.route.staged_run_manifest.absolute_path(),
297            self.route.acceptance_run_root.absolute_path(),
298            self.route.complete_run_inventory.absolute_path(),
299            self.route.accepted_250mhz_route_seal.absolute_path(),
300            self.transport.root.absolute_path(),
301            self.transport.top_manifest.absolute_path(),
302            self.oracle.result_root.absolute_path(),
303            self.oracle.result_manifest.absolute_path(),
304            self.oracle.cli_receipt_capture.absolute_path(),
305            self.coordination
306                .complete_burn_history_snapshot
307                .absolute_path(),
308            self.coordination.history_snapshot_manifest.absolute_path(),
309            self.coordination.selected_burn_receipt.absolute_path(),
310        ];
311        let unique = paths.into_iter().collect::<BTreeSet<_>>();
312        if unique.len() != paths.len() {
313            return Err(PromotionError::new(
314                FailureCode::Schema,
315                "promotion request aliases two independently bound input paths",
316            ));
317        }
318        Ok(())
319    }
320}
321
322/// Opaque validated legacy schema-1 promotion request.
323///
324/// The serde wire type is private, so callers cannot deserialize around
325/// [`Self::parse_canonical`]. Schema 1 contains locators only and has no success
326/// result type. A closed schema-2 request remains deferred until the route and
327/// coordination adapters can structurally require every retained proof.
328#[derive(Clone, Debug, Eq, PartialEq)]
329pub struct PromotionRequest {
330    wire: LegacyPromotionRequestWire,
331}
332
333impl PromotionRequest {
334    /// Parses an exact compact, newline-terminated, duplicate-free request.
335    ///
336    /// # Errors
337    ///
338    /// Returns [`FailureCode::Schema`] or [`FailureCode::Noncanonical`] for a
339    /// malformed request, a nonliteral policy, or a path/digest violation.
340    pub fn parse_canonical(bytes: &[u8]) -> PromotionResult<Self> {
341        if caller_supplied_terminal_claim(bytes) {
342            return Err(PromotionError::new(
343                FailureCode::PrematureClaim,
344                "promotion requests cannot contain terminal success fields",
345            ));
346        }
347        let request: LegacyPromotionRequestWire =
348            parse_canonical_line(bytes, REQUEST_MAXIMUM_BYTES, "promotion request")?;
349        request.validate()?;
350        Ok(Self { wire: request })
351    }
352
353    /// Returns the run identity consumed by coordination and promotion state.
354    #[must_use]
355    pub fn run_id(&self) -> &str {
356        &self.wire.run_id
357    }
358
359    pub(crate) fn coordination_history_binding(&self) -> (u64, &str) {
360        let locators = self.coordination_locators();
361        let history = locators.complete_burn_history_snapshot();
362        (history.bytes(), history.sha256())
363    }
364
365    pub(crate) fn selected_burn_binding(&self) -> (u64, &str) {
366        let locators = self.coordination_locators();
367        let selected = locators.selected_burn_receipt();
368        (selected.bytes(), selected.sha256())
369    }
370
371    /// Returns the five exact route locators from the validated legacy
372    /// request. A future descriptor-retaining AMD-format parser must consume
373    /// these locators; this bundle is not accepted-route evidence by itself.
374    pub(crate) fn route_locators(&self) -> RouteRequestLocators<'_> {
375        RouteRequestLocators {
376            staged_run_root: self.wire.route.staged_run_root.locator(),
377            staged_run_manifest: self.wire.route.staged_run_manifest.locator(),
378            acceptance_run_root: self.wire.route.acceptance_run_root.locator(),
379            complete_run_inventory: self.wire.route.complete_run_inventory.locator(),
380            accepted_250mhz_route_seal: self.wire.route.accepted_250mhz_route_seal.locator(),
381        }
382    }
383
384    /// Returns the three exact coordination locators from the validated legacy
385    /// request. They are inputs to descriptor retention, never proof by
386    /// themselves.
387    pub(crate) fn coordination_locators(&self) -> CoordinationRequestLocators<'_> {
388        CoordinationRequestLocators {
389            complete_burn_history_snapshot: self
390                .wire
391                .coordination
392                .complete_burn_history_snapshot
393                .locator(),
394            history_snapshot_manifest: self.wire.coordination.history_snapshot_manifest.locator(),
395            selected_burn_receipt: self.wire.coordination.selected_burn_receipt.locator(),
396        }
397    }
398
399    /// Returns the two exact transport locators from the validated legacy
400    /// request. A descriptor-retaining transport guard must consume these
401    /// locators and join them to typed oracle evidence; this bundle does not
402    /// prove transport identity or the selected cryptographic profile.
403    #[allow(dead_code)] // Called only by the future descriptor-retaining transport adapter.
404    pub(crate) fn transport_locators(&self) -> TransportRequestLocators<'_> {
405        TransportRequestLocators {
406            root: self.wire.transport.root.locator(),
407            top_manifest: self.wire.transport.top_manifest.locator(),
408        }
409    }
410
411    /// Returns the three exact oracle locators from the validated legacy
412    /// request. A descriptor-retaining oracle adapter must consume these
413    /// locators and join them to its typed result and receipt proofs; this
414    /// bundle is not an oracle-success claim by itself.
415    #[allow(dead_code)] // Called only by the future descriptor-retaining oracle adapter.
416    pub(crate) fn oracle_locators(&self) -> OracleRequestLocators<'_> {
417        OracleRequestLocators {
418            result_root: self.wire.oracle.result_root.locator(),
419            result_manifest: self.wire.oracle.result_manifest.locator(),
420            cli_receipt_capture: self.wire.oracle.cli_receipt_capture.locator(),
421        }
422    }
423}
424
425/// Validated request locators needed to open and retain one route evidence
426/// chain.
427///
428/// The fields remain private, non-serializable, and non-cloneable. Exact path,
429/// byte-count, and digest equality is necessary for route acceptance, but it
430/// is never sufficient without descriptor-backed parsing of the bound files.
431pub(crate) struct RouteRequestLocators<'a> {
432    staged_run_root: RequestDirectoryLocator<'a>,
433    staged_run_manifest: RequestFileLocator<'a>,
434    acceptance_run_root: RequestDirectoryLocator<'a>,
435    complete_run_inventory: RequestFileLocator<'a>,
436    accepted_250mhz_route_seal: RequestFileLocator<'a>,
437}
438
439impl<'a> RouteRequestLocators<'a> {
440    /// Immutable stage root locator.
441    pub(crate) const fn staged_run_root(&self) -> &RequestDirectoryLocator<'a> {
442        &self.staged_run_root
443    }
444
445    /// Immutable stage-manifest locator.
446    pub(crate) const fn staged_run_manifest(&self) -> &RequestFileLocator<'a> {
447        &self.staged_run_manifest
448    }
449
450    /// Acceptance-run root locator.
451    pub(crate) const fn acceptance_run_root(&self) -> &RequestDirectoryLocator<'a> {
452        &self.acceptance_run_root
453    }
454
455    /// Complete acceptance-run inventory locator.
456    pub(crate) const fn complete_run_inventory(&self) -> &RequestFileLocator<'a> {
457        &self.complete_run_inventory
458    }
459
460    /// Accepted 250 MHz route-seal locator.
461    pub(crate) const fn accepted_250mhz_route_seal(&self) -> &RequestFileLocator<'a> {
462        &self.accepted_250mhz_route_seal
463    }
464}
465
466/// Validated request locators needed to open and retain one transport evidence
467/// chain.
468///
469/// This bundle is deliberately neither cloneable nor serializable. Its exact
470/// path, byte-count, and digest bindings remain request metadata until a typed,
471/// descriptor-backed transport guard validates and retains them.
472#[allow(dead_code)] // Constructed only for the future transport adapter.
473pub(crate) struct TransportRequestLocators<'a> {
474    root: RequestDirectoryLocator<'a>,
475    top_manifest: RequestFileLocator<'a>,
476}
477
478#[allow(dead_code)] // Accessed only by the future transport adapter.
479impl<'a> TransportRequestLocators<'a> {
480    /// Canonical transport-root locator.
481    pub(crate) const fn root(&self) -> &RequestDirectoryLocator<'a> {
482        &self.root
483    }
484
485    /// Top-level transport-manifest locator.
486    pub(crate) const fn top_manifest(&self) -> &RequestFileLocator<'a> {
487        &self.top_manifest
488    }
489}
490
491/// Validated request locators needed to open and retain one oracle evidence
492/// chain.
493///
494/// This bundle is deliberately neither cloneable nor serializable. The
495/// locators must be joined to descriptor-backed oracle result and receipt
496/// proofs before they can contribute to hardware promotion.
497#[allow(dead_code)] // Constructed only for the future oracle adapter.
498pub(crate) struct OracleRequestLocators<'a> {
499    result_root: RequestDirectoryLocator<'a>,
500    result_manifest: RequestFileLocator<'a>,
501    cli_receipt_capture: RequestFileLocator<'a>,
502}
503
504#[allow(dead_code)] // Accessed only by the future oracle adapter.
505impl<'a> OracleRequestLocators<'a> {
506    /// Canonical oracle-result root locator.
507    pub(crate) const fn result_root(&self) -> &RequestDirectoryLocator<'a> {
508        &self.result_root
509    }
510
511    /// Oracle-result manifest locator.
512    pub(crate) const fn result_manifest(&self) -> &RequestFileLocator<'a> {
513        &self.result_manifest
514    }
515
516    /// Exact CLI receipt-capture locator.
517    pub(crate) const fn cli_receipt_capture(&self) -> &RequestFileLocator<'a> {
518        &self.cli_receipt_capture
519    }
520}
521
522/// Validated request locators needed to open and retain the complete
523/// coordination snapshot.
524///
525/// This bundle is deliberately neither cloneable nor serializable and does not
526/// represent a verified coordination proof.
527pub(crate) struct CoordinationRequestLocators<'a> {
528    complete_burn_history_snapshot: RequestFileLocator<'a>,
529    history_snapshot_manifest: RequestFileLocator<'a>,
530    selected_burn_receipt: RequestFileLocator<'a>,
531}
532
533impl<'a> CoordinationRequestLocators<'a> {
534    /// Complete genesis-rooted coordination history locator.
535    pub(crate) const fn complete_burn_history_snapshot(&self) -> &RequestFileLocator<'a> {
536        &self.complete_burn_history_snapshot
537    }
538
539    /// Manifest locator that must bind the complete history descriptor.
540    pub(crate) const fn history_snapshot_manifest(&self) -> &RequestFileLocator<'a> {
541        &self.history_snapshot_manifest
542    }
543
544    /// Exact selected burn-receipt locator.
545    pub(crate) const fn selected_burn_receipt(&self) -> &RequestFileLocator<'a> {
546        &self.selected_burn_receipt
547    }
548}
549
550fn caller_supplied_terminal_claim(bytes: &[u8]) -> bool {
551    let Ok(serde_json::Value::Object(object)) = serde_json::from_slice(bytes) else {
552        return false;
553    };
554    [
555        "hardware_execution",
556        "card_validated",
557        "cryptographic_outputs_valid",
558        "performance_validated",
559        "hardware_completion_promotable",
560    ]
561    .into_iter()
562    .any(|field| object.contains_key(field))
563}
564
565fn require_child(root: &DirectoryBinding, file: &FileBinding, label: &str) -> PromotionResult<()> {
566    let prefix = format!("{}/", root.absolute_path());
567    if !file.absolute_path().starts_with(&prefix) {
568        return Err(PromotionError::new(
569            FailureCode::Schema,
570            format!("{label} is not descriptor-rooted beneath its declared directory"),
571        ));
572    }
573    Ok(())
574}
575
576pub(crate) fn validate_absolute_path(value: &str, label: &str) -> PromotionResult<()> {
577    let canonical = value.starts_with('/')
578        && value.len() > 1
579        && !value.ends_with('/')
580        && !value.contains("//")
581        && !value.contains('\0')
582        && value
583            .split('/')
584            .skip(1)
585            .all(|component| !component.is_empty() && component != "." && component != "..");
586    if !canonical {
587        return Err(PromotionError::new(
588            FailureCode::Noncanonical,
589            format!("{label} is not a traversal-free canonical absolute path"),
590        ));
591    }
592    Ok(())
593}
594
595#[cfg(test)]
596pub(crate) fn synthetic_request(run_id: &str) -> PromotionRequest {
597    let file = |path: &str, salt: char| FileBinding {
598        absolute_path: path.to_owned(),
599        bytes: 10,
600        sha256: salt.to_string().repeat(64),
601    };
602    PromotionRequest {
603        wire: LegacyPromotionRequestWire {
604            schema: LEGACY_REQUEST_SCHEMA,
605            kind: LEGACY_REQUEST_KIND.to_owned(),
606            run_id: run_id.to_owned(),
607            route: RouteInputs {
608                staged_run_root: DirectoryBinding {
609                    absolute_path: "/evidence/route-stage".to_owned(),
610                },
611                staged_run_manifest: file("/evidence/route-stage/SHA256SUMS", '1'),
612                acceptance_run_root: DirectoryBinding {
613                    absolute_path: "/evidence/route-acceptance".to_owned(),
614                },
615                complete_run_inventory: file("/evidence/route-acceptance/inventory.json", '2'),
616                accepted_250mhz_route_seal: file("/evidence/route-acceptance/route-seal.json", '3'),
617            },
618            transport: TransportInputs {
619                root: DirectoryBinding {
620                    absolute_path: "/evidence/transport".to_owned(),
621                },
622                top_manifest: file("/evidence/transport/SHA256SUMS", '4'),
623            },
624            oracle: OracleInputs {
625                result_root: DirectoryBinding {
626                    absolute_path: "/evidence/oracle".to_owned(),
627                },
628                result_manifest: file("/evidence/oracle/SHA256SUMS", '5'),
629                cli_receipt_capture: file("/evidence/oracle-receipt.json", '6'),
630            },
631            coordination: CoordinationInputs {
632                complete_burn_history_snapshot: file("/evidence/coordination-history.jsonl", '7'),
633                history_snapshot_manifest: file("/evidence/coordination-history.SHA256SUMS", '8'),
634                selected_burn_receipt: file("/evidence/coordination-burn.json", '9'),
635            },
636            policy: PromotionPolicy {
637                profile: PERFORMANCE_PROFILE.to_owned(),
638                batch: PERFORMANCE_BATCH,
639                minimum_measured_samples: MINIMUM_MEASURED_SAMPLES,
640                baseline_clock_hz: BASELINE_CLOCK_HZ,
641                minimum_signatures_per_second: MINIMUM_SIGNATURES_PER_SECOND,
642                maximum_capture_cycles_per_result: MAXIMUM_CAPTURE_CYCLES_PER_RESULT,
643                maximum_luts: MAXIMUM_LUTS,
644                maximum_registers: MAXIMUM_REGISTERS,
645                maximum_dsps: MAXIMUM_DSPS,
646            },
647        },
648    }
649}
650
651#[cfg(test)]
652pub(crate) fn synthetic_request_with_coordination(
653    run_id: &str,
654    history: &[u8],
655    selected_receipt: &[u8],
656) -> PromotionRequest {
657    let mut request = synthetic_request(run_id);
658    request
659        .wire
660        .coordination
661        .complete_burn_history_snapshot
662        .bytes = u64::try_from(history.len()).unwrap();
663    request
664        .wire
665        .coordination
666        .complete_burn_history_snapshot
667        .sha256 = crate::canonical::sha256_hex(history);
668    request.wire.coordination.selected_burn_receipt.bytes =
669        u64::try_from(selected_receipt.len()).unwrap();
670    request.wire.coordination.selected_burn_receipt.sha256 =
671        crate::canonical::sha256_hex(selected_receipt);
672    request
673}
674
675#[cfg(test)]
676pub(crate) fn synthetic_request_with_coordination_snapshot(
677    run_id: &str,
678    files: [(&str, &[u8]); 3],
679) -> PromotionRequest {
680    let mut request = synthetic_request(run_id);
681    request
682        .wire
683        .coordination
684        .complete_burn_history_snapshot
685        .absolute_path = files[0].0.to_owned();
686    request
687        .wire
688        .coordination
689        .complete_burn_history_snapshot
690        .bytes = u64::try_from(files[0].1.len()).unwrap();
691    request
692        .wire
693        .coordination
694        .complete_burn_history_snapshot
695        .sha256 = crate::canonical::sha256_hex(files[0].1);
696    request
697        .wire
698        .coordination
699        .history_snapshot_manifest
700        .absolute_path = files[1].0.to_owned();
701    request.wire.coordination.history_snapshot_manifest.bytes =
702        u64::try_from(files[1].1.len()).unwrap();
703    request.wire.coordination.history_snapshot_manifest.sha256 =
704        crate::canonical::sha256_hex(files[1].1);
705    request
706        .wire
707        .coordination
708        .selected_burn_receipt
709        .absolute_path = files[2].0.to_owned();
710    request.wire.coordination.selected_burn_receipt.bytes =
711        u64::try_from(files[2].1.len()).unwrap();
712    request.wire.coordination.selected_burn_receipt.sha256 =
713        crate::canonical::sha256_hex(files[2].1);
714    request
715}
716
717#[cfg(test)]
718mod tests {
719    use super::{LegacyPromotionRequestWire, PromotionRequest, synthetic_request};
720    use crate::FailureCode;
721    use crate::canonical::canonical_line;
722
723    fn fixture() -> LegacyPromotionRequestWire {
724        synthetic_request(&"1".repeat(32)).wire
725    }
726
727    #[test]
728    fn exact_request_passes_and_success_fields_are_premature_claims() {
729        let canonical = canonical_line(&fixture()).unwrap();
730        assert_eq!(
731            PromotionRequest::parse_canonical(&canonical).unwrap().wire,
732            fixture()
733        );
734
735        let mut value: serde_json::Value = serde_json::from_slice(&canonical).unwrap();
736        value["hardware_execution"] = serde_json::Value::Bool(false);
737        let attempted = serde_json::to_vec(&value).unwrap();
738        assert_eq!(
739            PromotionRequest::parse_canonical(&attempted)
740                .unwrap_err()
741                .code(),
742            FailureCode::PrematureClaim
743        );
744    }
745
746    #[test]
747    fn duplicates_reordering_and_relaxed_policy_fail_closed() {
748        let canonical = String::from_utf8(canonical_line(&fixture()).unwrap()).unwrap();
749        let duplicate = canonical.replacen("{\"schema\":1", "{\"schema\":1,\"schema\":1", 1);
750        assert!(PromotionRequest::parse_canonical(duplicate.as_bytes()).is_err());
751
752        let reordered = canonical.replacen(
753            "{\"schema\":1,\"kind\":\"hashsigs-u280-hardware-promotion-request-v1\"",
754            "{\"kind\":\"hashsigs-u280-hardware-promotion-request-v1\",\"schema\":1",
755            1,
756        );
757        assert_eq!(
758            PromotionRequest::parse_canonical(reordered.as_bytes())
759                .unwrap_err()
760                .code(),
761            FailureCode::Noncanonical
762        );
763
764        let relaxed = canonical.replacen(
765            "\"minimum_signatures_per_second\":500000",
766            "\"minimum_signatures_per_second\":499999",
767            1,
768        );
769        assert_eq!(
770            PromotionRequest::parse_canonical(relaxed.as_bytes())
771                .unwrap_err()
772                .code(),
773            FailureCode::Schema
774        );
775
776        let unsupported_schema = canonical.replacen("\"schema\":1", "\"schema\":2", 1);
777        assert_eq!(
778            PromotionRequest::parse_canonical(unsupported_schema.as_bytes())
779                .unwrap_err()
780                .code(),
781            FailureCode::Schema
782        );
783    }
784
785    #[test]
786    fn coordination_locators_are_exact_request_inputs_not_proofs() {
787        let request = synthetic_request(&"1".repeat(32));
788        let locators = request.coordination_locators();
789        let history = locators.complete_burn_history_snapshot();
790        assert_eq!(
791            history.absolute_path(),
792            "/evidence/coordination-history.jsonl"
793        );
794        assert_eq!(history.bytes(), 10);
795        assert_eq!(history.sha256(), "7".repeat(64));
796        assert_eq!(
797            locators.history_snapshot_manifest().absolute_path(),
798            "/evidence/coordination-history.SHA256SUMS"
799        );
800        assert_eq!(
801            locators.selected_burn_receipt().absolute_path(),
802            "/evidence/coordination-burn.json"
803        );
804    }
805
806    #[test]
807    fn route_locators_are_exact_request_inputs_not_proofs() {
808        let request = synthetic_request(&"1".repeat(32));
809        let locators = request.route_locators();
810        assert_eq!(
811            locators.staged_run_root().absolute_path(),
812            "/evidence/route-stage"
813        );
814        assert_eq!(
815            locators.staged_run_manifest().absolute_path(),
816            "/evidence/route-stage/SHA256SUMS"
817        );
818        assert_eq!(
819            locators.acceptance_run_root().absolute_path(),
820            "/evidence/route-acceptance"
821        );
822        assert_eq!(locators.complete_run_inventory().bytes(), 10);
823        assert_eq!(locators.complete_run_inventory().sha256(), "2".repeat(64));
824        assert_eq!(
825            locators.accepted_250mhz_route_seal().absolute_path(),
826            "/evidence/route-acceptance/route-seal.json"
827        );
828    }
829
830    #[test]
831    fn transport_locators_are_exact_request_inputs_not_proofs() {
832        let request = synthetic_request(&"1".repeat(32));
833        let locators = request.transport_locators();
834        assert_eq!(locators.root().absolute_path(), "/evidence/transport");
835        assert_eq!(
836            locators.top_manifest().absolute_path(),
837            "/evidence/transport/SHA256SUMS"
838        );
839        assert_eq!(locators.top_manifest().bytes(), 10);
840        assert_eq!(locators.top_manifest().sha256(), "4".repeat(64));
841    }
842
843    #[test]
844    fn oracle_locators_are_exact_request_inputs_not_proofs() {
845        let request = synthetic_request(&"1".repeat(32));
846        let locators = request.oracle_locators();
847        assert_eq!(locators.result_root().absolute_path(), "/evidence/oracle");
848        assert_eq!(
849            locators.result_manifest().absolute_path(),
850            "/evidence/oracle/SHA256SUMS"
851        );
852        assert_eq!(locators.result_manifest().bytes(), 10);
853        assert_eq!(locators.result_manifest().sha256(), "5".repeat(64));
854        assert_eq!(
855            locators.cli_receipt_capture().absolute_path(),
856            "/evidence/oracle-receipt.json"
857        );
858        assert_eq!(locators.cli_receipt_capture().bytes(), 10);
859        assert_eq!(locators.cli_receipt_capture().sha256(), "6".repeat(64));
860    }
861}