1use std::io::{self, Read};
2use std::path::PathBuf;
3use std::process::{Command, Stdio};
4use std::sync::mpsc;
5use std::thread;
6use std::time::{Duration, Instant};
7
8use crate::domain::{CommitOwnership, MemberId};
9use crate::error::{Error, Result};
10use crate::integrity::{IntegrityStatus, VerifiedTransaction};
11use crate::transaction::TransactionReceipt;
12
13const DEFAULT_TIMEOUT: Duration = Duration::from_secs(5);
14const DEFAULT_OUTPUT_LIMIT: usize = 64 * 1024;
15
16#[derive(Debug, Clone)]
18pub struct CommandSpec {
19 program: PathBuf,
20 args: Vec<String>,
21 current_dir: Option<PathBuf>,
22 timeout: Duration,
23 max_output_bytes: usize,
24 environment: Vec<(String, String)>,
25}
26
27impl CommandSpec {
28 pub fn new(program: impl Into<PathBuf>) -> Self {
30 Self {
31 program: program.into(),
32 args: Vec::new(),
33 current_dir: None,
34 timeout: DEFAULT_TIMEOUT,
35 max_output_bytes: DEFAULT_OUTPUT_LIMIT,
36 environment: Vec::new(),
37 }
38 }
39
40 pub fn arg(mut self, argument: impl Into<String>) -> Self {
42 self.args.push(argument.into());
43 self
44 }
45
46 pub fn args<I, S>(mut self, arguments: I) -> Self
48 where
49 I: IntoIterator<Item = S>,
50 S: Into<String>,
51 {
52 self.args.extend(arguments.into_iter().map(Into::into));
53 self
54 }
55
56 pub fn current_dir(mut self, path: impl Into<PathBuf>) -> Self {
58 self.current_dir = Some(path.into());
59 self
60 }
61
62 pub fn timeout(mut self, timeout: Duration) -> Self {
64 self.timeout = timeout;
65 self
66 }
67
68 pub fn max_output_bytes(mut self, limit: usize) -> Self {
70 self.max_output_bytes = limit;
71 self
72 }
73
74 pub fn environment(mut self, name: impl Into<String>, value: impl Into<String>) -> Self {
76 self.environment.push((name.into(), value.into()));
77 self
78 }
79}
80
81#[derive(Debug, Clone, PartialEq, Eq)]
83pub struct CommandOutput {
84 exit_code: Option<i32>,
85 stdout: Vec<u8>,
86 stderr: Vec<u8>,
87 timed_out: bool,
88 output_limited: bool,
89}
90
91impl CommandOutput {
92 pub fn exit_code(&self) -> Option<i32> {
94 self.exit_code
95 }
96
97 pub fn stdout(&self) -> &[u8] {
99 &self.stdout
100 }
101
102 pub fn stderr(&self) -> &[u8] {
104 &self.stderr
105 }
106
107 pub fn timed_out(&self) -> bool {
109 self.timed_out
110 }
111
112 pub fn output_limited(&self) -> bool {
114 self.output_limited
115 }
116
117 pub fn success(&self) -> bool {
119 !self.timed_out && !self.output_limited && self.exit_code == Some(0)
120 }
121}
122
123pub fn run_bounded(spec: &CommandSpec) -> Result<CommandOutput> {
125 let mut command = Command::new(&spec.program);
126 command
127 .args(&spec.args)
128 .env_clear()
129 .stdin(Stdio::null())
130 .stdout(Stdio::piped())
131 .stderr(Stdio::piped());
132 if let Some(current_dir) = &spec.current_dir {
133 command.current_dir(current_dir);
134 }
135 for (name, value) in &spec.environment {
136 command.env(name, value);
137 }
138 let mut child = command
139 .spawn()
140 .map_err(|source| Error::io("spawning bounded candidate", source))?;
141 let stdout = child
142 .stdout
143 .take()
144 .ok_or_else(|| Error::CandidateExecution("missing stdout pipe".into()))?;
145 let stderr = child
146 .stderr
147 .take()
148 .ok_or_else(|| Error::CandidateExecution("missing stderr pipe".into()))?;
149 let (stdout_tx, stdout_rx) = mpsc::channel();
150 let (stderr_tx, stderr_rx) = mpsc::channel();
151 let limit = spec.max_output_bytes;
152 let stdout_thread = thread::spawn(move || {
153 let _ = stdout_tx.send(read_limited(stdout, limit));
154 });
155 let stderr_thread = thread::spawn(move || {
156 let _ = stderr_tx.send(read_limited(stderr, limit));
157 });
158
159 let deadline = Instant::now() + spec.timeout;
160 let mut timed_out = false;
161 let mut output_limited = false;
162 let mut status = None;
163 let mut stdout_result = None;
164 let mut stderr_result = None;
165 loop {
166 if stdout_result.is_none() {
167 if let Ok(result) = stdout_rx.try_recv() {
168 output_limited |= result.is_err();
169 stdout_result = Some(result.unwrap_or_default());
170 }
171 }
172 if stderr_result.is_none() {
173 if let Ok(result) = stderr_rx.try_recv() {
174 output_limited |= result.is_err();
175 stderr_result = Some(result.unwrap_or_default());
176 }
177 }
178 if status.is_none() {
179 status = child
180 .try_wait()
181 .map_err(|source| Error::io("polling bounded candidate", source))?;
182 }
183 if output_limited {
184 let _ = child.kill();
185 status = child.wait().ok();
186 } else if status.is_none() && Instant::now() >= deadline {
187 timed_out = true;
188 let _ = child.kill();
189 status = child.wait().ok();
190 }
191 if ((timed_out || output_limited) && status.is_some())
192 || (status.is_some() && stdout_result.is_some() && stderr_result.is_some())
193 {
194 break;
195 }
196 thread::sleep(Duration::from_millis(2));
197 }
198 if !timed_out && !output_limited {
199 let _ = stdout_thread.join();
200 let _ = stderr_thread.join();
201 }
202 Ok(CommandOutput {
203 exit_code: status.and_then(|status| status.code()),
204 stdout: stdout_result.unwrap_or_default(),
205 stderr: stderr_result.unwrap_or_default(),
206 timed_out,
207 output_limited,
208 })
209}
210
211fn read_limited(mut reader: impl Read, limit: usize) -> io::Result<Vec<u8>> {
212 let mut output = Vec::new();
213 let mut buffer = [0_u8; 4096];
214 loop {
215 let count = reader.read(&mut buffer)?;
216 if count == 0 {
217 return Ok(output);
218 }
219 if output.len().saturating_add(count) > limit {
220 return Err(io::Error::other("candidate output limit exceeded"));
221 }
222 output.extend_from_slice(&buffer[..count]);
223 }
224}
225
226pub trait CandidateValidator {
228 fn validate(&self, transaction: &VerifiedTransaction) -> Result<()>;
230}
231
232#[derive(Debug, Clone)]
234pub struct ExactIdentityValidator {
235 checks: Vec<IdentityCheck>,
236 timeout: Duration,
237 max_output_bytes: usize,
238}
239
240#[derive(Debug, Clone)]
241struct IdentityCheck {
242 member: MemberId,
243 args: Vec<String>,
244 expected_output: String,
245}
246
247impl ExactIdentityValidator {
248 pub fn new(member: MemberId, expected_output: impl Into<String>) -> Self {
250 Self {
251 checks: vec![IdentityCheck {
252 member,
253 args: Vec::new(),
254 expected_output: expected_output.into(),
255 }],
256 timeout: DEFAULT_TIMEOUT,
257 max_output_bytes: DEFAULT_OUTPUT_LIMIT,
258 }
259 }
260
261 pub fn for_members<I>(members: I, expected_output: impl Into<String>) -> Self
263 where
264 I: IntoIterator<Item = MemberId>,
265 {
266 let expected_output = expected_output.into();
267 let checks = members
268 .into_iter()
269 .map(|member| IdentityCheck {
270 member,
271 args: Vec::new(),
272 expected_output: expected_output.clone(),
273 })
274 .collect();
275 Self {
276 checks,
277 timeout: DEFAULT_TIMEOUT,
278 max_output_bytes: DEFAULT_OUTPUT_LIMIT,
279 }
280 }
281
282 pub fn args<I, S>(mut self, args: I) -> Self
284 where
285 I: IntoIterator<Item = S>,
286 S: Into<String>,
287 {
288 let args = args.into_iter().map(Into::into).collect::<Vec<_>>();
289 for check in &mut self.checks {
290 check.args = args.clone();
291 }
292 self
293 }
294
295 pub fn timeout(mut self, timeout: Duration) -> Self {
297 self.timeout = timeout;
298 self
299 }
300
301 pub fn max_output_bytes(mut self, limit: usize) -> Self {
303 self.max_output_bytes = limit;
304 self
305 }
306}
307
308impl CandidateValidator for ExactIdentityValidator {
309 fn validate(&self, transaction: &VerifiedTransaction) -> Result<()> {
310 for check in &self.checks {
311 let program = transaction.staged_path(&check.member)?;
312 let output = run_bounded(
313 &CommandSpec::new(program)
314 .args(check.args.clone())
315 .current_dir(transaction.stage_root())
316 .timeout(self.timeout)
317 .max_output_bytes(self.max_output_bytes),
318 )?;
319 if !output.success()
320 || !output.stderr().is_empty()
321 || output.stdout() != check.expected_output.as_bytes()
322 {
323 return Err(Error::CandidateExecution(format!(
324 "member {} did not produce the exact expected identity",
325 check.member
326 )));
327 }
328 }
329 Ok(())
330 }
331}
332
333#[derive(Debug, Clone)]
335pub struct CrossMemberAgreementValidator(ExactIdentityValidator);
336
337impl CrossMemberAgreementValidator {
338 pub fn new<I>(members: I, expected_output: impl Into<String>) -> Self
340 where
341 I: IntoIterator<Item = MemberId>,
342 {
343 Self(ExactIdentityValidator::for_members(
344 members,
345 expected_output,
346 ))
347 }
348
349 pub fn timeout(mut self, timeout: Duration) -> Self {
351 self.0 = self.0.timeout(timeout);
352 self
353 }
354
355 pub fn max_output_bytes(mut self, limit: usize) -> Self {
357 self.0 = self.0.max_output_bytes(limit);
358 self
359 }
360}
361
362impl CandidateValidator for CrossMemberAgreementValidator {
363 fn validate(&self, transaction: &VerifiedTransaction) -> Result<()> {
364 self.0.validate(transaction)
365 }
366}
367
368pub struct AllValidators<'a> {
370 validators: Vec<&'a dyn CandidateValidator>,
371}
372
373impl<'a> AllValidators<'a> {
374 pub fn new() -> Self {
376 Self {
377 validators: Vec::new(),
378 }
379 }
380
381 pub fn push(mut self, validator: &'a dyn CandidateValidator) -> Self {
383 self.validators.push(validator);
384 self
385 }
386}
387
388impl Default for AllValidators<'_> {
389 fn default() -> Self {
390 Self::new()
391 }
392}
393
394impl CandidateValidator for AllValidators<'_> {
395 fn validate(&self, transaction: &VerifiedTransaction) -> Result<()> {
396 for validator in &self.validators {
397 validator.validate(transaction)?;
398 }
399 Ok(())
400 }
401}
402
403#[derive(Debug)]
405pub struct ValidatedTransaction {
406 pub(crate) verified: VerifiedTransaction,
407}
408
409impl VerifiedTransaction {
410 pub fn validate(self, validator: &dyn CandidateValidator) -> Result<ValidatedTransaction> {
412 if self.artifacts().iter().any(|member| {
413 self.integrity(member.id())
414 .is_none_or(|result| result.status() != IntegrityStatus::Verified)
415 }) {
416 return Err(Error::VerificationFailed(
417 "candidate execution requires verified integrity evidence for every member".into(),
418 ));
419 }
420 validator.validate(&self)?;
421 Ok(ValidatedTransaction { verified: self })
422 }
423}
424
425impl ValidatedTransaction {
426 pub fn artifacts(&self) -> &crate::domain::ArtifactSet {
428 self.verified.artifacts()
429 }
430
431 pub fn staged_path(&self, member: &MemberId) -> Result<std::path::PathBuf> {
437 self.verified.staged_path(member)
438 }
439
440 pub fn commit(self, ownership: CommitOwnership<'_>) -> Result<TransactionReceipt> {
449 let digests = self.verified.verified_digests();
450 self.verified
451 .prepared
452 .commit_inner(ownership, &digests, None)
453 }
454
455 #[cfg(test)]
458 pub(crate) fn commit_with_fault(
459 self,
460 ownership: CommitOwnership<'_>,
461 fault: crate::transaction::CommitFault,
462 ) -> Result<TransactionReceipt> {
463 let digests = self.verified.verified_digests();
464 self.verified
465 .prepared
466 .commit_inner(ownership, &digests, Some(fault))
467 }
468}