wire_fuzzing.rs
1 //! Wire Format Fuzzing Tests 2 //! 3 //! Property-based tests using proptest to validate serialization invariants. 4 5 use abzu_transport::wire::AbzuFrame; 6 use proptest::prelude::*; 7 8 /// Maximum frame size for testing 9 const MAX_FRAME_SIZE: usize = 65536; 10 11 /// Generate arbitrary 32-byte keys/CIDs 12 fn arb_key32() -> impl Strategy<Value = [u8; 32]> { 13 prop::array::uniform32(any::<u8>()) 14 } 15 16 /// Generate arbitrary payload data 17 fn arb_payload() -> impl Strategy<Value = Vec<u8>> { 18 prop::collection::vec(any::<u8>(), 0..1024) 19 } 20 21 /// Generate arbitrary AbzuFrame variants 22 fn arb_frame() -> impl Strategy<Value = AbzuFrame> { 23 prop_oneof![ 24 // KeepAlive (no fields) 25 Just(AbzuFrame::KeepAlive), 26 27 // Chunk 28 (arb_key32(), arb_payload()).prop_map(|(cid, data)| AbzuFrame::Chunk { cid, data }), 29 30 // Route 31 (arb_key32(), arb_key32(), arb_payload()) 32 .prop_map(|(target, next_hop, payload)| AbzuFrame::Route { target, next_hop, payload }), 33 34 // Hello (with version fields and optional challenge) 35 (any::<u16>(), any::<u16>(), arb_key32(), any::<u64>(), proptest::option::of(arb_key32())) 36 .prop_map(|(version_major, version_minor, ephemeral_pub, timestamp, challenge)| AbzuFrame::Hello { version_major, version_minor, ephemeral_pub, timestamp, challenge }), 37 38 // HelloAck (with version fields and optional identity) 39 (any::<u16>(), any::<u16>(), arb_key32(), arb_payload(), proptest::option::of(arb_key32()), arb_payload()) 40 .prop_map(|(version_major, version_minor, ephemeral_pub, confirmation, identity_pubkey, identity_signature)| AbzuFrame::HelloAck { version_major, version_minor, ephemeral_pub, confirmation, identity_pubkey, identity_signature }), 41 42 // Request 43 (arb_key32(), arb_key32()) 44 .prop_map(|(cid, requester)| AbzuFrame::Request { cid, requester }), 45 46 // Chat 47 (any::<u64>(), arb_key32(), arb_payload(), any::<u64>()) 48 .prop_map(|(id, to, msg, timestamp)| AbzuFrame::Chat { id, to, msg, timestamp }), 49 50 // ChatAck 51 any::<u64>().prop_map(AbzuFrame::chat_ack), 52 53 // Cover (sized noise) 54 (8usize..512).prop_map(AbzuFrame::cover), 55 ] 56 } 57 58 proptest! { 59 /// Property: encode(frame) then decode returns the original frame 60 #[test] 61 fn roundtrip_encode_decode(frame in arb_frame()) { 62 let encoded = frame.encode().expect("Encoding should not fail"); 63 let decoded = AbzuFrame::decode(&encoded).expect("Decoding should not fail"); 64 prop_assert_eq!(frame, decoded); 65 } 66 67 /// Property: encoded size is bounded by MAX_FRAME_SIZE 68 #[test] 69 fn encoded_size_bounded(frame in arb_frame()) { 70 let encoded = frame.encode().expect("Encoding should not fail"); 71 prop_assert!( 72 encoded.len() <= MAX_FRAME_SIZE, 73 "Encoded size {} exceeds max {}", encoded.len(), MAX_FRAME_SIZE 74 ); 75 } 76 77 /// Property: decoding random bytes should not panic 78 /// (it may error, but must not crash) 79 #[test] 80 fn decode_garbage_no_panic(data in prop::collection::vec(any::<u8>(), 0..256)) { 81 // This should either succeed or return an error, but never panic 82 let _ = AbzuFrame::decode(&data); 83 } 84 85 /// Property: KeepAlive is always tiny (< 4 bytes) 86 #[test] 87 fn keepalive_compact(_iter in 0..10u32) { 88 let frame = AbzuFrame::KeepAlive; 89 let encoded = frame.encode().unwrap(); 90 prop_assert!(encoded.len() < 4, "KeepAlive too large: {} bytes", encoded.len()); 91 } 92 93 /// Property: Cover frames have exact wire size (or rounds up for impossible sizes) 94 /// 95 /// Note: Due to varint encoding, size 130 is impossible (128 noise = 131 bytes, 96 /// 127 noise = 129 bytes). For this edge case, we accept size >= target. 97 #[test] 98 fn cover_exact_size(target_size in 8usize..1024) { 99 let frame = AbzuFrame::cover(target_size); 100 let encoded = frame.encode().expect("Encoding should not fail"); 101 102 // Size 130 is impossible due to varint boundary - accept >= target 103 if target_size == 130 { 104 prop_assert!( 105 encoded.len() >= target_size, 106 "Cover wire size {} < target {}", encoded.len(), target_size 107 ); 108 } else { 109 prop_assert_eq!( 110 encoded.len(), target_size, 111 "Cover wire size {} != target {}", encoded.len(), target_size 112 ); 113 } 114 } 115 116 /// Property: Chat frames preserve all fields 117 #[test] 118 fn chat_field_preservation( 119 id in any::<u64>(), 120 to in arb_key32(), 121 msg in prop::collection::vec(any::<u8>(), 0..256), 122 timestamp in any::<u64>() 123 ) { 124 let frame = AbzuFrame::chat(id, to, msg.clone(), timestamp); 125 let encoded = frame.encode().unwrap(); 126 let decoded = AbzuFrame::decode(&encoded).unwrap(); 127 128 if let AbzuFrame::Chat { id: d_id, to: d_to, msg: d_msg, timestamp: d_ts } = decoded { 129 prop_assert_eq!(id, d_id); 130 prop_assert_eq!(to, d_to); 131 prop_assert_eq!(msg, d_msg); 132 prop_assert_eq!(timestamp, d_ts); 133 } else { 134 prop_assert!(false, "Expected Chat frame"); 135 } 136 } 137 138 /// Property: Route frames preserve onion structure 139 #[test] 140 fn route_field_preservation( 141 target in arb_key32(), 142 next_hop in arb_key32(), 143 payload in arb_payload() 144 ) { 145 let frame = AbzuFrame::route(target, next_hop, payload.clone()); 146 let encoded = frame.encode().unwrap(); 147 let decoded = AbzuFrame::decode(&encoded).unwrap(); 148 149 if let AbzuFrame::Route { target: t, next_hop: n, payload: p } = decoded { 150 prop_assert_eq!(target, t); 151 prop_assert_eq!(next_hop, n); 152 prop_assert_eq!(payload, p); 153 } else { 154 prop_assert!(false, "Expected Route frame"); 155 } 156 } 157 } 158 159 /// Test that double-encoding is idempotent 160 #[test] 161 fn double_encode_idempotent() { 162 let frame = AbzuFrame::chat(1, [0xAB; 32], b"test".to_vec(), 12345); 163 let encoded1 = frame.encode().unwrap(); 164 let decoded = AbzuFrame::decode(&encoded1).unwrap(); 165 let encoded2 = decoded.encode().unwrap(); 166 assert_eq!(encoded1, encoded2, "Double encoding should be identical"); 167 } 168 169 /// Test truncated input handling 170 #[test] 171 fn truncated_input_graceful() { 172 let frame = AbzuFrame::chunk([0xAB; 32], vec![1, 2, 3, 4, 5]); 173 let encoded = frame.encode().unwrap(); 174 175 // Try decoding progressively shorter slices 176 for len in 0..encoded.len() { 177 let truncated = &encoded[..len]; 178 // Should not panic, may return error 179 let result = AbzuFrame::decode(truncated); 180 assert!(result.is_err(), "Truncated input should fail: len={}", len); 181 } 182 }