1use std::collections::HashMap;
21
22use confium_tc::Message;
23
24#[derive(Debug, Clone, Copy, PartialEq, Eq)]
38pub enum PeerBehavior {
39 Honest,
40 Drop,
41 Tamper,
42 Replay,
43 Malicious,
44 Collusion,
45}
46
47impl PeerBehavior {
48 pub fn from_tag(tag: &str) -> Option<Self> {
52 match tag {
53 "honest" => Some(PeerBehavior::Honest),
54 "byzantine-drop" => Some(PeerBehavior::Drop),
55 "byzantine-tamper" => Some(PeerBehavior::Tamper),
56 "byzantine-replay" => Some(PeerBehavior::Replay),
57 "byzantine-malicious" => Some(PeerBehavior::Malicious),
58 "byzantine-collusion" => Some(PeerBehavior::Collusion),
59 _ => None,
60 }
61 }
62
63 pub fn as_tag(self) -> &'static str {
65 match self {
66 PeerBehavior::Honest => "honest",
67 PeerBehavior::Drop => "byzantine-drop",
68 PeerBehavior::Tamper => "byzantine-tamper",
69 PeerBehavior::Replay => "byzantine-replay",
70 PeerBehavior::Malicious => "byzantine-malicious",
71 PeerBehavior::Collusion => "byzantine-collusion",
72 }
73 }
74}
75
76#[derive(Debug, Clone, PartialEq, Eq)]
81pub struct BehaviorSpec {
82 pub party_id: String,
83 pub behavior: PeerBehavior,
84 pub drop_round: Option<u8>,
86}
87
88#[derive(Debug, Default)]
99pub struct ByzantineTransport {
100 behaviors: HashMap<String, BehaviorSpec>,
101 last_sent: HashMap<String, Vec<Message>>,
103}
104
105impl ByzantineTransport {
106 pub fn new() -> Self {
107 ByzantineTransport::default()
108 }
109
110 pub fn from_specs(specs: Vec<BehaviorSpec>) -> Self {
111 let mut behaviors = HashMap::new();
112 for spec in specs {
113 behaviors.insert(spec.party_id.clone(), spec);
114 }
115 ByzantineTransport {
116 behaviors,
117 last_sent: HashMap::new(),
118 }
119 }
120
121 pub fn set(&mut self, spec: BehaviorSpec) {
123 self.behaviors.insert(spec.party_id.clone(), spec);
124 }
125
126 pub fn behavior_for(&self, party_id: &str) -> PeerBehavior {
128 self.behaviors
129 .get(party_id)
130 .map(|s| s.behavior)
131 .unwrap_or(PeerBehavior::Honest)
132 }
133
134 pub fn route(&mut self, outgoing: &[Message]) -> Vec<Message> {
138 let mut by_sender: HashMap<&str, Vec<&Message>> = HashMap::new();
141 for msg in outgoing {
142 by_sender
143 .entry(msg.from_party_id.as_str())
144 .or_default()
145 .push(msg);
146 }
147
148 let mut delivered = Vec::with_capacity(outgoing.len());
149 for (sender, msgs) in by_sender {
150 let behavior = self.behavior_for(sender);
151 let honest_view: Vec<Message> = msgs.iter().map(|m| (*m).clone()).collect();
154 let delivered_for_sender = match behavior {
155 PeerBehavior::Honest => honest_view.clone(),
156 PeerBehavior::Drop => {
157 let spec = self.behaviors.get(sender);
158 let target_round = spec.and_then(|s| s.drop_round).unwrap_or(1);
159 if msgs.iter().any(|m| m.round == target_round) {
160 Vec::new()
162 } else {
163 honest_view.clone()
164 }
165 }
166 PeerBehavior::Tamper => msgs.iter().map(|m| tamper_message(m)).collect::<Vec<_>>(),
167 PeerBehavior::Malicious | PeerBehavior::Collusion => msgs
168 .iter()
169 .map(|m| malicious_message(m))
170 .collect::<Vec<_>>(),
171 PeerBehavior::Replay => {
172 if let Some(prev) = self.last_sent.get(sender) {
173 prev.clone()
174 } else {
175 honest_view.clone()
178 }
179 }
180 };
181 self.last_sent.insert(sender.to_string(), honest_view);
182 delivered.extend(delivered_for_sender);
183 }
184 delivered
185 }
186}
187
188fn tamper_message(msg: &Message) -> Message {
192 let mut payload = msg.payload.clone();
193 if !payload.is_empty() {
194 payload[0] ^= 0x01;
195 }
196 Message {
197 from_party_id: msg.from_party_id.clone(),
198 to_party_id: msg.to_party_id.clone(),
199 round: msg.round,
200 payload,
201 }
202}
203
204fn malicious_message(msg: &Message) -> Message {
208 Message {
209 from_party_id: msg.from_party_id.clone(),
210 to_party_id: msg.to_party_id.clone(),
211 round: msg.round,
212 payload: b"BYZANTINE-MALICIOUS-PAYLOAD".to_vec(),
213 }
214}
215
216#[cfg(test)]
217mod tests {
218 use super::*;
219
220 fn msg(from: &str, to: Option<&str>, round: u8, payload: &[u8]) -> Message {
221 Message {
222 from_party_id: from.to_string(),
223 to_party_id: to.map(|s| s.to_string()),
224 round,
225 payload: payload.to_vec(),
226 }
227 }
228
229 #[test]
230 fn honest_passes_through_unchanged() {
231 let mut tport = ByzantineTransport::new();
232 tport.set(BehaviorSpec {
233 party_id: "alice".into(),
234 behavior: PeerBehavior::Honest,
235 drop_round: None,
236 });
237 let m = msg("alice", None, 1, &[0xAA, 0xBB]);
238 let out = tport.route(std::slice::from_ref(&m));
239 assert_eq!(out, vec![m]);
240 }
241
242 #[test]
243 fn unconfigured_party_defaults_honest() {
244 let mut tport = ByzantineTransport::new();
245 let m = msg("ghost", None, 1, &[1]);
246 let out = tport.route(std::slice::from_ref(&m));
247 assert_eq!(out.len(), 1);
248 assert_eq!(out[0].payload, vec![1]);
249 }
250
251 #[test]
252 fn drop_actually_drops_target_round() {
253 let mut tport = ByzantineTransport::new();
254 tport.set(BehaviorSpec {
255 party_id: "eve".into(),
256 behavior: PeerBehavior::Drop,
257 drop_round: Some(2),
258 });
259 let r1 = msg("eve", None, 1, &[10]);
261 assert_eq!(tport.route(&[r1]).len(), 1);
262 let r2 = msg("eve", None, 2, &[20]);
264 assert!(
265 tport.route(&[r2]).is_empty(),
266 "round-2 message must be dropped"
267 );
268 let r3 = msg("eve", None, 3, &[30]);
270 assert_eq!(tport.route(&[r3]).len(), 1);
271 }
272
273 #[test]
274 fn drop_defaults_to_round_one_when_unspecified() {
275 let mut tport = ByzantineTransport::new();
276 tport.set(BehaviorSpec {
277 party_id: "eve".into(),
278 behavior: PeerBehavior::Drop,
279 drop_round: None,
280 });
281 let r1 = msg("eve", None, 1, &[1]);
282 assert!(tport.route(&[r1]).is_empty());
283 }
284
285 #[test]
286 fn tamper_flips_a_bit() {
287 let mut tport = ByzantineTransport::new();
288 tport.set(BehaviorSpec {
289 party_id: "mallory".into(),
290 behavior: PeerBehavior::Tamper,
291 drop_round: None,
292 });
293 let original = msg("mallory", None, 1, &[0b0000_0000, 0xFF]);
294 let out = tport.route(std::slice::from_ref(&original));
295 assert_eq!(out.len(), 1);
296 assert_eq!(out[0].payload, vec![0b0000_0001, 0xFF]);
298 assert_ne!(out[0].payload, original.payload);
299 }
300
301 #[test]
302 fn tamper_leaves_empty_payload_untouched() {
303 let mut tport = ByzantineTransport::from_specs(vec![BehaviorSpec {
304 party_id: "m".into(),
305 behavior: PeerBehavior::Tamper,
306 drop_round: None,
307 }]);
308 let empty = msg("m", None, 1, &[]);
309 let out = tport.route(std::slice::from_ref(&empty));
310 assert!(out[0].payload.is_empty());
311 }
312
313 #[test]
314 fn malicious_substitutes_payload() {
315 let mut tport = ByzantineTransport::new();
316 tport.set(BehaviorSpec {
317 party_id: "mallory".into(),
318 behavior: PeerBehavior::Malicious,
319 drop_round: None,
320 });
321 let original = msg("mallory", Some("alice"), 2, &[0xDE, 0xAD]);
322 let out = tport.route(&[original]);
323 assert_eq!(out[0].payload, b"BYZANTINE-MALICIOUS-PAYLOAD");
324 assert_eq!(out[0].from_party_id, "mallory");
326 assert_eq!(out[0].to_party_id.as_deref(), Some("alice"));
327 assert_eq!(out[0].round, 2);
328 }
329
330 #[test]
331 fn collusion_behaves_like_malicious() {
332 let mut tport = ByzantineTransport::from_specs(vec![BehaviorSpec {
333 party_id: "c".into(),
334 behavior: PeerBehavior::Collusion,
335 drop_round: None,
336 }]);
337 let out = tport.route(&[msg("c", None, 1, &[0x01])]);
338 assert_eq!(out[0].payload, b"BYZANTINE-MALICIOUS-PAYLOAD");
339 }
340
341 #[test]
342 fn replay_re_sends_previous_round() {
343 let mut tport = ByzantineTransport::new();
344 tport.set(BehaviorSpec {
345 party_id: "ralph".into(),
346 behavior: PeerBehavior::Replay,
347 drop_round: None,
348 });
349 let r1 = msg("ralph", None, 1, &[11]);
351 let out1 = tport.route(std::slice::from_ref(&r1));
352 assert_eq!(out1, vec![r1.clone()]);
353 let r2 = msg("ralph", None, 2, &[22]);
355 let out2 = tport.route(&[r2]);
356 assert_eq!(out2.len(), 1);
357 assert_eq!(out2[0].payload, vec![11]);
358 assert_eq!(out2[0].round, 1, "replayed message keeps old round number");
359 }
360
361 #[test]
362 fn behavior_round_trips_through_tags() {
363 for behavior in [
364 PeerBehavior::Honest,
365 PeerBehavior::Drop,
366 PeerBehavior::Tamper,
367 PeerBehavior::Replay,
368 PeerBehavior::Malicious,
369 PeerBehavior::Collusion,
370 ] {
371 let tag = behavior.as_tag();
372 assert_eq!(PeerBehavior::from_tag(tag), Some(behavior));
373 }
374 }
375
376 #[test]
377 fn unknown_tag_yields_none() {
378 assert!(PeerBehavior::from_tag("byzantine-shenanigans").is_none());
379 }
380}