/ abzu-transport / tests / wire_fuzzing.rs
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  }