solverforge_solver/stats/
candidate_trace_qualified.rs1use std::fmt;
11
12use super::{
13 CandidateTraceExternalDigest, CandidateTraceInputAttestation, CandidateTraceInputProvenance,
14};
15
16#[derive(Debug, Clone, PartialEq, Eq)]
17pub enum CandidateTraceQualificationError {
18 QualificationNotRequested,
19 EmptyExternalProducer,
20 MissingCoreTreeDigest,
21 MissingBuildDigest,
22}
23
24impl fmt::Display for CandidateTraceQualificationError {
25 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
26 match self {
27 Self::QualificationNotRequested => {
28 write!(f, "qualified candidate-trace provenance was not requested")
29 }
30 Self::EmptyExternalProducer => {
31 write!(f, "qualified candidate-trace producer must not be empty")
32 }
33 Self::MissingCoreTreeDigest => write!(
34 f,
35 "qualified candidate-trace provenance requires a core-tree digest"
36 ),
37 Self::MissingBuildDigest => write!(
38 f,
39 "qualified candidate-trace provenance requires a loaded-build digest"
40 ),
41 }
42 }
43}
44
45impl std::error::Error for CandidateTraceQualificationError {}
46
47#[derive(Debug, Clone, Copy, PartialEq, Eq)]
53pub enum CandidateTraceQualificationStatus {
54 NotRequested,
55 Qualified,
56}
57
58#[derive(Debug, Clone, PartialEq, Eq)]
66pub struct QualifiedCandidateTraceRunProvenance {
67 input: CandidateTraceInputProvenance,
68}
69
70impl QualifiedCandidateTraceRunProvenance {
71 #[allow(clippy::too_many_arguments)]
72 pub fn externally_attested(
73 schema_digest: CandidateTraceExternalDigest,
74 instance_digest: CandidateTraceExternalDigest,
75 initial_state_digest: CandidateTraceExternalDigest,
76 core_tree_digest: CandidateTraceExternalDigest,
77 build_digest: CandidateTraceExternalDigest,
78 producer: impl Into<String>,
79 ) -> Result<Self, CandidateTraceQualificationError> {
80 let producer = producer.into();
81 if producer.trim().is_empty() {
82 return Err(CandidateTraceQualificationError::EmptyExternalProducer);
83 }
84 Ok(Self {
85 input: CandidateTraceInputProvenance {
86 schema_digest,
87 instance_digest,
88 initial_state_digest,
89 core_tree_digest: Some(core_tree_digest),
90 build_digest: Some(build_digest),
91 attestation: CandidateTraceInputAttestation::external(producer),
92 },
93 })
94 }
95
96 pub fn try_from_input(
99 input: CandidateTraceInputProvenance,
100 ) -> Result<Self, CandidateTraceQualificationError> {
101 let producer = input.attestation.external_producer();
102 if producer.trim().is_empty() {
103 return Err(CandidateTraceQualificationError::EmptyExternalProducer);
104 }
105 if input.core_tree_digest.is_none() {
106 return Err(CandidateTraceQualificationError::MissingCoreTreeDigest);
107 }
108 if input.build_digest.is_none() {
109 return Err(CandidateTraceQualificationError::MissingBuildDigest);
110 }
111 Ok(Self { input })
112 }
113
114 pub fn input_provenance(&self) -> &CandidateTraceInputProvenance {
115 &self.input
116 }
117}
118
119#[cfg(test)]
120mod tests {
121 use super::*;
122 use crate::stats::{
123 CandidateTraceExecutionPolicy, CandidateTraceHeader, CandidateTracePhasePlan,
124 CandidateTraceQualificationStatus, CandidateTraceTelemetry,
125 };
126
127 fn digest(byte: u8) -> CandidateTraceExternalDigest {
128 CandidateTraceExternalDigest::sha256([byte; 32])
129 }
130
131 fn qualified() -> QualifiedCandidateTraceRunProvenance {
132 QualifiedCandidateTraceRunProvenance::externally_attested(
133 digest(1),
134 digest(2),
135 digest(3),
136 digest(4),
137 digest(5),
138 "solverforge-bench",
139 )
140 .expect("all immutable digests and a producer qualify the run")
141 }
142
143 #[test]
144 fn qualification_rejects_an_empty_producer_before_execution() {
145 assert_eq!(
146 QualifiedCandidateTraceRunProvenance::externally_attested(
147 digest(1),
148 digest(2),
149 digest(3),
150 digest(4),
151 digest(5),
152 " ",
153 ),
154 Err(CandidateTraceQualificationError::EmptyExternalProducer),
155 );
156 }
157
158 #[test]
159 fn optional_provenance_cannot_silently_upgrade_to_qualified() {
160 let input = CandidateTraceInputProvenance::externally_attested(
161 digest(1),
162 digest(2),
163 digest(3),
164 "solverforge-bench",
165 );
166 assert_eq!(
167 QualifiedCandidateTraceRunProvenance::try_from_input(input),
168 Err(CandidateTraceQualificationError::MissingCoreTreeDigest),
169 );
170 }
171
172 #[test]
173 fn qualified_header_exposes_exact_attestation_and_normal_header_stays_unqualified() {
174 let qualified = qualified();
175 let header = CandidateTraceHeader::new_qualified(
176 "[candidate_trace]\nmax_entries = 1\n".to_string(),
177 CandidateTraceExecutionPolicy::known("test", std::iter::empty::<(String, String)>()),
178 CandidateTracePhasePlan::known(
179 "test",
180 std::iter::empty::<(String, String)>(),
181 Vec::new(),
182 ),
183 qualified.clone(),
184 );
185 let trace = CandidateTraceTelemetry::new(header, 1);
186 assert_eq!(trace.require_qualified_run_provenance(), Ok(&qualified));
187 assert_eq!(
188 trace.provenance_status().qualification,
189 CandidateTraceQualificationStatus::Qualified,
190 );
191
192 let normal = CandidateTraceTelemetry::new(
193 CandidateTraceHeader::new(
194 "[candidate_trace]\nmax_entries = 1\n".to_string(),
195 CandidateTraceExecutionPolicy::known(
196 "test",
197 std::iter::empty::<(String, String)>(),
198 ),
199 CandidateTracePhasePlan::known(
200 "test",
201 std::iter::empty::<(String, String)>(),
202 Vec::new(),
203 ),
204 Some(qualified.input_provenance().clone()),
205 ),
206 1,
207 );
208 assert_eq!(
209 normal.require_qualified_run_provenance(),
210 Err(CandidateTraceQualificationError::QualificationNotRequested),
211 );
212 assert_eq!(
213 normal.provenance_status().qualification,
214 CandidateTraceQualificationStatus::NotRequested,
215 );
216 }
217
218 #[test]
219 fn finalizing_a_plan_preserves_qualified_run_attestation() {
220 let qualified = qualified();
221 let mut trace = CandidateTraceTelemetry::new(
222 CandidateTraceHeader::new_qualified(
223 "[candidate_trace]\nmax_entries = 1\n".to_string(),
224 CandidateTraceExecutionPolicy::known(
225 "test",
226 std::iter::empty::<(String, String)>(),
227 ),
228 CandidateTracePhasePlan::opaque("test.pending"),
229 qualified.clone(),
230 ),
231 1,
232 );
233
234 let terminal_plan = CandidateTracePhasePlan::known(
235 "test.terminal",
236 std::iter::empty::<(String, String)>(),
237 Vec::new(),
238 );
239 trace.finalize_resolved_phase_plan(terminal_plan.clone());
240
241 assert_eq!(trace.header.resolved_phase_plan, terminal_plan);
242 assert!(trace.header.resolved_phase_plan_complete);
243 assert_eq!(trace.require_qualified_run_provenance(), Ok(&qualified));
244 assert_eq!(
245 trace.provenance_status().qualification,
246 CandidateTraceQualificationStatus::Qualified,
247 );
248 }
249}