Skip to main content

confium_test_harness/
runner.rs

1//! Vector runner: execute one [`TestVector`] against a registered TC
2//! scheme and produce a [`TestResult`].
3//!
4//! The runner stands up one [`confium_tc::Session`] per party, drives
5//! every session forward one round at a time, routes all outgoing
6//! messages through a [`crate::ByzantineTransport`] (so configured
7//! behaviors take effect), and feeds the surviving messages back as
8//! the next round's incoming set. When every session reports
9//! `complete`, it reads the result and compares it against the
10//! vector's expected bytes.
11//!
12//! The runner is the integration point between the deterministic
13//! environment, the Byzantine wrapper, and the link-time scheme
14//! registry. NIST evaluators point it at a vector file plus a
15//! `confium install`-ed scheme name; everything else is automatic.
16//!
17//! Schemes under test are resolved via the link-time
18//! [`confium_tc::registry`] — the same mechanism real plugins use.
19//! Tests that want to drive a mock scheme register it via
20//! `inventory::submit!` (see the tests at the bottom of this file).
21
22use std::time::Instant;
23
24use confium_tc::Message;
25use confium_tc::Party;
26use confium_tc::PartyList;
27use confium_tc::Session;
28use confium_tc::SessionParams;
29
30use crate::ByzantineTransport;
31use crate::DeterministicEnv;
32use crate::Result;
33use crate::TestResult;
34use crate::TestVector;
35use crate::error::SessionDidNotCompleteSnafu;
36
37/// Maximum rounds before the runner gives up. Generous — real
38/// threshold schemes are 3–7 rounds; this is a safety valve against a
39/// buggy scheme that never completes.
40const MAX_ROUNDS: u8 = 64;
41
42/// Drives a vector through a registered TC scheme.
43pub struct VectorRunner;
44
45impl VectorRunner {
46    /// Execute `vector` against the scheme resolved from
47    /// `vector.scheme.name` via the link-time registry.
48    ///
49    /// All parties run in-process in the calling thread, round by
50    /// round. The deterministic env is seeded from the vector; the
51    /// Byzantine transport applies the vector's per-party behaviors.
52    pub fn run(vector: &TestVector) -> Result<TestResult> {
53        let started = Instant::now();
54
55        // The env exists for side effects (clock, memory) that schemes
56        // under test consult. Seeded from the vector so transcripts are
57        // reproducible.
58        let _env = DeterministicEnv::from_seed(vector.seed_u64()?);
59        let mut tport = ByzantineTransport::from_specs(vector.behavior_specs());
60
61        let parties = build_party_list(vector);
62        let message_bytes = vector.test.message_bytes();
63
64        // One session per party. Session::create resolves the scheme
65        // from the link-time registry, same path real plugins take.
66        let mut sessions: Vec<Session> = Vec::with_capacity(parties.len());
67        for idx in 0..parties.len() {
68            let params = SessionParams {
69                scheme: vector.scheme.name.clone(),
70                parties: parties.clone(),
71                threshold: vector.test.threshold,
72                this_party_idx: idx,
73                local_share: None,
74                message: Some(message_bytes.clone()),
75            };
76            let session = Session::create(&params)?;
77            sessions.push(session);
78        }
79
80        let mut total_messages: u64 = 0;
81        let mut total_bytes: u64 = 0;
82        let mut round: u8 = 0;
83        let last_output;
84
85        // Pending incoming messages per party index, populated by the
86        // previous round's routing.
87        let mut incoming: Vec<Vec<Message>> = vec![Vec::new(); sessions.len()];
88
89        loop {
90            round = round
91                .checked_add(1)
92                .ok_or_else(|| SessionDidNotCompleteSnafu { rounds: round }.build())?;
93            if round > MAX_ROUNDS {
94                return Err(SessionDidNotCompleteSnafu { rounds: round }.build());
95            }
96
97            // Step every non-complete session.
98            let mut outgoing_all: Vec<Message> = Vec::new();
99            let mut all_complete = true;
100            for (idx, session) in sessions.iter_mut().enumerate() {
101                if session.is_complete() {
102                    continue;
103                }
104                let my_id = parties.get(idx)?.id.clone();
105                let incoming_for_me = incoming[idx]
106                    .iter()
107                    .filter(|m| m.is_for(&my_id) && m.from_party_id != my_id)
108                    .cloned()
109                    .collect::<Vec<_>>();
110                let rr = match session.round_step(&incoming_for_me) {
111                    Ok(rr) => rr,
112                    Err(scheme_err) => {
113                        // The scheme signaled an error mid-protocol —
114                        // typically a threshold-violation or
115                        // misbehavior-detection abort. From the harness's
116                        // point of view this is a clean abort, not a
117                        // harness fault: the candidate detected the
118                        // configured Byzantine behavior and refused to
119                        // produce a (potentially invalid) signature.
120                        let elapsed = started.elapsed();
121                        return Ok(TestResult::aborted(
122                            vector,
123                            format!(
124                                "scheme '{}' aborted at round {}: {}",
125                                vector.scheme.name, round, scheme_err
126                            ),
127                            round,
128                            elapsed,
129                        ));
130                    }
131                };
132                for msg in &rr.outgoing {
133                    total_messages += 1;
134                    total_bytes += msg.payload.len() as u64;
135                }
136                outgoing_all.extend(rr.outgoing);
137                if !session.is_complete() {
138                    all_complete = false;
139                }
140            }
141
142            // Route the round's outgoing through the Byzantine wrapper.
143            let delivered = tport.route(&outgoing_all);
144
145            // Partition delivered messages into next round's incoming
146            // buckets per recipient party.
147            for bucket in incoming.iter_mut() {
148                bucket.clear();
149            }
150            for msg in delivered {
151                let recipients: Vec<usize> = match &msg.to_party_id {
152                    None => (0..parties.len()).collect(),
153                    Some(to) => parties
154                        .parties()
155                        .iter()
156                        .position(|p| &p.id == to)
157                        .into_iter()
158                        .collect(),
159                };
160                for ridx in recipients {
161                    if let Some(bucket) = incoming.get_mut(ridx) {
162                        bucket.push(msg.clone());
163                    }
164                }
165            }
166
167            if all_complete {
168                // Read the result from the first session; threshold
169                // schemes produce identical output on every party.
170                last_output = sessions
171                    .first()
172                    .map(|s| s.result().unwrap_or_default())
173                    .unwrap_or_default();
174                break;
175            }
176        }
177
178        let elapsed = started.elapsed();
179        Ok(TestResult::from_run(
180            vector,
181            last_output,
182            total_messages,
183            total_bytes,
184            round,
185            elapsed,
186        ))
187    }
188
189    /// Convenience: parse a vector from a path and run it.
190    pub fn run_path(path: &std::path::Path) -> Result<TestResult> {
191        let vector = TestVector::from_path(path)?;
192        Self::run(&vector)
193    }
194}
195
196/// Build the [`PartyList`] for a vector. If the vector declares
197/// `[[peer_behavior]]` entries for all parties, use those ids in
198/// order; otherwise synthesize `p0..pN-1` and prepend any declared ids.
199fn build_party_list(vector: &TestVector) -> PartyList {
200    let n = vector.test.parties as usize;
201    if vector.peer_behavior.len() == n {
202        let parties = vector
203            .peer_behavior
204            .iter()
205            .map(|e| Party::inproc(e.party_id.clone()))
206            .collect();
207        PartyList::from_parties(parties)
208    } else if !vector.peer_behavior.is_empty() {
209        // Partial: use declared ids first, then synthesize the rest.
210        let mut parties: Vec<Party> = vector
211            .peer_behavior
212            .iter()
213            .map(|e| Party::inproc(e.party_id.clone()))
214            .collect();
215        for i in vector.peer_behavior.len()..n {
216            parties.push(Party::inproc(format!("p{i}")));
217        }
218        PartyList::from_parties(parties)
219    } else {
220        let parties = (0..n).map(|i| Party::inproc(format!("p{i}"))).collect();
221        PartyList::from_parties(parties)
222    }
223}
224
225#[cfg(test)]
226mod tests {
227    use super::*;
228    use crate::vector::SchemeSpec;
229    use crate::vector::TestVectorTest;
230    use confium_tc::Message;
231    use confium_tc::error;
232    use confium_tc::registry::{RoundResult, SessionImpl, TcScheme, TcSchemeKind};
233
234    /// A no-op mock scheme that "completes" on round 1, echoing the
235    /// input message as the output. Registered at link time via
236    /// `inventory::submit!` so the runner can resolve it the same way
237    /// it resolves real plugins.
238    struct RunnerMockScheme;
239
240    impl TcScheme for RunnerMockScheme {
241        fn name(&self) -> &'static str {
242            "runner-mock"
243        }
244        fn kind(&self) -> TcSchemeKind {
245            TcSchemeKind::Signature
246        }
247        fn create_session(
248            &self,
249            params: &SessionParams,
250        ) -> confium_tc::Result<Box<dyn SessionImpl>> {
251            Ok(Box::new(RunnerMockSession {
252                msg: params.message.clone().unwrap_or_default(),
253                done: false,
254            }))
255        }
256    }
257
258    struct RunnerMockSession {
259        msg: Vec<u8>,
260        done: bool,
261    }
262
263    impl SessionImpl for RunnerMockSession {
264        fn round(&mut self, _incoming: &[Message]) -> confium_tc::Result<RoundResult> {
265            self.done = true;
266            Ok(RoundResult::done())
267        }
268        fn result(&self) -> confium_tc::Result<Vec<u8>> {
269            if !self.done {
270                return Err(error::SessionNotCompleteSnafu {}.build());
271            }
272            Ok(self.msg.clone())
273        }
274        fn destroy(&mut self) {
275            self.msg.fill(0);
276        }
277    }
278
279    inventory::submit! {
280        confium_tc::registry::RegisteredScheme {
281            scheme: &RunnerMockScheme as &dyn TcScheme
282        }
283    }
284
285    fn sample_vector(expected: Option<&str>) -> TestVector {
286        TestVector {
287            scheme: SchemeSpec {
288                name: "runner-mock".into(),
289                version: "test".into(),
290            },
291            test: TestVectorTest {
292                parties: 3,
293                threshold: 2,
294                message: "hello".into(),
295                seed: "0x42".into(),
296                expected_signature_hex: expected.unwrap_or("").to_string(),
297            },
298            peer_behavior: vec![],
299            conformance_level: Default::default(),
300            reference: None,
301            expected_round_count: None,
302            share_material: None,
303        }
304    }
305
306    fn hex_str(bytes: &[u8]) -> String {
307        let mut s = String::from("0x");
308        for b in bytes {
309            s.push_str(&format!("{b:02x}"));
310        }
311        s
312    }
313
314    #[test]
315    fn runner_completes_mock_scheme_and_passes() {
316        let expected = hex_str(b"hello");
317        let vector = sample_vector(Some(&expected));
318        let result = VectorRunner::run(&vector).expect("run succeeds");
319        assert_eq!(result.outcome, crate::Outcome::Pass);
320        assert_eq!(result.output, b"hello");
321        assert_eq!(result.rounds, 1);
322    }
323
324    #[test]
325    fn runner_passes_without_expected_bytes() {
326        let vector = sample_vector(None);
327        let result = VectorRunner::run(&vector).unwrap();
328        assert_eq!(result.outcome, crate::Outcome::Pass);
329    }
330
331    #[test]
332    fn runner_records_zero_messages_for_noop_scheme() {
333        let vector = sample_vector(None);
334        let result = VectorRunner::run(&vector).unwrap();
335        assert_eq!(result.messages_exchanged, 0);
336        assert_eq!(result.bytes_exchanged, 0);
337    }
338
339    #[test]
340    fn runner_fails_when_output_mismatches_expected() {
341        let vector = sample_vector(Some("0xdeadbeef"));
342        let result = VectorRunner::run(&vector).unwrap();
343        assert_eq!(result.outcome, crate::Outcome::Fail);
344    }
345}