1use std::fmt::Write as _;
2use std::io::{self, Read};
3use std::path::PathBuf;
4use std::process::{Command, Stdio};
5use std::sync::mpsc;
6use std::thread;
7use std::time::{Duration, Instant};
8
9use crate::domain::{CommitOwnership, MemberId};
10use crate::error::{Error, Result};
11use crate::integrity::{IntegrityStatus, VerifiedTransaction};
12use crate::transaction::{PostCommitFailurePolicy, TransactionReceipt};
13
14const DEFAULT_TIMEOUT: Duration = Duration::from_secs(5);
15const DEFAULT_OUTPUT_LIMIT: usize = 64 * 1024;
16
17#[derive(Debug, Clone)]
19pub struct CommandSpec {
20 program: PathBuf,
21 args: Vec<String>,
22 current_dir: Option<PathBuf>,
23 timeout: Duration,
24 max_output_bytes: usize,
25 environment: Vec<(String, String)>,
26}
27
28impl CommandSpec {
29 pub fn new(program: impl Into<PathBuf>) -> Self {
31 Self {
32 program: program.into(),
33 args: Vec::new(),
34 current_dir: None,
35 timeout: DEFAULT_TIMEOUT,
36 max_output_bytes: DEFAULT_OUTPUT_LIMIT,
37 environment: Vec::new(),
38 }
39 }
40
41 pub fn arg(mut self, argument: impl Into<String>) -> Self {
43 self.args.push(argument.into());
44 self
45 }
46
47 pub fn args<I, S>(mut self, arguments: I) -> Self
49 where
50 I: IntoIterator<Item = S>,
51 S: Into<String>,
52 {
53 self.args.extend(arguments.into_iter().map(Into::into));
54 self
55 }
56
57 pub fn current_dir(mut self, path: impl Into<PathBuf>) -> Self {
59 self.current_dir = Some(path.into());
60 self
61 }
62
63 pub fn timeout(mut self, timeout: Duration) -> Self {
65 self.timeout = timeout;
66 self
67 }
68
69 pub fn max_output_bytes(mut self, limit: usize) -> Self {
71 self.max_output_bytes = limit;
72 self
73 }
74
75 pub fn environment(mut self, name: impl Into<String>, value: impl Into<String>) -> Self {
77 self.environment.push((name.into(), value.into()));
78 self
79 }
80}
81
82#[derive(Debug, Clone, PartialEq, Eq)]
84pub struct CommandOutput {
85 exit_code: Option<i32>,
86 stdout: Vec<u8>,
87 stderr: Vec<u8>,
88 timed_out: bool,
89 output_limited: bool,
90}
91
92impl CommandOutput {
93 pub fn exit_code(&self) -> Option<i32> {
95 self.exit_code
96 }
97
98 pub fn stdout(&self) -> &[u8] {
100 &self.stdout
101 }
102
103 pub fn stderr(&self) -> &[u8] {
105 &self.stderr
106 }
107
108 pub fn timed_out(&self) -> bool {
110 self.timed_out
111 }
112
113 pub fn output_limited(&self) -> bool {
115 self.output_limited
116 }
117
118 pub fn success(&self) -> bool {
120 !self.timed_out && !self.output_limited && self.exit_code == Some(0)
121 }
122}
123
124pub fn run_bounded(spec: &CommandSpec) -> Result<CommandOutput> {
126 let mut command = Command::new(&spec.program);
127 command
128 .args(&spec.args)
129 .env_clear()
130 .stdin(Stdio::null())
131 .stdout(Stdio::piped())
132 .stderr(Stdio::piped());
133 if let Some(current_dir) = &spec.current_dir {
134 command.current_dir(current_dir);
135 }
136 for (name, value) in &spec.environment {
137 command.env(name, value);
138 }
139 let mut child = command
140 .spawn()
141 .map_err(|source| Error::io("spawning bounded candidate", source))?;
142 let stdout = child
143 .stdout
144 .take()
145 .ok_or_else(|| Error::CandidateExecution("missing stdout pipe".into()))?;
146 let stderr = child
147 .stderr
148 .take()
149 .ok_or_else(|| Error::CandidateExecution("missing stderr pipe".into()))?;
150 let (stdout_tx, stdout_rx) = mpsc::channel();
151 let (stderr_tx, stderr_rx) = mpsc::channel();
152 let limit = spec.max_output_bytes;
153 let stdout_thread = thread::spawn(move || {
154 let _ = stdout_tx.send(read_limited(stdout, limit));
155 });
156 let stderr_thread = thread::spawn(move || {
157 let _ = stderr_tx.send(read_limited(stderr, limit));
158 });
159
160 let deadline = Instant::now() + spec.timeout;
161 let mut timed_out = false;
162 let mut output_limited = false;
163 let mut status = None;
164 let mut stdout_result = None;
165 let mut stderr_result = None;
166 loop {
167 if stdout_result.is_none() {
168 if let Ok(result) = stdout_rx.try_recv() {
169 output_limited |= result.is_err();
170 stdout_result = Some(result.unwrap_or_default());
171 }
172 }
173 if stderr_result.is_none() {
174 if let Ok(result) = stderr_rx.try_recv() {
175 output_limited |= result.is_err();
176 stderr_result = Some(result.unwrap_or_default());
177 }
178 }
179 if status.is_none() {
180 status = child
181 .try_wait()
182 .map_err(|source| Error::io("polling bounded candidate", source))?;
183 }
184 if output_limited {
185 let _ = child.kill();
186 status = child.wait().ok();
187 } else if status.is_none() && Instant::now() >= deadline {
188 timed_out = true;
189 let _ = child.kill();
190 status = child.wait().ok();
191 }
192 if ((timed_out || output_limited) && status.is_some())
193 || (status.is_some() && stdout_result.is_some() && stderr_result.is_some())
194 {
195 break;
196 }
197 thread::sleep(Duration::from_millis(2));
198 }
199 if !timed_out && !output_limited {
200 let _ = stdout_thread.join();
201 let _ = stderr_thread.join();
202 }
203 Ok(CommandOutput {
204 exit_code: status.and_then(|status| status.code()),
205 stdout: stdout_result.unwrap_or_default(),
206 stderr: stderr_result.unwrap_or_default(),
207 timed_out,
208 output_limited,
209 })
210}
211
212fn read_limited(mut reader: impl Read, limit: usize) -> io::Result<Vec<u8>> {
213 let mut output = Vec::new();
214 let mut buffer = [0_u8; 4096];
215 loop {
216 let count = reader.read(&mut buffer)?;
217 if count == 0 {
218 return Ok(output);
219 }
220 if output.len().saturating_add(count) > limit {
221 return Err(io::Error::other("candidate output limit exceeded"));
222 }
223 output.extend_from_slice(&buffer[..count]);
224 }
225}
226
227pub trait CandidateValidator {
229 fn validate(&self, transaction: &VerifiedTransaction) -> Result<()>;
231}
232
233#[derive(Debug, Clone)]
235pub struct ExactIdentityValidator {
236 checks: Vec<IdentityCheck>,
237 timeout: Duration,
238 max_output_bytes: usize,
239}
240
241#[derive(Debug, Clone)]
242struct IdentityCheck {
243 member: MemberId,
244 args: Vec<String>,
245 expected_output: String,
246}
247
248impl ExactIdentityValidator {
249 pub fn new(member: MemberId, expected_output: impl Into<String>) -> Self {
251 Self {
252 checks: vec![IdentityCheck {
253 member,
254 args: Vec::new(),
255 expected_output: expected_output.into(),
256 }],
257 timeout: DEFAULT_TIMEOUT,
258 max_output_bytes: DEFAULT_OUTPUT_LIMIT,
259 }
260 }
261
262 pub fn for_members<I>(members: I, expected_output: impl Into<String>) -> Self
264 where
265 I: IntoIterator<Item = MemberId>,
266 {
267 let expected_output = expected_output.into();
268 let checks = members
269 .into_iter()
270 .map(|member| IdentityCheck {
271 member,
272 args: Vec::new(),
273 expected_output: expected_output.clone(),
274 })
275 .collect();
276 Self {
277 checks,
278 timeout: DEFAULT_TIMEOUT,
279 max_output_bytes: DEFAULT_OUTPUT_LIMIT,
280 }
281 }
282
283 pub fn args<I, S>(mut self, args: I) -> Self
285 where
286 I: IntoIterator<Item = S>,
287 S: Into<String>,
288 {
289 let args = args.into_iter().map(Into::into).collect::<Vec<_>>();
290 for check in &mut self.checks {
291 check.args = args.clone();
292 }
293 self
294 }
295
296 pub fn timeout(mut self, timeout: Duration) -> Self {
298 self.timeout = timeout;
299 self
300 }
301
302 pub fn max_output_bytes(mut self, limit: usize) -> Self {
304 self.max_output_bytes = limit;
305 self
306 }
307}
308
309impl CandidateValidator for ExactIdentityValidator {
310 fn validate(&self, transaction: &VerifiedTransaction) -> Result<()> {
311 for check in &self.checks {
312 let program = transaction.staged_path(&check.member)?;
313 let output = run_bounded(
314 &CommandSpec::new(program)
315 .args(check.args.clone())
316 .current_dir(transaction.stage_root())
317 .timeout(self.timeout)
318 .max_output_bytes(self.max_output_bytes),
319 )?;
320 if !output.success()
321 || !output.stderr().is_empty()
322 || output.stdout() != check.expected_output.as_bytes()
323 {
324 return Err(Error::CandidateExecution(format!(
325 "member {} did not produce the exact expected identity",
326 check.member
327 )));
328 }
329 }
330 Ok(())
331 }
332}
333
334#[derive(Debug, Clone)]
336pub struct CrossMemberAgreementValidator(ExactIdentityValidator);
337
338impl CrossMemberAgreementValidator {
339 pub fn new<I>(members: I, expected_output: impl Into<String>) -> Self
341 where
342 I: IntoIterator<Item = MemberId>,
343 {
344 Self(ExactIdentityValidator::for_members(
345 members,
346 expected_output,
347 ))
348 }
349
350 pub fn timeout(mut self, timeout: Duration) -> Self {
352 self.0 = self.0.timeout(timeout);
353 self
354 }
355
356 pub fn max_output_bytes(mut self, limit: usize) -> Self {
358 self.0 = self.0.max_output_bytes(limit);
359 self
360 }
361}
362
363impl CandidateValidator for CrossMemberAgreementValidator {
364 fn validate(&self, transaction: &VerifiedTransaction) -> Result<()> {
365 self.0.validate(transaction)
366 }
367}
368
369pub struct AllValidators<'a> {
371 validators: Vec<&'a dyn CandidateValidator>,
372}
373
374impl<'a> AllValidators<'a> {
375 pub fn new() -> Self {
377 Self {
378 validators: Vec::new(),
379 }
380 }
381
382 pub fn push(mut self, validator: &'a dyn CandidateValidator) -> Self {
384 self.validators.push(validator);
385 self
386 }
387}
388
389impl Default for AllValidators<'_> {
390 fn default() -> Self {
391 Self::new()
392 }
393}
394
395impl CandidateValidator for AllValidators<'_> {
396 fn validate(&self, transaction: &VerifiedTransaction) -> Result<()> {
397 for validator in &self.validators {
398 validator.validate(transaction)?;
399 }
400 Ok(())
401 }
402}
403
404#[derive(Debug)]
406pub struct ValidatedTransaction {
407 pub(crate) verified: VerifiedTransaction,
408}
409
410impl VerifiedTransaction {
411 pub fn validate(self, validator: &dyn CandidateValidator) -> Result<ValidatedTransaction> {
413 if self.artifacts().iter().any(|member| {
414 self.integrity(member.id())
415 .is_none_or(|result| result.status() != IntegrityStatus::Verified)
416 }) {
417 return Err(Error::VerificationFailed(
418 "candidate execution requires verified integrity evidence for every member".into(),
419 ));
420 }
421 validator.validate(&self)?;
422 Ok(ValidatedTransaction { verified: self })
423 }
424}
425
426impl ValidatedTransaction {
427 pub fn artifacts(&self) -> &crate::domain::ArtifactSet {
429 self.verified.artifacts()
430 }
431
432 pub fn staged_path(&self, member: &MemberId) -> Result<std::path::PathBuf> {
438 self.verified.staged_path(member)
439 }
440
441 pub fn commit(self, ownership: CommitOwnership<'_>) -> Result<TransactionReceipt> {
450 let digests = self.verified.verified_digests();
451 self.verified
452 .prepared
453 .commit_inner(ownership, &digests, None, None)
454 }
455
456 pub fn commit_with_post_commit<F, E>(
466 self,
467 ownership: CommitOwnership<'_>,
468 policy: PostCommitFailurePolicy,
469 check: F,
470 ) -> Result<TransactionReceipt>
471 where
472 F: FnOnce() -> std::result::Result<(), E>,
473 E: std::fmt::Display,
474 {
475 let digests = self.verified.verified_digests();
476 let check = Box::new(move || check().map_err(|error| bounded_display(&error)));
477 self.verified
478 .prepared
479 .commit_inner(ownership, &digests, None, Some((policy, check)))
480 }
481
482 #[cfg(test)]
485 pub(crate) fn commit_with_fault(
486 self,
487 ownership: CommitOwnership<'_>,
488 fault: crate::transaction::CommitFault,
489 ) -> Result<TransactionReceipt> {
490 let digests = self.verified.verified_digests();
491 self.verified
492 .prepared
493 .commit_inner(ownership, &digests, Some(fault), None)
494 }
495
496 #[cfg(test)]
497 pub(crate) fn commit_with_post_commit_fault(
498 self,
499 ownership: CommitOwnership<'_>,
500 policy: PostCommitFailurePolicy,
501 fault: crate::transaction::CommitFault,
502 ) -> Result<TransactionReceipt> {
503 let digests = self.verified.verified_digests();
504 self.verified.prepared.commit_inner(
505 ownership,
506 &digests,
507 Some(fault),
508 Some((
509 policy,
510 Box::new(|| Err("injected post-commit failure".to_string())),
511 )),
512 )
513 }
514}
515
516fn bounded_display(value: &impl std::fmt::Display) -> String {
517 struct Bounded(String);
518 impl std::fmt::Write for Bounded {
519 fn write_str(&mut self, text: &str) -> std::fmt::Result {
520 for character in text.chars() {
521 if self.0.len() + character.len_utf8() > 512 {
522 break;
523 }
524 self.0.push(character);
525 }
526 Ok(())
527 }
528 }
529 let mut output = Bounded(String::new());
530 let _ = std::write!(&mut output, "{value}");
531 output.0
532}