1use serde::Deserialize;
33
34use crate::Result;
35use crate::byzantine::{BehaviorSpec, PeerBehavior};
36use crate::error;
37use crate::error::VectorSnafu;
38
39#[derive(Debug, Clone, Copy, Deserialize, PartialEq, Eq, Default)]
50pub enum ConformanceLevel {
51 #[default]
54 #[serde(rename = "must_pass")]
55 MustPass,
56 #[serde(rename = "should_pass")]
59 ShouldPass,
60 #[serde(rename = "informational")]
63 Informational,
64}
65
66impl ConformanceLevel {
67 pub fn from_tag(tag: &str) -> Option<Self> {
70 match tag {
71 "must_pass" => Some(ConformanceLevel::MustPass),
72 "should_pass" => Some(ConformanceLevel::ShouldPass),
73 "informational" => Some(ConformanceLevel::Informational),
74 _ => None,
75 }
76 }
77
78 pub fn as_tag(self) -> &'static str {
80 match self {
81 ConformanceLevel::MustPass => "must_pass",
82 ConformanceLevel::ShouldPass => "should_pass",
83 ConformanceLevel::Informational => "informational",
84 }
85 }
86}
87
88#[derive(Debug, Clone, Deserialize, PartialEq, Eq)]
90pub struct TestVector {
91 pub scheme: SchemeSpec,
92 pub test: TestVectorTest,
93 #[serde(default)]
94 pub peer_behavior: Vec<PeerBehaviorEntry>,
95 #[serde(default)]
98 pub conformance_level: ConformanceLevel,
99 #[serde(default)]
103 pub reference: Option<String>,
104 #[serde(default)]
108 pub expected_round_count: Option<u8>,
109 #[serde(default)]
115 pub share_material: Option<String>,
116}
117
118#[derive(Debug, Clone, Deserialize, PartialEq, Eq)]
120pub struct SchemeSpec {
121 pub name: String,
122 #[serde(default)]
123 pub version: String,
124}
125
126#[derive(Debug, Clone, Deserialize, PartialEq, Eq)]
128pub struct TestVectorTest {
129 pub parties: u32,
130 pub threshold: u32,
131 #[serde(default)]
134 pub message: String,
135 #[serde(default)]
137 pub seed: String,
138 #[serde(default)]
140 pub expected_signature_hex: String,
141}
142
143#[derive(Debug, Clone, Deserialize, PartialEq, Eq)]
145pub struct PeerBehaviorEntry {
146 pub party_id: String,
147 #[serde(rename = "type")]
150 pub behavior_tag: String,
151 #[serde(default)]
153 pub drop_round: Option<u8>,
154}
155
156impl TestVector {
157 pub fn parse(toml_str: &str) -> Result<Self> {
159 let raw: TestVector = toml::from_str(toml_str).map_err(|e| {
160 error::VectorSnafu {
161 message: e.to_string(),
162 }
163 .build()
164 })?;
165 raw.validate()?;
166 Ok(raw)
167 }
168
169 pub fn from_path(path: &std::path::Path) -> Result<Self> {
171 let body = std::fs::read_to_string(path).map_err(|e| {
172 VectorSnafu {
173 message: format!("could not read {}: {}", path.display(), e),
174 }
175 .build()
176 })?;
177 Self::parse(&body)
178 }
179
180 fn validate(&self) -> Result<()> {
181 if self.test.parties == 0 {
182 return Err(VectorSnafu {
183 message: "[test] parties must be at least 1".to_string(),
184 }
185 .build());
186 }
187 if self.test.threshold == 0 {
188 return Err(VectorSnafu {
189 message: "[test] threshold must be at least 1".to_string(),
190 }
191 .build());
192 }
193 if self.test.threshold > self.test.parties {
194 return Err(VectorSnafu {
195 message: format!(
196 "[test] threshold {} exceeds parties {}",
197 self.test.threshold, self.test.parties
198 ),
199 }
200 .build());
201 }
202 for entry in &self.peer_behavior {
204 if PeerBehavior::from_tag(&entry.behavior_tag).is_none() {
205 return Err(VectorSnafu {
206 message: format!(
207 "unknown peer_behavior type '{}' for party '{}'",
208 entry.behavior_tag, entry.party_id
209 ),
210 }
211 .build());
212 }
213 }
214 if let Some(rc) = self.expected_round_count {
215 if rc == 0 {
216 return Err(VectorSnafu {
217 message: "expected_round_count must be at least 1".to_string(),
218 }
219 .build());
220 }
221 }
222 Ok(())
223 }
224
225 pub fn seed_u64(&self) -> Result<u64> {
228 decode_hex_u64(&self.test.seed).ok_or_else(|| {
229 VectorSnafu {
230 message: format!("could not decode seed '{}' as hex u64", self.test.seed),
231 }
232 .build()
233 })
234 }
235
236 pub fn behavior_specs(&self) -> Vec<BehaviorSpec> {
239 self.peer_behavior
240 .iter()
241 .map(|entry| BehaviorSpec {
242 party_id: entry.party_id.clone(),
243 behavior: PeerBehavior::from_tag(&entry.behavior_tag)
244 .unwrap_or(PeerBehavior::Honest),
245 drop_round: entry.drop_round,
246 })
247 .collect()
248 }
249}
250
251impl TestVectorTest {
252 pub fn message_bytes(&self) -> Vec<u8> {
255 if let Some(rest) = self.message.strip_prefix("0x") {
256 decode_hex_bytes(rest).unwrap_or_else(|| self.message.as_bytes().to_vec())
257 } else {
258 self.message.as_bytes().to_vec()
259 }
260 }
261
262 pub fn expected_bytes(&self) -> Option<Vec<u8>> {
264 if self.expected_signature_hex.is_empty() {
265 return None;
266 }
267 let stripped = self
268 .expected_signature_hex
269 .strip_prefix("0x")
270 .unwrap_or(&self.expected_signature_hex);
271 decode_hex_bytes(stripped)
272 }
273}
274
275fn decode_hex_u64(s: &str) -> Option<u64> {
277 let s = s.strip_prefix("0x").unwrap_or(s);
278 let s = s.trim();
279 if s.is_empty() {
280 return Some(0);
281 }
282 u64::from_str_radix(s, 16).ok()
283}
284
285fn decode_hex_bytes(s: &str) -> Option<Vec<u8>> {
288 let s = s.trim();
289 if s.len() % 2 != 0 {
290 return None;
291 }
292 let mut out = Vec::with_capacity(s.len() / 2);
293 let bytes = s.as_bytes();
294 let mut i = 0;
295 while i < bytes.len() {
296 let hi = hex_nibble(bytes[i])?;
297 let lo = hex_nibble(bytes[i + 1])?;
298 out.push((hi << 4) | lo);
299 i += 2;
300 }
301 Some(out)
302}
303
304fn hex_nibble(b: u8) -> Option<u8> {
305 match b {
306 b'0'..=b'9' => Some(b - b'0'),
307 b'a'..=b'f' => Some(b - b'a' + 10),
308 b'A'..=b'F' => Some(b - b'A' + 10),
309 _ => None,
310 }
311}
312
313#[cfg(test)]
314mod tests {
315 use super::*;
316
317 const SAMPLE: &str = include_str!("../vectors/sample.toml");
318
319 #[test]
320 fn sample_parses_correctly() {
321 let v = TestVector::parse(SAMPLE).expect("sample.toml must parse");
322 assert_eq!(v.scheme.name, "FROST-ed25519");
323 assert_eq!(v.scheme.version, "draft-irtf-cfrg-frost-13");
324 assert_eq!(v.test.parties, 5);
325 assert_eq!(v.test.threshold, 3);
326 assert_eq!(v.test.message_bytes(), b"hello world");
327 assert_eq!(v.peer_behavior.len(), 3);
328 assert_eq!(v.peer_behavior[0].party_id, "alice");
329 assert_eq!(v.peer_behavior[0].behavior_tag, "honest");
330 assert_eq!(v.peer_behavior[2].behavior_tag, "byzantine-drop");
331 }
332
333 #[test]
334 fn seed_decodes_as_hex_u64() {
335 let v = TestVector::parse(SAMPLE).unwrap();
336 assert_eq!(v.seed_u64().unwrap(), 0xDEAD_BEEF);
337 }
338
339 #[test]
340 fn behavior_specs_round_trip() {
341 let v = TestVector::parse(SAMPLE).unwrap();
342 let specs = v.behavior_specs();
343 assert_eq!(specs.len(), 3);
344 assert_eq!(specs[2].behavior, PeerBehavior::Drop);
345 assert_eq!(specs[2].drop_round, Some(2));
346 }
347
348 #[test]
349 fn rejects_zero_parties() {
350 let bad = r#"
351[scheme]
352name = "x"
353version = "1"
354[test]
355parties = 0
356threshold = 1
357"#;
358 assert!(TestVector::parse(bad).is_err());
359 }
360
361 #[test]
362 fn rejects_threshold_above_parties() {
363 let bad = r#"
364[scheme]
365name = "x"
366[test]
367parties = 2
368threshold = 5
369"#;
370 assert!(TestVector::parse(bad).is_err());
371 }
372
373 #[test]
374 fn rejects_unknown_behavior_tag() {
375 let bad = r#"
376[scheme]
377name = "x"
378[test]
379parties = 2
380threshold = 1
381[[peer_behavior]]
382party_id = "a"
383type = "byzantine-flip-table"
384"#;
385 let err = TestVector::parse(bad).unwrap_err();
386 assert!(format!("{err}").contains("unknown peer_behavior type"));
387 }
388
389 #[test]
390 fn binary_message_via_hex_prefix() {
391 let v = TestVector::parse(
392 r#"
393[scheme]
394name = "x"
395[test]
396parties = 2
397threshold = 1
398message = "0xdeadbeef"
399"#,
400 )
401 .unwrap();
402 assert_eq!(v.test.message_bytes(), vec![0xDE, 0xAD, 0xBE, 0xEF]);
403 }
404
405 #[test]
406 fn expected_signature_decodes_when_present() {
407 let v = TestVector::parse(
408 r#"
409[scheme]
410name = "x"
411[test]
412parties = 2
413threshold = 1
414expected_signature_hex = "0x01020304"
415"#,
416 )
417 .unwrap();
418 assert_eq!(v.test.expected_bytes(), Some(vec![1, 2, 3, 4]));
419 }
420
421 #[test]
422 fn expected_signature_absent_when_empty() {
423 let v = TestVector::parse(
424 r#"
425[scheme]
426name = "x"
427[test]
428parties = 2
429threshold = 1
430"#,
431 )
432 .unwrap();
433 assert!(v.test.expected_bytes().is_none());
434 }
435
436 #[test]
437 fn conformance_level_defaults_to_must_pass() {
438 let v = TestVector::parse(
439 r#"
440[scheme]
441name = "x"
442[test]
443parties = 2
444threshold = 1
445"#,
446 )
447 .unwrap();
448 assert_eq!(v.conformance_level, ConformanceLevel::MustPass);
449 }
450
451 #[test]
452 fn conformance_level_parses_should_pass() {
453 let v = TestVector::parse(
454 r#"
455conformance_level = "should_pass"
456[scheme]
457name = "x"
458[test]
459parties = 2
460threshold = 1
461"#,
462 )
463 .unwrap();
464 assert_eq!(v.conformance_level, ConformanceLevel::ShouldPass);
465 }
466
467 #[test]
468 fn conformance_level_parses_informational() {
469 let v = TestVector::parse(
470 r#"
471conformance_level = "informational"
472[scheme]
473name = "x"
474[test]
475parties = 2
476threshold = 1
477"#,
478 )
479 .unwrap();
480 assert_eq!(v.conformance_level, ConformanceLevel::Informational);
481 }
482
483 #[test]
484 fn conformance_level_rejects_unknown_tag_with_useful_error() {
485 let bad = r#"
486conformance_level = "maybe_pass"
487[scheme]
488name = "x"
489[test]
490parties = 2
491threshold = 1
492"#;
493 let err = TestVector::parse(bad).unwrap_err();
494 let msg = format!("{err}");
495 assert!(
496 msg.contains("conformance_level") || msg.contains("maybe_pass"),
497 "error must point at the bad conformance_level tag: {msg}"
498 );
499 }
500
501 #[test]
502 fn reference_and_share_material_parse() {
503 let v = TestVector::parse(
504 r#"
505reference = "https://example.org/spec"
506share_material = "nist-dkg-A"
507expected_round_count = 4
508[scheme]
509name = "x"
510[test]
511parties = 2
512threshold = 1
513"#,
514 )
515 .unwrap();
516 assert_eq!(v.reference.as_deref(), Some("https://example.org/spec"));
517 assert_eq!(v.share_material.as_deref(), Some("nist-dkg-A"));
518 assert_eq!(v.expected_round_count, Some(4));
519 }
520
521 #[test]
522 fn rejects_zero_expected_round_count() {
523 let bad = r#"
524expected_round_count = 0
525[scheme]
526name = "x"
527[test]
528parties = 2
529threshold = 1
530"#;
531 let err = TestVector::parse(bad).unwrap_err();
532 assert!(
533 format!("{err}").contains("expected_round_count"),
534 "error must name the offending field"
535 );
536 }
537
538 #[test]
539 fn malformed_toml_surfaces_useful_error() {
540 let bad = "this is not toml at all {{{";
544 let err = TestVector::parse(bad).unwrap_err();
545 let msg = format!("{err}");
546 assert!(
547 msg.to_lowercase().contains("malformed") || msg.contains("expected"),
548 "error must describe the malformation: {msg}"
549 );
550 }
551
552 #[test]
553 fn missing_scheme_name_surfaces_useful_error() {
554 let bad = r#"
557[scheme]
558[test]
559parties = 2
560threshold = 1
561"#;
562 let err = TestVector::parse(bad).unwrap_err();
563 let msg = format!("{err}");
564 assert!(
565 msg.contains("name") || msg.contains("scheme"),
566 "error must point at the missing scheme.name: {msg}"
567 );
568 }
569
570 #[test]
571 fn conformance_level_round_trips_through_tags() {
572 for level in [
573 ConformanceLevel::MustPass,
574 ConformanceLevel::ShouldPass,
575 ConformanceLevel::Informational,
576 ] {
577 let tag = level.as_tag();
578 assert_eq!(ConformanceLevel::from_tag(tag), Some(level));
579 }
580 }
581}